開發與維運

Nginx 限流模塊

【轉載請註明出處】:https://developer.aliyun.com/article/758507

生活中的 “限流”?

限流並非新鮮事,在生活中亦無處不在,下面例舉一二:

  • 博物館:限制每天參觀總人數以保護文物
  • 高鐵安檢:有若干安檢口,旅客依次排隊,工作人員根據安檢快慢決定是否放人進去。遇到節假日,可以增加安檢口來提高處理能力(橫向拓展),同時增加排隊等待區長度(緩存待處理任務)。
  • 辦理銀行業務:所有人先領號,各窗口叫號處理。每個窗口處理速度根據客戶具體業務而定,所有人排隊等待叫號即可。若快下班時,告知客戶明日再來(拒絕流量)。
  • 水壩洩洪:水壩可以通過閘門控制洩洪速度(控制處理速度)。

在開發高併發系統時有三把利器用來保護系統:緩存、降級和限流

  • 緩存:緩存的目的是提升系統訪問速度和增大系統處理容量
  • 降級:降級是當服務器壓力劇增的情況下,根據當前業務情況及流量對一些服務和頁面有策略的降級,以此釋放服務器資源以保證核心任務的正常運行
  • 限流:限流的目的是通過對併發訪問/請求進行限速,或者對一個時間窗口內的請求進行限速來保護系統,一旦達到限制速率則可以拒絕服務、排隊或等待、降級等處理

兩大限流算法

常用的限流算法有令牌桶和和漏桶,而Google開源項目Guava中的RateLimiter使用的就是令牌桶控制算法。

漏桶算法

把請求比作是水,水來了都先放進桶裡,並以限定的速度出水,當水來得過猛而出水不夠快時就會導致水直接溢出,即拒絕服務。
image.png
漏斗有一個進水口 和 一個出水口,出水口以一定速率出水,並且有一個最大出水速率:

在漏斗中沒有水的時候

  • 如果進水速率小於等於最大出水速率,那麼,出水速率等於進水速率,此時,不會積水
  • 如果進水速率大於最大出水速率,那麼,漏斗以最大速率出水,此時,多餘的水會積在漏斗中

在漏斗中有水的時候

  • 出水口以最大速率出水
  • 如果漏斗未滿,且有進水的話,那麼這些水會積在漏斗中
  • 如果漏斗已滿,且有進水的話,那麼這些水會溢出到漏斗之外
令牌桶算法

對於很多應用場景來說,除了要求能夠限制數據的平均傳輸速率外,還要求允許某種程度的突發傳輸。這時候漏桶算法可能就不合適了,令牌桶算法更為適合。
image.png

令牌桶算法的原理是系統以恆定的速率產生令牌,然後把令牌放到令牌桶中,令牌桶有一個容量,當令牌桶滿了的時候,再向其中放令牌,那麼多餘的令牌會被丟棄;當想要處理一個請求的時候,需要從令牌桶中取出一個令牌,如果此時令牌桶中沒有令牌,那麼則拒絕該請求。

令牌桶算法VS漏桶算法

漏桶
漏桶的出水速度是恆定的,那麼意味著如果瞬時大流量的話,將有大部分請求被丟棄掉(也就是所謂的溢出)。

令牌桶
生成令牌的速度是恆定的,而請求去拿令牌是沒有速度限制的。這意味,面對瞬時大流量,該算法可以在短時間內請求拿到大量令牌,而且拿令牌的過程並不是消耗很大的事情。

Nginx限流

Nginx官方版本限制IP的連接和併發分別有兩個模塊:

  • limit_req_zone 用來限制單位時間內的請求數,即速率限制,採用的漏桶算法 "leaky bucket"。
  • limit_req_conn 用來限制同一時間連接數,即併發限制。

ngx_http_limit_req_module 模塊

Nginx按請求速率限速模塊使用的是漏桶算法,即能夠強行保證請求的實時處理速度不會超過設置的閾值。
指令

