接入層通常指請求流量的入口,該層的主要目的有:負載均衡、非法請求過濾、請求聚合、緩存、降級、限流、a/b測試、服務品質監控等等,可以參考筆者寫的《使用nginx+lua(openresty)開發高性能web應用》。
對于nginx接入層限流可以使用nginx自帶了兩個子產品:連接配接數限流子產品ngx_http_limit_conn_module和漏桶算法實作的請求限流子產品ngx_http_limit_req_module。還可以使用openresty提供的lua限流子產品lua-resty-limit-traffic進行更複雜的限流場景。
limit_conn用來對某個key對應的總的網絡連接配接數進行限流,可以按照如ip、域名次元進行限流。limit_req用來對某個key對應的請求的平均速率進行限流,并有兩種用法:平滑模式(delay)和允許突發模式(nodelay)。
limit_conn是對某個key對應的總的網絡連接配接數進行限流。可以按照ip來限制ip次元的總連接配接數,或者按照服務域名來限制某個域名的總連接配接數。但是記住不是每一個請求連接配接都會被計數器統計,隻有那些被nginx處理的且已經讀取了整個請求頭的請求連接配接才會被計數器統計。
<b>配置示例:</b>
================================
limit_conn:要配置存放key和計數器的共享記憶體區域和指定key的最大連接配接數;此處指定的最大連接配接數是1,表示nginx最多同時并發處理1個連接配接;
limit_conn_zone:用來配置限流key、及存放key對應資訊的共享記憶體區域大小;此處的key是“$binary_remote_addr”其表示ip位址,也可以使用如$server_name作為key來限制域名級别的最大連接配接數;
limit_conn_status:配置被限流後傳回的狀态碼,預設傳回503;
limit_conn_log_level:配置記錄被限流後的日志級别,預設error級别。
<b>limit_conn的主要執行過程如下所示:</b>
1、請求進入後首先判斷目前limit_conn_zone中相應key的連接配接數是否超出了配置的最大連接配接數;
2.1、如果超過了配置的最大大小,則被限流,傳回limit_conn_status定義的錯誤狀态碼;
2.2、否則相應key的連接配接數加1,并注冊請求處理完成的回調函數;
3、進行請求處理;
4、在結束請求階段會調用注冊的回調函數對相應key的連接配接數減1。
limt_conn可以限流某個key的總并發/請求數,key可以根據需要變化。
<b>按照ip限制并發連接配接數配置示例:</b>
首先定義ip次元的限流區域:
接着在要限流的location中添加限流邏輯:
即允許每個ip最大并發連接配接數為2。
使用ab測試工具進行測試,并發數為5個,總的請求數為5個:
将得到如下access.log輸出:
此處我們把access log格式設定為log_format main '[$time_local] [$msec] $status';分别是“日期 日期秒/毫秒值 響應狀态碼”。
如果被限流了,則在error.log中會看到類似如下的内容:
<b></b>
按照域名限制并發連接配接數配置示例:
首先定義域名次元的限流區域:
即允許每個域名最大并發請求連接配接數為2;這樣配置可以實作伺服器最大連接配接數限制。
limit_req是令牌桶算法實作,用于對指定key對應的請求進行限流,比如按照ip次元限制請求速率。
配置示例:
limit_req:配置限流區域、桶容量(突發容量,預設0)、是否延遲模式(預設延遲);
limit_req_zone:配置限流key、及存放key對應資訊的共享記憶體區域大小、固定請求速率;此處指定的key是“$binary_remote_addr”表示ip位址;固定請求速率使用rate參數配置,支援10r/s和60r/m,即每秒10個請求和每分鐘60個請求,不過最終都會轉換為每秒的固定請求速率(10r/s為每100毫秒處理一個請求;60r/m,即每1000毫秒處理一個請求)。
limit_req的主要執行過程如下所示:
1、請求進入後首先判斷最後一次請求時間相對于目前時間(第一次是0)是否需要限流,如果需要限流則執行步驟2,否則執行步驟3;
2.1、如果沒有配置桶容量(burst),則桶容量為0;按照固定速率處理請求;如果請求被限流,則直接傳回相應的錯誤碼(預設503);
2.2、如果配置了桶容量(burst>0)且延遲模式(沒有配置nodelay);如果桶滿了,則新進入的請求被限流;如果沒有滿則請求會以固定平均速率被處理(按照固定速率并根據需要延遲處理請求,延遲使用休眠實作);
2.3、如果配置了桶容量(burst>0)且非延遲模式(配置了nodelay);不會按照固定速率處理請求,而是允許突發處理請求;如果桶滿了,則請求被限流,直接傳回相應的錯誤碼;
3、如果沒有被限流,則正常處理請求;
4、nginx會在相應時機進行選擇一些(3個節點)限流key進行過期處理,進行記憶體回收。
<b>首先定義ip次元的限流區域:</b>
限制為每秒500個請求,固定平均速率為2毫秒一個請求。
即桶容量為0(burst預設為0),且延遲模式。
使用ab測試工具進行測試,并發數為2個,總的請求數為10個:
雖然每秒允許500個請求,但是因為桶容量為0,是以流入的請求要麼被處理要麼被限流,無法延遲處理;另外平均速率在2毫秒左右,比如1465381556.410和1465381556.411被處理了;有朋友會說這固定平均速率不是1毫秒嘛,其實這是因為實作算法沒那麼精準造成的。
如果被限流在error.log中會看到如下内容:
如果被延遲了在error.log(日志級别要info級别)中會看到如下内容:
為了友善測試設定速率為每秒2個請求,即固定平均速率是500毫秒一個請求。
固定平均速率為500毫秒一個請求,通容量為3,如果桶滿了新的請求被限流,否則可以進入桶中排隊并等待(實作延遲模式)。
為了看出限流效果我們寫了一個req.sh腳本:
首先進行6個并發請求6次url,然後休眠300毫秒,然後再進行6個并發請求6次url;中間休眠目的是為了能跨越2秒看到效果,如果看不到如下的效果可以調節休眠時間。

