限流算法和nginx請求限流


一、限流算法

常見的限流算法有計數器(固定窗口)、滑動窗口、漏桶、令牌桶

1、計數器(固定窗口)

最簡單的限流算法,計數器限制每一分鍾或者每一秒鍾內請求不能超過一定的次數,在下一秒鍾計數器清零重新計算

計數器限流存在一個缺陷,比如限制每分鍾訪問不能超過100次,客戶端在第一分鍾的59秒請求100次,在第二分鍾的第1秒又請求了100次,那么在這2秒內后端會受到200次請求的壓力,形成了流量突刺

2、滑動窗口

滑動窗口其實是細分后的計數器,它將每個時間窗口又細分成若干個時間片段,每過一個時間片段,整個時間窗口就會往右移動一格

比如限制每分鍾訪問不能超過100次,如圖每分鍾被分成了4個時間片段,每個時間片段15秒,假設客戶端在第一分鍾的50秒請求了100次,時間到了第二分鍾的10秒,時間窗口向右滑動一格,這時這個時間窗口其實已經打滿了100次,客戶端將被拒絕訪問

時間窗口划分的越細,滑動窗口的滾動就越平滑,限流的效果就會越精確

3、漏桶

漏桶算法類似一個限制出水速度的水桶,通過一個固定大小FIFO隊列+定時取隊列元素的方式實現,請求進入隊列后會被勻速的取出處理(桶底部開口勻速出水),當隊列被占滿后后來的請求會直接拒絕(水倒的太快從桶中溢出來)

漏桶桶的優點是可以削峰填谷,不論請求多大多快,都只會勻速發給后端,不會出現突刺現象,保證下游服務正常運行

缺點就是在桶隊列中的請求會排隊,響應時間拉長

4、令牌桶

令牌桶算法是以一個恆定的速度往桶里放置令牌(如果桶里的令牌滿了就廢棄),每進來一個請求去桶里找令牌,有的話就拿走令牌繼續處理,沒有就拒絕請求

令牌桶的優點是可以應對突發流量,當桶里有令牌時請求可以快速的響應,也不會產生漏桶隊列中的等待時間

缺點就是相對漏桶一定程度上減小了對下游服務的保護

 

二、nginx請求限流(ngx_http_limit_req_module)

對於nginx接入層限流可以使用nginx自帶的兩個模塊:連接數限流模塊ngx_http_limit_conn_module和漏桶算法實現的請求限流模塊ngx_http_limit_req_module,還可以使用OpenResty提供的Lua限流模塊lua-resty-limit-traffic進行更復雜的限流場景

本文只介紹請求限流模塊ngx_http_limit_req_module,主要的指令是limit_req_zone和limit_req

1、指令介紹

(1)limit_req_zone

設置共享內存區域的大小和請求的速率

語法:limit_req_zone key zone=name:size rate=rate;
位置:http
版本:1.7.6之前key只可包含一個變量
示例:limit_req_zone $binary_remote_addr zone=test123:10m rate=10r/s;

key

定義要限流的對象,通常是nginx內置變量,多個key可以用逗號分隔,示例中$binary_remote_addr是限制每個ip的請求速率

一般有$binary_remote_addr(客戶的ip)、$server_name(服務器名稱)、$uri(不帶參數的請求地址)、$request_uri(帶參數的請求地址),更多變量可以在nginx包的"\src\http\ngx_http_variables.c"文件中查看,或者查看本文的最后

zone

定義存放限流信息的共享內存區域,記錄每類客戶端的訪問頻率,在worker進程間共享,size表示區域大小

比如使用$binary_remote_addr的情況,“binary_”表示內存占用量經過縮減,IPv4固定占用4字節、IPv6固定占用16字節,在32位系統,每一個IP在32位系統將占用64字節、在64位系統將占用128字節來保存狀態,1m空間在32位系統能保存1w6多個IP的狀態,在64位系統能保存8k多個IP的狀態

當內存空間耗盡時nginx使用lru算法淘汰最長時間未使用的key,如果釋放的空間仍不足以容納新記錄,nginx將直接限制請求返回狀態碼,所以需要提前預估key的數量分配合理的內存空間,避免指定的內存空間被耗盡

rate

設置最大請求速率,在示例中速率不能超過每秒10個請求,nginx以毫秒粒度跟蹤請求,因此實際上是限制每100ms1個請求

如果希望限制每分鍾可以指定“r/m”

limit_req_zone指令只是定義了共享區域和速率的參數,實際並沒有限制請求,需要在server或者location中設置limit_req來搭配使用

(2)limit_req

設置所屬共享區域名稱和請求最大突發大小,並在指令出現的上下文中啟用速率限制

語法:limit_req zone=name [burst=number] [nodelay | delay=number];
位置:http, server, location
版本:1.15.7后可以使用delay參數
示例:limit_req zone=test123 burst=5;

zone:和需要對應的limit_req_zone內存區域名稱一致

burst:可選參數,設置允許突發請求的數量

nodelay:無延遲排隊

delay:分段限速

burst、nodelay、delay參數不同的組合可以產生4種限流效果,在下一節限流效果演示中會逐一說明

