一、限流算法
常見的限流算法有計數器(固定窗口)、滑動窗口、漏桶、令牌桶
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,可能和最初的值有不同,比如經過重定向之類的