桶容量為3,即桶中在時間視窗内最多流入3個請求,且按照2r/s的固定速率處理請求(即每隔500毫秒處理一個請求);桶計算時間視窗(1.5秒)=速率(2r/s)/桶容量(3),也就是說在這個時間視窗内桶最多暫存3個請求。是以我們要以目前時間往前推1.5秒和1秒來計算時間視窗内的總請求數;另外因為預設是延遲模式,是以時間窗内的請求要被暫存到桶中,并以固定平均速率處理請求:
第一輪:有4個請求處理成功了,按照漏桶桶容量應該最多3個才對;這是因為計算算法的問題,第一次計算因沒有參考值,是以第一次計算後,後續的計算才能有參考值,是以第一次成功可以忽略;這個問題影響很小可以忽略;而且按照固定500毫秒的速率處理請求。
第二輪:因為第一輪請求是突發來的,差不多都在1465433203.959時間點,隻是因為漏桶将速率進行了平滑變成了固定平均速率(每500毫秒一個請求);而第二輪計算時間應基于1465433203.959;而第二輪突發請求差不多都在1465433205.766時間點,是以計算桶容量的時間視窗應基于1465433203.959和1465433205.766來計算,計算結果為1465433205.766這個時間點漏桶為空了,可以流入桶中3個請求,其他請求被拒絕;又因為第一輪最後一次處理時間是1465433205.453,是以第二輪第一個請求被延遲到了1465433205.950。這裡也要注意固定平均速率隻是在配置的速率左右,存在計算精度問題,會有一些偏差。
如果桶容量改為1(burst=1),執行req.sh腳本可以看到如下輸出:
桶容量為1,按照每1000毫秒一個請求的固定平均速率處理請求。
為了友善測試配置為每秒2個請求,固定平均速率是500毫秒一個請求。
<b>接着在要限流的location中添加限流邏輯:</b>
桶容量為3,如果桶滿了直接拒絕新請求,且每秒2最多兩個請求,桶按照固定500毫秒的速率以nodelay模式處理請求。
為了看到限流效果我們寫了一個req.sh腳本:
<b>将得到類似如下access.log輸出:</b>
桶容量為3(,即桶中在時間視窗内最多流入3個請求,且按照2r/s的固定速率處理請求(即每隔500毫秒處理一個請求);桶計算時間視窗(1.5秒)=速率(2r/s)/桶容量(3),也就是說在這個時間視窗内桶最多暫存3個請求。是以我們要以目前時間往前推1.5秒和1秒來計算時間視窗内的總請求數;另外因為配置了nodelay,是非延遲模式,是以允許時間窗内突發請求的;另外從本示例會看出兩個問題:
第一輪和第七輪:有4個請求處理成功了;這是因為計算算法的問題,本示例是如果2秒内沒有請求,然後接着突然來了很多請求,第一次計算的結果将是不正确的;這個問題影響很小可以忽略;
第五輪:1.0秒計算出來是3個請求;此處也是因計算精度的問題,也就是說limit_req實作的算法不是非常精準的,假設此處看成相對于2.75的話,1.0秒内隻有1次請求,是以還是允許1次請求的。
如果限流出錯了,可以配置錯誤頁面:
limit_conn_zone/limit_req_zone定義的記憶體不足,則後續的請求将一直被限流,是以需要根據需求設定好相應的記憶體大小。
此處的限流都是單nginx的,假設我們接入層有多個nginx,此處就存在和應用級限流相同的問題;那如何處理呢?一種解決辦法:建立一個負載均衡層将按照限流key進行一緻性雜湊演算法将請求哈希到接入層nginx上,進而相同key的将打到同一台接入層nginx上;另一種解決方案就是使用nginx+lua(openresty)調用分布式限流邏輯實作。
之前介紹的兩個子產品使用上比較簡單,指定key、指定限流速率等就可以了,如果我們想根據實際情況變化key、變化速率、變化桶大小等這種動态特性,使用标準子產品就很難去實作了,是以我們需要一種可程式設計來解決我們問題;而openresty提供了lua限流子產品lua-resty-limit-traffic,通過它可以按照更複雜的業務邏輯進行動态限流處理了。其提供了limit.conn和limit.req實作,算法與nginx limit_conn和limit_req是一樣的。
此處我們來實作ngx_http_limit_req_module中的【場景2.2測試】,不要忘記下載下傳lua-resty-limit-traffic子產品并添加到openresty的lualib中。
<b>配置用來存放限流用的共享字典:</b>
<b>以下是實作【場景2.2測試】的限流代碼limit_req.lua:</b>
即限流邏輯再nginx access階段被通路,如果不被限流繼續後續流程;如果需要被限流要麼sleep一段時間繼續後續流程,要麼傳回相應的狀态碼拒絕請求。
在分布式限流中我們使用了簡單的nginx+lua進行分布式限流,有了這個子產品也可以使用這個子產品來實作分布式限流。
另外在使用nginx+lua時也可以擷取ngx.var.connections_active進行過載保護,即如果目前活躍連接配接數超過門檻值進行限流保護。
nginx也提供了limit_rate用來對流量限速,如limit_rate 50k,表示限制下載下傳速度為50k。
到此筆者在工作中涉及的限流用法就介紹完,這些算法中有些允許突發,有些會整形為平滑,有些計算算法簡單粗暴;其中令牌桶算法和漏桶算法實作上是類似的,隻是表述的方向不太一樣,對于業務來說不必刻意去區分它們;是以需要根據實際場景來決定如何限流,最好的算法不一定是最适用的。
https://en.wikipedia.org/wiki/token_bucket
https://en.wikipedia.org/wiki/leaky_bucket
http://redis.io/commands/incr
http://nginx.org/en/docs/http/ngx_http_limit_req_module.html
http://nginx.org/en/docs/http/ngx_http_limit_conn_module.html
https://github.com/openresty/lua-resty-limit-traffic
http://nginx.org/en/docs/http/ngx_http_core_module.html#limit_rate
本文轉載自 開濤的部落格 kaitao-1234567 微信公衆号