分享一次機房出口頻寬跑滿的案例

2022-07-08 15:01:02

背景

有客戶反饋6月12號下午14:00左右xxx域名有大量資料傳出,一度佔滿出口頻寬,導致系統緩慢,希望我們儘快查一下,頻寬監控如下圖所示:
 

 

 

xxx正是我們部署在客戶機房的招投標系統所使用的的域名。

 

概念

工作這麼多年,頻寬跑滿這事也只是聽說過,倒沒親身經歷過,落到自己頭上的時候多少有點不知所措,正式開始解決問題之前先把概念搞清楚,免得最終脫離目標。

 

網路頻寬:網路頻寬是指在單位時間(一般指的是1秒鐘)內能傳輸的資料量。網路和高速公路類似,頻寬越大,就類似高速公路的車道越多,其通行能力越強。數位資訊流的基本單位是bit(位元),時間的基本單位是s(秒),因此bit/s(位元/秒)是描述頻寬的單位,1bit/s是頻寬的基本單位。

來源於百度百科

 

網速:網速一般是指電腦或手機上網時,上傳和下載資料時,請求和返回資料所用的時間長短。電腦中存取資料的單位是「位元組」,即byte(大寫B),而資料通訊是以「字位」作為單位,即bit(小寫b),兩者之間的關係是1byte=8bit。電信業務中提到的網速為1M、2M、3M、4M等是以資料通訊的字位作為單位計算的。所以電腦軟體顯示的下載速度為200KB時,實際線路連線速率不小於1.6Mbit(1600Kbit)。

來源於百度百科

 

下載、上傳:網路資料傳輸分為傳送資料和接收資料兩部分。上傳就是向外部傳送資料,下載為從外部接收資料。他們都受網路頻寬和裝置效能制約。在日常網路傳輸中大致1Mbps=1024/8KB/s=128KB/s(1/8)。例如上行的網路頻寬為100Mbps,那麼最大上傳速度就是12800KB/s,也就是12.5MB/s。

使用者申請的寬頻業務速率指技術上所能達到的最大理論速率值,使用者上網時還受到使用者電腦軟硬體的設定、所瀏覽網站的位置、對端網站頻寬等情況的影響,故使用者上網時的速率通常低於理論速率值。理論上:2M(即2Mb/s)寬頻理論速率是:256KB/s(即2048Kb/s),實際速率大約為103--200KB/s;4M(即4Mb/s)的寬頻理論速率是:512KB/s,實際速率大約為200---440KB/s。以此類推。

來源於測速網

 

大概總結一下,一般提到的1M頻寬、10M頻寬是指Mb/s,小b是代表位元,使用者從瀏覽器下載時的速率顯示是Byte(位元組)/s(秒),1Byte/s=8bps,2M頻寬的理論速率是256KB/s(2/8*1024),100M頻寬的理論速率是12.5MB/s(100/8)。

 

初步思考

結合上一節的概念介紹,從伺服器角度出發,對入網和出網頻寬進行說明,以下內容來自阿里雲官網介紹:

 

 

 下圖為資料流的方向說明

 

 

 

在我們案例中其實就是上圖中的紅框圈起來部分佔滿了,多餘的位元流只能等著,和堵車一樣,路就那麼寬,超負荷了以後就只能堵車。

 

到這兒,基本問題才算是搞清楚了,「出口頻寬被佔滿,導致使用者請求長時間拿不到響應,其實系統一點也不忙,只是回來的路上堵車了。」

 

找出真凶

從存取量上來看,系統非常平穩,沒有所謂的突發流量,再者說一個toB性質的網站,那點突發流量也是蚍蜉撼樹。

 

從業務屬性來看,就是普通的crud業務,對頻寬的需求不大,不像抖音、快手這類頻寬消耗大戶,那究竟是什麼佔滿了頻寬呢,要是有紀錄檔就好了,還真有。這裡順便普及一個知識點,網站一般都會記錄access log(存取紀錄檔),access log中會記錄響應體的大小,所以我們只要抓取事發時間前後的access log分析是否有超大響應體的情況,果不其然在14:02分發現有兩個附件下載的超大響應(響應體大小463兆),如下圖所示(點選可放大):

 

 

 可謂是防不勝防,最不起眼的附件功能,竟然成了「殺手鐗」。

 

解決問題

正式解決之前先說一個生活中的案例,相信大家都不陌生。

 