指令可以疊加使用,示例中配置了單個ip地址的處理速度,同時限制了整個服務的處理速度

limit_req_zone $binary_remote_addr zone=perip:10m rate=1r/s;
limit_req_zone $server_name zone=perserver:10m rate=10r/s;

server {
    ...
    limit_req zone=perip burst=5 nodelay;
    limit_req zone=perserver burst=10;
}

將基本速率限制與其他nginx功能結合使用,可以實現更細微的流量限制,比如搭配geo和map指令可以實現對來自不在“白名單”上的任何人的請求施加速率限制:

geo $limit {
    default 1;
    10.0.0.0/8 0;
    192.168.0.0/24 0;
}
 
map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}
 
limit_req_zone $limit_key zone=req_zone:10m rate=5r/s;
 
server {
    location / {
        limit_req zone=req_zone burst=10 nodelay;
 
        # ...
    }
}
(3)limit_req_log_level

設置速率超出而拒絕請求或延遲請求處理的日志記錄級別

語法:limit_req_log_level info | notice | warn | error;
默認:error
位置:http, server, location
版本:該指令出現在版本0.8.18以后

延遲請求比拒絕請求第一個等級,比如配置的是error,拒絕請求日志記錄為error,延遲請求日志記錄為warn

(4)limit_req_status

設置響應被拒絕請求的狀態碼

語法:limit_req_status code;
默認:503
位置:http, server, location
版本:該指令出現在1.3.15版以后
(5)limit_req_dry_run

啟用空運行模式,開啟后請求速率不受限制,但在共享內存區域中請求的數量將照常計算

語法:limit_req_dry_run on | off;
默認:off	
位置:http, server, location
版本:該指令出現在1.17.1版以后

2、限流效果演示

(1)無burst的情況

 沒有配置burst桶容量,桶容量為0,按照固定速率處理請求,如果請求被限流,直接返回503

limit_req_zone $server_name zone=test123:10m rate=50r/s;
limit_req zone=test123;
jmeter線程數1,次數20

可以看到請求每隔20ms成功一次

(2)burst的情況

配置了burst桶容量,沒有配置nodelay就是延遲模式,來不及處理的請求會進入桶中,桶內的請求會以固定速率被處理,如果桶滿了,新進入的請求被限流

limit_req_zone $server_name zone=test123:10m rate=2r/s;
limit_req zone=test123 burst=3;
jmeter線程數6,一起請求2次,2批間隔300ms

速率為500ms成功一次,設置了burst桶容量為3,相當於一個長度3的緩沖隊列

我們的預期是當同時有6個請求到達時,nginx將第1個請求立即處理,並將其余3個請求放入桶隊列,然后它每500毫秒處理一個排隊的請求,在請求使排隊請求的數量超過3時返回503到客戶端

第一次6個請求進入后,請求1-1第一個被執行,請求1-6、1-3、1-4幸運的進入桶隊列中等待勻速執行,看到這4個請求間隔500ms,請求1-5、1-2因為來不及處理且桶滿了被限流

第二次6個請求在最后一個請求1-4執行完后間隔300ms進入,這時距離下一次還不到500ms,所以新的請求得先進入桶隊列中等待,看到請求1-3、1-5、1-6幸運的進入桶隊列中,請求1-4、1-2被限流

如果2批請求間隔600ms呢?

那第二批請求將會成功4個,和第一批的情況一樣

(3)burst+nodelay的情況

配置了burst桶容量,同時配置了nodelay就是非延遲模式,桶隊列是一個有狀態的插槽隊列,當請求“過早”到達時,只要桶隊列中有可用的插槽,nginx就會立即處理請求,並將該插槽標記為“已占用”,當某一次限流間隔過后沒有請求時,該插槽就會被標記為“可用”

這種邏輯和令牌桶非常像,只要桶的插槽沒有被占用完,突發的請求就能迅速被處理,不用像延遲模式一樣需要進入隊列排隊等待,在流量洪峰過去后插槽可以慢慢被恢復,類似令牌慢慢被填充滿桶

limit_req_zone $server_name zone=test123:10m rate=2r/s;
limit_req zone=test123 burst=3 nodelay;
jmeter線程數6,一起請求2次,2批間隔600ms

速率為500ms成功一次,設置了burst桶容量為3,相當於有3個插槽可用

我們的預期是當同時有6個請求到達時,nginx將第1個請求立即處理,同時也立即處理之后3個請求,同時將桶中的3個插槽標記占用,將其他2個請求限流,在第二批請求間隔600ms到達后,有一個請求被處理

第一次6個請求進入后,請求1-6第一個被執行,請求1-4、1-1、1-5幸運的使用了桶隊列中的插槽被執行,看到這4個請求沒有等待時間都是立即執行,請求1-3、1-2因為來不及處理且桶插槽用完被限流

第二次6個請求在第一批請求執行完后間隔600ms進入,這時距離下一次間隔超過了500ms,一個插槽被重置,請求1-5進入后幸運的使用了這個插槽被執行,其他5個請求因為來不及處理且桶插槽用完被限流

再看一下復雜一點的情況:

