Nginx接入層限流降級方案

                                         

Nginx 接入層限流方案

 

     接入層通常指請求的入口,該層的主要目的有:負載均衡、非法請求過濾、請求聚合、緩存、降級、限流、A/B測試、服務器質量監控等等。

     根據具體業務場景,限流措施我們選用Nginx 自帶的兩個模塊:連接數限流模塊ngx_http_limit_conn_module 和漏桶算法實現請求限流模塊ngx_http_limit_req_module

 

     limit_conn用來對某個KEY對應的總的網絡連接數進行限流,可以按照如IP、域名維度進行限流。limit_req用來對某個KEY對應的請求的平均速率進行限流,並有兩種用法:平滑模式(delay)和允許突發模式(nodelay)

 

ngx_http_limit_conn_module

 

    limit_conn是對某個KEY對應的總的網絡連接數進行限流。可以按照IP來限制IP維度的總連接數,或者按照服務域名來限制某個域名的總連接數。但是記住不是每一個請求連接都會被計數器統計,只有那些被Nginx處理的且已經讀取了整個請求頭的請求連接纔會被計數器統計。

 

 

http {

 

#Test ngx_http_limit_conn_module

 

    limit_conn_zone $binary_remote_addr zone=addr:10m;

 

    limit_conn_log_level error;

 

    limit_conn_status 508;

 

 

 

#server

 

    server {

 

        listen       80;

 

        server_name  10.6.8.123;

 

        server_name_in_redirect off;

 

        fastcgi_intercept_errors on;

 

        index index.htm index.html;

 

        

 

        access_log  logs/10.6.8.123_access.log access ; 

 

       

 

        location /limit {

 

            limit_conn addr 2;

 

        alias   /tmp/;

 

        }

 

     }

 

}

 

 

 

limit_conn:要配置存放KEY和計數器的共享內存區域和指定KEY的最大連接數;此處指定的最大連接數是1,表示Nginx最多同時併發處理2個連接;

limit_conn_zone:用來配置限流KEY、及存放KEY對應信息的共享內存區域大小;此處的KEY是“$binary_remote_addr”其表示IP地址,也可以使用如$server_name作爲KEY來限制域名級別的最大連接數;

limit_conn_status:配置被限流後返回的狀態碼,默認返回503

limit_conn_log_level:配置記錄被限流後的日誌級別,默認error級別。

 

limit_conn的主要執行過程如下所示:

1、請求進入後首先判斷當前limit_conn_zone中相應KEY的連接數是否超出了配置的最大連接數;

2.1、如果超過了配置的最大大小,則被限流,返回limit_conn_status定義的錯誤狀態碼;

2.2、否則相應KEY的連接數加1,並註冊請求處理完成的回調函數;

3、進行請求處理;

4、在結束請求階段會調用註冊的回調函數對相應KEY的連接數減1

 

limt_conn可以限流某個KEY的總併發/請求數,KEY可以根據需要變化。

 

通過ab測試工具測試:ab -n 5 -c 5 http://10.6.8.123/limit/ 

image2017-1-5%2011%3A12%3A9.png?version=1&modificationDate=1483585904000&api=v2

 

按照IP限制併發連接數配置示例:

 

首先定義IP緯度的限流區域:

 

limit_conn_zone $binary_remote_addr zone=perip:10m;

接着在要限流的location中添加限流邏輯:

 

location /limit {

 

     limit_conn perip 2;

 

     alias   /tmp/;

 

     }

使用AB測試工具進行測試,併發數爲5個,總的請求數爲5個:

將得到如下access.log輸出:

[08/Jun/2016:20:10:51+0800] [1465373451.802] 200

[08/Jun/2016:20:10:51+0800] [1465373451.803] 200

[08/Jun/2016:20:10:51 +0800][1465373451.803] 503

[08/Jun/2016:20:10:51 +0800][1465373451.803] 503

[08/Jun/2016:20:10:51 +0800][1465373451.803] 503

 

此處我們把access log格式設置爲log_format main  '[$time_local] [$msec] $status';分別是“日期 日期秒/毫秒值 響應狀態碼”。

 

如果被限流了,則在error.log中會看到類似如下的內容:

2016/06/08 20:10:51 [error] 5662#0: *5limiting connections by zone "perip", client: 127.0.0.1, server: _,request: "GET /limit HTTP/1.0", host: "localhost"

 

 

 