筆者剛工作那陣和幾個朋友一起合租,上網無疑成了大家下班之後消遣的主要方式,有聽歌的、刷劇的、看八卦的、打網遊的,無數次的聽到打網遊的小哥從房間衝出來大喊:「誰在幹啥呢,我這卡的都不動了」,我心想「大家各玩各的,憑什麼你打遊戲就不讓別人刷劇、聽歌了」,想歸想,架不住天天在屋裡嚎啊,最後大家想出來一個辦法,給每個人限速,比如上下行都不超過500KB/s,從那以後屋裡確實安靜了一些。

 拍攝於西安老城根某一藝術品展覽

 

回到我們的案例,可以借鑑限速\限流的思想來保護機房出口頻寬不被下載請求給佔滿,大概有兩種方式,下面分別介紹。

 

方案1:nginx層-limit_rate指令

http://nginx.org/en/docs/http/ngx_http_core_module.html#limit_rate

Syntax:  limit_rate rate;
Default:  
limit_rate 0;
Context:  http, , , serverlocationif in location

  

Limits the rate of response transmission to a client. The is specified in bytes per second. The zero value disables rate limiting. The limit is set per a request, and so if a client simultaneously opens two connections, the overall rate will be twice as much as the specified limit. rate

 

比如下面這個就是限制每個請求的下載速度不超過512KB/s

server {
    location /down {
        limit_rate 512k;
    }
}

使用起來還是很方便的,考慮到並不是所有客戶環境都有nginx,所以這個方案最終沒有采用,而是選擇在應用層自己處理,接下來介紹。  

 

方案2:應用層通過Guava-RateLimiter

第一次接觸RateLimiter是幾年前,也是為了使用其限流功能,比如限制某介面的請求量不能超過100QPS,在處理當下這個出口頻寬佔滿的案例時我著實沒有想起它,究其原因我認為有兩點:

  1. 沒有理解透徹其背後原理;

  2. 看待問題沒有遵循第一性原理,拿到問題時大多時候都是去百度找相同問題的解決方案,這樣一來答案一定是侷限的,無法創新。

 

雖然這一次藉助RateLimiter來解決問題也是網路上的提示,但是帶給我的收穫不僅僅是解決當下問題這麼簡單,還有深層次的思考方式上的變革,我覺得這是非常重要的。

 

下面是一段再熟悉不過的檔案下載的程式碼片段

String disposition = "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8");
res.setHeader("Content-disposition", disposition);

try(InputStream is = new FileInputStream("file")){
    byte buf[] = new byte[4*1024];
    int len = -1;
    while((len = is.read(buf)) != -1){
          res.getOutputStream().write(buf,0,len);
    }
    
    res.getOutputStream().flush();
 }

源源不斷的從檔案輸入流中讀取內容,然後寫到網路輸出流,不加任何限制的情況下導致機房出口頻寬被佔滿,改進方式就是引入RateLimiter,往網路輸出流中寫之前呼叫RateLimiter獲取令牌,如果超過了限定的速度就會阻塞。

//定義RateLimter,每秒發出1兆令牌
static RateLimiter rateLimiter = RateLimiter.create(1*1024*1024);

String disposition = "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8");
res.setHeader("Content-disposition", disposition);

try(InputStream is = new FileInputStream("file")){
    byte buf[] = new byte[4*1024];
    int len = -1;
    while((len = is.read(buf)) != -1){
          //輸出之前前往RateLimiter獲取令牌,保證不要超過1兆每秒
          rateLimiter.acquire(len);
          res.getOutputStream().write(buf,0,len);
    }
    
    res.getOutputStream().flush();
 }

限制效果如下,下載速度基本穩定在1兆左右:

 

 最終採用方案2解決問題,在我們的場景中,其更通用、靈活。

 

總結

通過一個出口頻寬被佔滿的案例介紹了兩種解決方案,也囉嗦了一點自己思考問題的方式,希望能給大家帶來些許收穫,最後帶來一個彩蛋,如果幾年前我就認真看了RateLimiter的Api檔案,處理當下問題時也能節約不少網路上找答案的時間,而且收穫的是一手資料。

 

RateLimiter類註釋裡的一段,提到了網路限速的場景,一起來看下

 

As another example, imagine that we produce a stream of data, and we want to cap it at 5kb per second. This could be accomplished by requiring a permit per byte, and specifying a rate of 5000 permits per second:
 final RateLimiter rateLimiter = RateLimiter.create(5000.0); // rate = 5000 permits per second 
  void submitPacket(byte[] packet) {    
     rateLimiter.acquire(packet.length);    
     networkService.send(packet);  
}