Syntax: limit_req zone=name [burst=number] [nodelay | delay=number];

Default: —
Context: http, server, location

正常流量

limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

  • key :定義限流對象,binary_remote_addr 是一種key,表示基於 remote_addr(客戶端IP) 來做限流,binary_ 的目的是壓縮內存佔用量。
  • zone:定義共享內存區來存儲訪問信息, one:10m 表示一個大小為10M,名字為one的內存區域。1M能存儲16000 IP地址的訪問信息,10M可以存儲16W IP地址訪問信息。
  • rate 用於設置最大訪問速率,rate=10r/s 表示每秒最多處理10個請求。Nginx 實際上以毫秒為粒度來跟蹤請求信息,因此 10r/s 實際上是限制:每100毫秒處理一個請求。這意味著,自上一個請求處理完後,若後續100毫秒內又有請求到達,將拒絕處理該請求。如果限制的頻率低於1r/s,則可以使用r/m,如30r/m。
突發流量

limit_req zone=one burst=5 nodelay;

  • zone=one 設置使用哪個配置區域來做限制,與上面limit_req_zone 裡的name對應。
  • burst=5,burst爆發的意思,這個配置的意思是設置一個大小為5的緩衝區當有大量請求(爆發)過來時,超過了訪問頻次限制的請求可以先放到這個緩衝區內。
  • nodelay,如果設置,超過訪問頻次而且緩衝區也滿了的時候就會直接返回503,如果沒有設置,則所有請求會等待排隊。
日誌級別

為服務器由於超過頻次或延遲處理而拒絕處理請求的情況設置所需的日誌記錄級別。延遲的日誌記錄級別比拒絕的日誌記錄級別低1級;例如,“limit_req_log_level notice” 的延遲日誌記錄級別是info。

Syntax: limit_req_log_level info | notice | warn | error;

Default: limit_req_log_level error;
Context: http, server, location

拒絕響應狀態碼

Syntax: limit_req_status code;

Default: limit_req_status 503;
Context: http, server, location

案例
http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; 
    server {
        location /search/ {
            limit_req zone=one; 
        }
}       

限流速度為每秒10次請求,如果有10次請求同時到達一個空閒的nginx,他們都能得到執行嗎?
image.png

漏桶漏出請求是勻速的。10r/s是怎樣勻速的呢?每100ms漏出一個請求。在這樣的配置下,桶是空的,所有不能實時漏出的請求,都會被拒絕掉。所以如果10次請求同時到達,那麼只有一個請求能夠得到執行,其它的,都會被拒絕。
這不太友好,大部分業務場景下我們希望這10個請求都能得到執行,添加突發流量處理機制。

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; 
    server {
        location /search/ {
            limit_req zone=one burst=12; 
        }
}       

burst=12 漏桶的大小設置為12。
image.png

邏輯上叫漏桶,實現起來是FIFO隊列,把得不到執行的請求暫時緩存起來。這樣漏出的速度仍然是100ms一個請求,但就併發而言,暫時得不到執行的請求,可以先緩存起來。只有當隊列滿了的時候,才會拒絕接受新請求。這樣漏桶在限流的同時,也起到了削峰填谷的作用。

在這樣的配置下,如果有10次請求同時到達,它們會依次執行,每100ms執行1個。雖然得到執行了,但因為排隊執行,延遲大大增加,在很多場景下仍然是不能接受的。繼續修改配置,解決Delay太久導致延遲增加的問題。

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; 
    server {
        location /search/ {
            limit_req zone=one burst=12 nodelay;
        }
}       

nodelay 把開始執行請求的時間提前,以前是delay到從桶裡漏出來才執行,現在不delay了,只要入桶就開始執行。
image.png

要麼立刻執行,要麼被拒絕,請求不會因為限流而增加延遲了。因為請求從桶裡漏出來還是勻速的(100ms釋放1個),桶的空間又是固定的,最終平均下來,還是每秒執行了10次請求,限流的目的還是達到了。
但是請注意,雖然設置burst和nodelay能夠降低突發請求的處理時間,但是長期來看並不會提高吞吐量的上限,長期吞吐量的上限是由rate決定的,因為nodelay只能保證burst的請求被立即處理,但Nginx會限制隊列元素釋放的速度,就像是限制了令牌桶中令牌產生的速度。

但這樣也有缺點,限流是限了,但是限得不那麼勻速。以上面的配置舉例,如果有12個請求同時到達,那麼這12個請求都能夠立刻執行,然後後面的請求只能勻速進桶,100ms執行1個。如果有一段時間沒有請求,桶空了,那麼又可能出現併發的12個請求一起執行。
大部分情況下,這種限流不勻速,不算是大問題。不過nginx也提供了一個參數控制併發執行也就是nodelay的請求的數量。

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; 
    server {
        location /search/ {
            limit_req zone=one burst=12 delay=4;
        }
}       

delay=4 從桶內第5個請求開始delay
image.png

這樣通過控制delay參數的值,可以調整允許併發執行的請求的數量,使得請求變的均勻起來,在有些耗資源的服務上控制這個數量,還是有必要的。

ngx_http_limit_conn_module 模塊

這個模塊用來限制單個IP的請求數。並非所有的連接都被計數。只有在服務器處理了請求並且已經讀取了整個請求頭時,連接才被計數。

Syntax: limit_conn zone number;

Default: —
Context: http, server, location

如:

limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;

server {
    ...
    limit_conn perip 10;
    limit_conn perserver 100;
}
  • limit_conn perip 10 作用的key 是 $binary_remote_addr,表示限制單個IP同時最多能持有10個連接。
  • limit_conn perserver 100 作用的key是 $server_name,表示虛擬主機(server) 同時能處理併發連接的總數。

需要注意的是:只有當 request header 被後端server處理後,這個連接才進行計數。

日誌級別

Syntax: limit_conn_log_level info | notice | warn | error;

Default: limit_conn_log_level error;
Context: http, server, location

拒絕響應狀態碼

Syntax: limit_conn_status code;

Default: limit_conn_status 503;
Context: http, server, location

設置白名單

限流主要針對外部訪問,內網訪問相對安全,可以不做限流,通過設置白名單即可。利用 Nginx ngx_http_geo_module 和 ngx_http_map_module 兩個工具模塊即可搞定。

    geo $limit {
        default 1;
        10.0.0.0/8 0;
        192.168.0.0/24 0;
        172.20.0.35 0;
    }
    map $limit $limit_key {
        0 "";
        1 $binary_remote_addr;
    }
    limit_req_zone $limit_key zone=myRateLimit:10m rate=10r/s;

geo 對於白名單(子網或IP都可以) 將返回0,其他IP將返回1。

map 將 $limit 轉換為 $limit_key,如果是 $limit 是0(白名單),則返回空字符串;如果是1,則返回客戶端實際IP。

limit_req_zone 限流的key不再使用 $binary_remote_addr,而是 $limit_key 來動態獲取值。如果是白名單,limit_req_zone 的限流key則為空字符串,將不會限流;若不是白名單,將會對客戶端真實IP進行限流。

限制數據傳輸速度

除限流外,ngx_http_core_module 還提供了限制數據傳輸速度的能力(即常說的下載速度)。

例如:

location /flv/ {
        flv;
        limit_rate_after 20m;
        limit_rate       100k;
    }

這個限制是針對每個請求的,表示客戶端下載前20M時不限速,後續限制100kb/s。

限制特定UA

可以限制特定UA(比如爬蟲)的訪問

limit_req_zone  $anti_spider  zone=one:10m   rate=10r/s;
limit_req zone=one burst=100 nodelay;
if ($http_user_agent ~* (YisouSpider|Scrapy)) {
    set $anti_spider $http_user_agent;
}

【轉載請註明出處】:https://developer.aliyun.com/article/758507

Leave a Reply

Your email address will not be published. Required fields are marked *