ngx_http_limit_req_module

 

limit_req是漏桶算法實現,用於對指定KEY對應的請求進行限流,比如按照IP維度限制請求速率。

 

配置示例:

http {

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

    limit_req_log_level error;

    limit_req_status 503;

    ...

    server {

    ...

    location /limit {

        limit_req zone=one burst=5 nodelay;

    }

limit_req:配置限流區域、桶容量(突發容量,默認0)、是否延遲模式(默認延遲);

limit_req_zone:配置限流KEY、及存放KEY對應信息的共享內存區域大小、固定請求速率;此處指定的KEY是“$binary_remote_addr”表示IP地址;固定請求速率使用rate參數配置,支持10r/s60r/m,即每秒10個請求和每分鐘60個請求,不過最終都會轉換爲每秒的固定請求速率(10r/s爲每100毫秒處理一個請求;60r/m,即每1000毫秒處理一個請求)。

limit_conn_status:配置被限流後返回的狀態碼,默認返回503

limit_conn_log_level:配置記錄被限流後的日誌級別,默認error級別。

 

注意使用$binary_remote_addr ,此變量在32位服務器上面佔用32字節,在64 位服務器上佔用64字節,因此,前面設置10m的zone,在32位服務器上面就能夠容納320000個狀態,在64位服務器上面就能容納160000個狀態。

 

limit_req的主要執行過程如下所示:

1、請求進入後首先判斷最後一次請求時間相對於當前時間(第一次是0)是否需要限流,如果需要限流則執行步驟2,否則執行步驟3

2.1、如果沒有配置桶容量(burst),則桶容量爲0;按照固定速率處理請求;如果請求被限流,則直接返回相應的錯誤碼(默認503);

2.2、如果配置了桶容量(burst>0)且延遲模式(沒有配置nodelay);如果桶滿了,則新進入的請求被限流;如果沒有滿則請求會以固定平均速率被處理(按照固定速率並根據需要延遲處理請求,延遲使用休眠實現);

2.3、如果配置了桶容量(burst>0)且非延遲模式(配置了nodelay);不會按照固定速率處理請求,而是允許突發處理請求;如果桶滿了,則請求被限流,直接返回相應的錯誤碼;

3、如果沒有被限流,則正常處理請求;

4Nginx會在相應時機進行選擇一些(3個節點)限流KEY進行過期處理,進行內存回收。

 

場景2.1測試

首先定義IP維度的限流區域:

limit_req_zone $binary_remote_addr zone=test:10m rate=500r/s;

限制爲每秒500個請求,固定平均速率爲2毫秒一個請求。

 

接着在要限流的location中添加限流邏輯:

location /limit {

    limit_req zone=test;

    echo "123";

}

即桶容量爲0burst默認爲0),且延遲模式。

 

使用AB測試工具進行測試,併發數爲2個,總的請求數爲10個:

ab -n 10 -c 2 http://localhost/limit

                 

將得到如下access.log輸出:

[08/Jun/2016:20:25:56+0800] [1465381556.410] 200

[08/Jun/2016:20:25:56 +0800][1465381556.410] 503

[08/Jun/2016:20:25:56 +0800][1465381556.411] 503

[08/Jun/2016:20:25:56+0800] [1465381556.411] 200

[08/Jun/2016:20:25:56 +0800][1465381556.412] 503

[08/Jun/2016:20:25:56 +0800][1465381556.412] 503

 

雖然每秒允許500個請求,但是因爲桶容量爲0,所以流入的請求要麼被處理要麼被限流,無法延遲處理;另外平均速率在2毫秒左右,比如1465381556.4101465381556.411被處理了;有朋友會說這固定平均速率不是1毫秒嘛,其實這是因爲實現算法沒那麼精準造成的。

 

如果被限流在error.log中會看到如下內容:

2016/06/08 20:25:56 [error] 6130#0: *1962limiting requests, excess: 1.000 by zone "test", client: 127.0.0.1,server: _, request: "GET /limit HTTP/1.0", host:"localhost"

 

如果被延遲了在error.log(日誌級別要INFO級別)中會看到如下內容:

2016/06/10 09:05:23 [warn] 9766#0: *97021delaying request, excess: 0.368, by zone "test", client: 127.0.0.1,server: _, request: "GET /limit HTTP/1.0", host:"localhost"

 

場景2.2測試

首先定義IP維度的限流區域:

limit_req_zone $binary_remote_addr zone=test:10m rate=2r/s;

爲了方便測試設置速率爲每秒2個請求,即固定平均速率是500毫秒一個請求。

 

接着在要限流的location中添加限流邏輯:

location /limit {

    limit_req zone=test burst=3;

    echo "123";

}

固定平均速率爲500毫秒一個請求,通容量爲3,如果桶滿了新的請求被限流,否則可以進入桶中排隊並等待(實現延遲模式)。

 

爲了看出限流效果我們寫了一個req.sh腳本:

ab -c 6 -n 6 http://localhost/limit

sleep 0.3

ab -c 6 -n 6 http://localhost/limit

首先進行6個併發請求6URL,然後休眠300毫秒,然後再進行6個併發請求6URL;中間休眠目的是爲了能跨越2秒看到效果,如果看不到如下的效果可以調節休眠時間。

 

將得到如下access.log輸出:

[09/Jun/2016:08:46:43+0800] [1465433203.959] 200

[09/Jun/2016:08:46:43 +0800][1465433203.959] 503

[09/Jun/2016:08:46:43 +0800][1465433203.960] 503

[09/Jun/2016:08:46:44+0800] [1465433204.450] 200

[09/Jun/2016:08:46:44+0800] [1465433204.950] 200

[09/Jun/2016:08:46:45 +0800][1465433205.453] 200

 

[09/Jun/2016:08:46:45 +0800][1465433205.766] 503

[09/Jun/2016:08:46:45 +0800][1465433205.766] 503

[09/Jun/2016:08:46:45 +0800][1465433205.767] 503

[09/Jun/2016:08:46:45+0800] [1465433205.950] 200

[09/Jun/2016:08:46:46+0800] [1465433206.451] 200

[09/Jun/2016:08:46:46+0800] [1465433206.952] 200

 

image2017-1-5%2011%3A12%3A49.png?version=1&modificationDate=1483585944000&api=v2

桶容量爲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.9591465433205.766來計算,計算結果爲1465433205.766這個時間點漏桶爲空了,可以流入桶中3個請求,其他請求被拒絕;又因爲第一輪最後一次處理時間是1465433205.453,所以第二輪第一個請求被延遲到了1465433205.950。這裏也要注意固定平均速率只是在配置的速率左右,存在計算精度問題,會有一些偏差。

 

如果桶容量改爲1burst=1),執行req.sh腳本可以看到如下輸出:

09/Jun/2016:09:04:30+0800] [1465434270.362] 200

[09/Jun/2016:09:04:30 +0800][1465434270.371] 503

[09/Jun/2016:09:04:30 +0800] [1465434270.372]503

[09/Jun/2016:09:04:30 +0800][1465434270.372] 503

[09/Jun/2016:09:04:30 +0800][1465434270.372] 503

[09/Jun/2016:09:04:30+0800] [1465434270.864] 200

 

[09/Jun/2016:09:04:31 +0800][1465434271.178] 503

[09/Jun/2016:09:04:31 +0800][1465434271.178] 503

[09/Jun/2016:09:04:31 +0800][1465434271.178] 503

[09/Jun/2016:09:04:31 +0800][1465434271.178] 503

[09/Jun/2016:09:04:31 +0800][1465434271.179] 503

[09/Jun/2016:09:04:31+0800] [1465434271.366] 200

桶容量爲1,按照每1000毫秒一個請求的固定平均速率處理請求。

 

場景2.3測試

首先定義IP維度的限流區域:

limit_req_zone $binary_remote_addr zone=test:10m rate=2r/s;

爲了方便測試配置爲每秒2個請求,固定平均速率是500毫秒一個請求。

 

接着在要限流的location中添加限流邏輯:

location /limit {

    limit_req zone=test burst=3 nodelay;

    echo "123";

}

桶容量爲3,如果桶滿了直接拒絕新請求,且每秒2最多兩個請求,桶按照固定500毫秒的速率以nodelay模式處理請求。

 

爲了看到限流效果我們寫了一個req.sh腳本:

ab -c 6 -n 6 http://localhost/limit

sleep 1

ab -c 6 -n 6 http://localhost/limit

sleep 0.3

ab -c 6 -n 6 http://localhost/limit

sleep 0.3

ab -c 6 -n 6 http://localhost/limit

sleep 0.3

ab -c 6 -n 6 http://localhost/limit

sleep 2

ab -c 6 -n 6 http://localhost/limit

 

將得到類似如下access.log輸出:

[09/Jun/2016:14:30:11+0800] [1465453811.754] 200

[09/Jun/2016:14:30:11+0800] [1465453811.755] 200

[09/Jun/2016:14:30:11+0800] [1465453811.755] 200

[09/Jun/2016:14:30:11+0800] [1465453811.759] 200

[09/Jun/2016:14:30:11 +0800][1465453811.759] 503

[09/Jun/2016:14:30:11 +0800][1465453811.759] 503

 

[09/Jun/2016:14:30:12+0800] [1465453812.776] 200

[09/Jun/2016:14:30:12+0800] [1465453812.776] 200

[09/Jun/2016:14:30:12 +0800][1465453812.776] 503

[09/Jun/2016:14:30:12 +0800][1465453812.777] 503

[09/Jun/2016:14:30:12 +0800][1465453812.777] 503

[09/Jun/2016:14:30:12 +0800][1465453812.777] 503

 

[09/Jun/2016:14:30:13 +0800] [1465453813.095]503

[09/Jun/2016:14:30:13 +0800][1465453813.097] 503

[09/Jun/2016:14:30:13 +0800][1465453813.097] 503

[09/Jun/2016:14:30:13 +0800][1465453813.097] 503

[09/Jun/2016:14:30:13 +0800][1465453813.097] 503

[09/Jun/2016:14:30:13 +0800][1465453813.098] 503

 

[09/Jun/2016:14:30:13+0800] [1465453813.425] 200

[09/Jun/2016:14:30:13 +0800][1465453813.425] 503

[09/Jun/2016:14:30:13 +0800][1465453813.425] 503

[09/Jun/2016:14:30:13 +0800][1465453813.426] 503

[09/Jun/2016:14:30:13 +0800][1465453813.426] 503

[09/Jun/2016:14:30:13 +0800][1465453813.426] 503

 

[09/Jun/2016:14:30:13+0800] [1465453813.754] 200

[09/Jun/2016:14:30:13 +0800][1465453813.755] 503

[09/Jun/2016:14:30:13 +0800][1465453813.755] 503

[09/Jun/2016:14:30:13 +0800][1465453813.756] 503

[09/Jun/2016:14:30:13 +0800][1465453813.756] 503

[09/Jun/2016:14:30:13 +0800][1465453813.756] 503

 

[09/Jun/2016:14:30:15+0800] [1465453815.278] 200

[09/Jun/2016:14:30:15+0800] [1465453815.278] 200

[09/Jun/2016:14:30:15+0800] [1465453815.278] 200

[09/Jun/2016:14:30:15 +0800][1465453815.278] 503

[09/Jun/2016:14:30:15 +0800][1465453815.279] 503

[09/Jun/2016:14:30:15 +0800][1465453815.279] 503

 

[09/Jun/2016:14:30:17+0800] [1465453817.300] 200

[09/Jun/2016:14:30:17+0800] [1465453817.300] 200

[09/Jun/2016:14:30:17+0800] [1465453817.300] 200

[09/Jun/2016:14:30:17+0800] [1465453817.301] 200

[09/Jun/2016:14:30:17 +0800][1465453817.301] 503

[09/Jun/2016:14:30:17 +0800][1465453817.301] 503

 

 image2017-1-5%2011%3A13%3A8.png?version=1&modificationDate=1483585963000&api=v2

 

桶容量爲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次請求的。

 

如果限流出錯了,可以配置錯誤頁面:

proxy_intercept_errors on;

recursive_error_pages on;

error_page 503 //www.jd.com/error.aspx;

limit_conn_zone/limit_req_zone定義的內存不足,則後續的請求將一直被限流,所以需要根據需求設置好相應的內存大小。

 

此處的限流都是單Nginx的,假設我們接入層有多個nginx,此處就存在和應用級限流相同的問題;那如何處理呢?一種解決辦法:建立一個負載均衡層將按照限流KEY進行一致性哈希算法將請求哈希到接入層Nginx上,從而相同KEY的將打到同一臺接入層Nginx上;另一種解決方案就是使用Nginx+LuaOpenResty)調用分佈式限流邏輯實現


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章