limit_req_zone $server_name zone=test123:10m rate=2r/s;
limit_req zone=test123 burst=3 nodelay;
jmeter線程數6,一起請求7次,分別間隔1000ms、300ms、300ms、300ms、1500ms、2000ms

一樣的參數,線程請求7次,每批分別間隔1000ms、300ms、300ms、300ms、1500ms、2000ms

第一批序號1-6同第一個例子

第二批序號7-12,因為這次間隔是1000ms,所以有2個插槽被重置,成功了2個請求

第三批序號12-18,因為間隔300ms太短,沒有到500ms間隔,請求全部限流

第四批序號19-24,序號1的第一個請求是51.815執行的,每隔500ms恢復一個插槽的話,第3次恢復是在53.315,剛好53.447序號19的請求1-1拿到了這個插槽,之后其他請求被限流

第五批序號25-30,間隔300ms,因為上一批53.447剛用掉插槽,下一個插槽恢復是在53.815(51.815+4*500ms),這一批是53.754沒有到時間,所以請求全部被限流

第六批序號31-36,間隔了1500ms,到55.261已經恢復了3個插槽了(53.815,54.315,54.815),所以成功了3個請求

第七批序號37-42,間隔了2000ms,到57.307恢復了4個插槽(55.315,55.815,56.315,56.815),所以成功了4個請求

(4)burst+delay的情況

配置了burst桶容量和delay參數后,就是部分延遲模式,比如burst=12,delay=8,則桶的前8位是插槽隊列,后4位是緩沖隊列

假設有這樣的配置:

limit_req_zone $server_namezone=test123:10m rate=5r/s;
limit_req zone=test123 burst=12 delay=8;

該配置最多允許12個突發請求,其中前8個突發請求將被立即處理,后4個請求被強制以5 r / s的勻速執行,在緩沖隊列空出之前多於12個的請求被限流

使用此配置后,以8 r / s連續發出請求流的客戶端將表現為圖中的情況

通過測試可以發現nginx會先等待緩沖隊列清空后再恢復插槽隊列

limit_req_zone $server_name zone=test123:10m rate=2r/s;
limit_req zone=test123 burst=6 delay=4;
jmeter線程數10,一起請求2次,2批間隔300ms

速率為500ms成功一次,設置了burst桶容量為6,delay為4,相當於有4個插槽可用,附帶一個長度為2的緩沖隊列

我們的預期是第一批10個請求進來立即執行1+4個請求,有2個請求進入隊列緩慢執行,執行完后間隔300ms,第二批10個請求進來,有2個請求進入空出的緩沖隊列,其他8個請求限流(因為300ms,沒有到500ms的間隔,插槽沒有來得及恢復)

第一批10個請求進入后,前5個請求立即被執行(4個使用了插槽),請求1-10、1-1進入緩沖隊列勻速執行,可以看到請求間隔了500ms,其他3個請求既沒有使用插槽,也沒有進緩沖隊列,被限流

第二批10個請求間隔300ms進入,這時雖然緩沖隊列是空的,但是插槽來不及恢復,所以只有2個緩沖隊列的位置可用,所以看到請求1-6、1-8進入了隊列勻速執行

如果把兩批請求的間隔延長一些呢?

limit_req_zone $server_name zone=test123:10m rate=2r/s;
limit_req zone=test123 burst=6 delay=4;
jmeter線程數10,一起請求2次,2批間隔1600ms

間隔改為1600ms,足夠3個插槽恢復了

可以看到表現和預期的一樣,和間隔300ms的圖唯一區別,序號11-13的3個請求拿到了恢復的3個插槽,立刻被執行了

3、附nginx內置變量

$args                      請求中的參數;
$binary_remote_addr        遠程地址的二進制表示
$body_bytes_sent           已發送的消息體字節數
$content_length            HTTP請求信息里的"Content-Length"
$content_type              請求信息里的"Content-Type"
$document_root             針對當前請求的根路徑設置值
$document_uri              與$uri相同
$host                      請求信息中的"Host",如果請求中沒有Host行,則等於設置的服務器名;    
$http_cookie               cookie 信息 
$http_referer              來源地址
$http_user_agent           客戶端代理信息
$http_via                  最后一個訪問服務器的Ip地址
$http_x_forwarded_for      相當於網絡訪問路徑。    
$limit_rate                對連接速率的限制          
$remote_addr               客戶端地址
$remote_port               客戶端端口號
$remote_user               客戶端用戶名,認證用
$request                   用戶請求信息
$request_body              用戶請求主體
$request_body_file         發往后端的本地文件名稱      
$request_filename          當前請求的文件路徑名
$request_method            請求的方法,比如"GET"、"POST"等
$request_uri               請求的URI,帶參數   
$server_addr               服務器地址,如果沒有用listen指明服務器地址,使用這個變量將發起一次系統調用以取得地址(造成資源浪費)
$server_name               請求到達的服務器名
$server_port               請求到達的服務器端口號
$server_protocol           請求的協議版本,"HTTP/1.0"或"HTTP/1.1"
$uri                       請求的URI,可能和最初的值有不同,比如經過重定向之類的


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM