淺談限流(上)

限流的必要性

隨着應用的訪問量愈來愈高,瞬時流量不可預估,爲了保證服務對外的穩定性,限流成爲每一個應用必備的一道安全防火牆,即便普通的用戶也會常常遇到,如微博的限流,抖音的限流,小米搶購的限流......若是沒有這道安全防火牆,請求的流量超過服務的負載能力,很容易形成整個服務的癱瘓。
限流須要提早評估好,若是用的不當,可能會致使有些該限制的流量沒有被限流,服務被這些過載流量打垮。有些不應限制流量的被限制,被用戶抱怨。例如,總體服務的QPS是400/s,若是限流閥值是300,就會致使每秒有100個請求本該接受服務,卻被限制訪問,若是閥值是500,就會致使每秒有100個請求負載,時間越長累積越多,這些過載的流量就有可能致使整個服務的癱瘓。node

限流的算法

常見的限流算法有令牌同、漏桶,還有一種計數器。web

令牌桶

令牌算法的過程以下算法

  1. 假如用戶配置的平均發送速率爲r,則每隔1/r秒一個令牌被加入到桶中
  2. 假設桶最多能夠存發b個令牌。若是令牌到達時令牌桶已經滿了,那麼這個令牌會被丟棄;
  3. 當一個n個字節的數據包到達時,就從令牌桶中刪除n個令牌,而且數據包被髮送到網絡;
  4. 若是令牌桶中少於n個令牌,那麼不會刪除令牌,而且認爲這個數據包在流量限制以外,要不丟棄要不緩衝區等待
    在這裏插入圖片描述後端

    漏桶

    一直以爲應該叫漏斗啊。
  5. 一個固定容量的漏桶,按照固定速率流出漏桶
  6. 能夠以任意速度流入水桶
  7. 若是流入的水超過桶的容量,則水就溢出,被丟棄
    在這裏插入圖片描述
    #### 令牌桶和漏桶的比較
  8. 令牌桶是按照固定速率往桶中添加令牌,請求是否處理主要看同種是否有令牌,流入不限制,能夠一次拿多個令牌,只要桶中有令牌,則處理請求;若是沒有令牌,則拒絕請求。
  9. 漏桶則是流入請求不限制,按照固定速率流出請求,若是流入的請求的速度小於等於流出的請求,桶爲空桶,則處理請求;若是流入的請求的速度大於流出的請求,累積請求留在同種,可是桶未滿,則處理請求;若是累積請求大於桶容量時,則拒絕請求。
  10. 兩個算法實現同樣,方向相反,令牌是勻速流入,流通是勻速流出。安全

#### 計數器
計數器比較簡單,沒有什麼算法和描述。知足必定的條件的流量計數加1,達到閥值了限制,顧名思義叫計數限流。服務器

限流使用

使用最多見的就是Nginx自帶兩個限流模塊:鏈接數限流模塊ngx_http_limit_conn_module 和請求數限流模塊ngx_http_limit_req_module;還有openresty的限流模塊lua-resty-limit-traffic;還可能須要應對複雜的業務需求而自研的計數限流。咱們一一介紹下這些限流方法的使用網絡

ngx_http_limit_conn_module

從名字就能夠看出是Nginx的鏈接數限流。大多都是按照IP來源進行鏈接數限流,也能夠按照域名對總的鏈接數進行限流。
咱們看下鏈接數限流的配置性能

http {
    limit_conn_zone $binary_remote_addr zone=addr:10m;
    limit_conn_log_level error;
    limit_conn_status 503;
    ...
    server {
        ...
        location /download/ {
            limit_conn addr 1;
        }

limit_conn_zone: 配置限流的key以及存儲這些key共用的共享內存的大小;
樣例中的key 是$binary_remote_addr,表示IP地址,若是若是須要對總域名進行限流,key就應該使用 $server_name $host等等,能惟一表示該域名的Nginx變量;
zone=addr:10m中,addr表示鏈接數限流的區域名稱,10m表示能夠分配的共享空間的大小。
binary_remote_addr變量在64位平臺中佔用64字節。1M共享空間能夠保存1.6萬個64位的,10m就能夠保存16萬個。若是超過16萬個,共享空間被用完,服務器將會對後續全部的請求返回 503。
limit_conn:配置指定key的最大鏈接數。樣例中指定的最大鏈接數是1,表示Nginx最多同時容許1個鏈接進行location /limit 的行爲操做。
limit_conn_status:配置被限流後返回的狀態碼,樣例中設置的是503.
limit_conn_log_level:配置被限流後的日誌級別,設置的是error級別
看下測試代碼測試

limit_conn_zone $server_name zone=addr:10m;
limit_conn_log_level error;
limit_conn_status 503;
server{
    listen      80;
    server_name test.test.com;
    access_log /var/log/openresty/web_test.test.com_access.log test;
    error_log /var/log/openresty/web_test.test.com_error.log;
    location /test/ {
        limit_conn addr 2;
        content_by_lua '
                ngx.sleep(1)
                ngx.say("helloworld")
        ';
    }
}
ab 命令
ab -n10 -c3 http://test.test.com/test/
access_log
127.0.0.1|1553438999.158|200
127.0.0.1|1553438999.160|503
127.0.0.1|1553438999.160|503
127.0.0.1|1553438999.161|503
127.0.0.1|1553438999.162|503
127.0.0.1|1553438999.163|503
127.0.0.1|1553438999.163|503
127.0.0.1|1553438999.164|503
127.0.0.1|1553439000.160|200
127.0.0.1|1553439000.160|200
error_log
2019/03/24 22:49:59 [error] 700#0: *63 limiting connections by zone "addr", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
2019/03/24 22:49:59 [error] 700#0: *64 limiting connections by zone "addr", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
2019/03/24 22:49:59 [error] 700#0: *65 limiting connections by zone "addr", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
2019/03/24 22:49:59 [error] 700#0: *66 limiting connections by zone "addr", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
2019/03/24 22:49:59 [error] 700#0: *67 limiting connections by zone "addr", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
2019/03/24 22:49:59 [error] 700#0: *68 limiting connections by zone "addr", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
2019/03/24 22:49:59 [error] 700#0: *69 limiting connections by zone "addr", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"

能夠看到是符合咱們配置語氣的。若是咱們將
limit_conn_log_level info;
limit_conn_status 500;
能夠看到,error_log裏面記錄的日誌就是info的,固然error_log的級別要調到info級別。返回的HTTP狀態碼也會變爲500.能夠動手試下。lua

limit_conn 的執行過程
請求進入首先判判定義的key的鏈接數是否超過limit_conn配置的閥值,若是超過直接返回limit_conn_status定義的錯誤碼;若是沒有超過鏈接數+1
請求處理
請求處理完成以後鏈接數-1

這就是爲何要作下sleep操做,不然在測試環境下沒有任何壓力,兩個鏈接數徹底能夠在一秒以內處理完10個請求。爲了測試出效果,就須要在一秒以內讓Nginx沒法完成10個請求。

ngx_http_limit_req_module

Nginx的請求數限流,請求數限流是漏桶算法實現的。經過定義的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 /search/ {
            limit_req zone=one burst=5;
        }

limit_req_zone:配置限流的key,存放key對應的共享區域空間大小,固定的請求速率。樣例中的key binary_remote_addr 表示IP地址。one 表示共享區域空間的名稱,10m表示共享區域空間的大小,跟limit_conn的定義一致,10m就能夠保存16萬個IP地址。rate=1r/s 固定請求速率設置,每秒1個請求。
limit_req:配置限流區域,桶容量,是否延遲模式。樣例中桶容量是5,延遲模式默認是延遲。
limit_req_status:配置被限流後返回的狀態。樣例中是503
limit_req_log_level:配置被限流後的日誌級別,樣例中是error
測試下上面的代碼

看下測試代碼

limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
 limit_req_log_level error;
 limit_req_status 503;
server{
    listen      80;
    server_name test.test.com;
    access_log /var/log/openresty/web_test.test.com_access.log test;
    error_log /var/log/openresty/web_test.test.com_error.log info;
    location /test/ {
        limit_req zone=one burst=5;
        content_by_lua '
                ngx.say("helloworld")
        ';
    }
}

ab 命令
ab -n10 -c10 http://test.test.com/test/

access_log
127.0.0.1|1553525058.469|200
127.0.0.1|1553525058.470|503
127.0.0.1|1553525058.470|503
127.0.0.1|1553525058.470|503
127.0.0.1|1553525058.470|503
127.0.0.1|1553525059.471|200
127.0.0.1|1553525060.470|200
127.0.0.1|1553525061.470|200
127.0.0.1|1553525062.471|200
127.0.0.1|1553525063.471|200

error_log
2019/03/25 22:44:18 [warn] 833#0: *144 delaying request, excess: 0.999, by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
2019/03/25 22:44:18 [warn] 833#0: *145 delaying request, excess: 1.999, by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
2019/03/25 22:44:18 [warn] 833#0: *146 delaying request, excess: 2.999, by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
2019/03/25 22:44:18 [warn] 833#0: *147 delaying request, excess: 3.999, by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
2019/03/25 22:44:18 [warn] 833#0: *148 delaying request, excess: 4.999, by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
2019/03/25 22:44:18 [error] 833#0: *149 limiting requests, excess: 5.999 by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
2019/03/25 22:44:18 [error] 833#0: *150 limiting requests, excess: 5.999 by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
2019/03/25 22:44:18 [error] 833#0: *151 limiting requests, excess: 5.999 by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"
2019/03/25 22:44:18 [error] 833#0: *152 limiting requests, excess: 5.999 by zone "one", client: 127.0.0.1, server: test.test.com, request: "GET /test/ HTTP/1.0", host: "test.test.com"

測試代碼中桶容量是5,按照1r/s的速度處理。能夠看到在,因爲默認是延遲模式,因此1553525059.471到1553525063.471這個時間段最多存儲5個請求,而後按照1r/s的速度處理,因爲延遲模式,error_log能夠看到這五條記錄都是延遲執行的(delaying request)。大於五條的記錄都限流503 了。
那爲何第一條記錄執行成功了?這應該是計算算法的問題,第一條記錄沒有參考值,因此第一秒沒有計算在內,這以後的都是按照第一條記錄參考的時間,因此後面的基本上都是精確的。

咱們將延遲模式改成不延遲模式看下。

location /test/ {
        limit_req zone=one burst=5 nodelay;
    content_by_lua '
        ngx.say("helloworld")
    ';
    }
ab 測試
ab -n7 -c7 http://test.test.com/test/
ab -n7 -c7 http://test.test.com/test/
ab -n7 -c7 http://test.test.com/test/

access_log

127.0.0.1|1554385661.861|200
127.0.0.1|1554385661.862|200
127.0.0.1|1554385661.862|200
127.0.0.1|1554385661.862|200
127.0.0.1|1554385661.862|200
127.0.0.1|1554385661.862|200
127.0.0.1|1554385661.862|503

127.0.0.1|1554385665.513|200
127.0.0.1|1554385665.514|200
127.0.0.1|1554385665.514|200
127.0.0.1|1554385665.514|503
127.0.0.1|1554385665.514|503
127.0.0.1|1554385665.514|503
127.0.0.1|1554385665.514|503

127.0.0.1|1554385667.361|200
127.0.0.1|1554385667.361|200
127.0.0.1|1554385667.362|503
127.0.0.1|1554385667.362|503
127.0.0.1|1554385667.362|503
127.0.0.1|1554385667.362|503
127.0.0.1|1554385667.362|503

咱們爲了跨時間窗口測試,咱們測試三組。先看下第一組,7個請求6個成功,一個503,其實理論上桶容量是5,至多隻可能成功5個,有個503纔對。咱們說了第一組計算算法問題基本上忽略的。
咱們看下第二組,跟第一組相差4秒,處理速度是1r/s.4秒以後按按理應該桶裏有4個位置,應該成功處理4個,3個503,怎麼如今是4個503,成功處理三個,此處仍是要強調下limit_req的實現算法不是特別精確
咱們看下第三組,比第二組晚了2秒,因此桶裏會有2個位置,應該有2個請求成功,5個請求503.這個跟預想的基本吻合。
因此總體上和理解是一致的。就是算法上不是特別的精確。咱們生產上限流也是至少幾千幾萬的限流,算法上的精確差別實際上是能夠忽略不計的。
這一部分主要是聊了下限流的原理和常見的Nginx的兩個限流模塊。下一部分咱們聊下生產中比較常見的lua限流。

------------------------------------end
一塊兒關注高性能WEB後端技術,關注公衆號

相關文章
相關標籤/搜索