OpenResty實現限流的幾種方式

本文轉載自:http://www.itzh.org/2018/01/14/openresty_rate_limiter_methods/


在開發 api 網關的時,做過一些簡單的限流,比如說靜態攔截和動態攔截;靜態攔截說白了就是限流某一個接口在一定時間窗口的請求數。用戶可以在系統上給他們的接口配置一個每秒最大調用量,如果超過這個限制,則拒絕服務此接口,而動態攔截其實也是基於靜態攔截進行改進,我們可以依據當前系統的響應時間來動態調整限流的閾值,如果響應較快則可以把閾值調的大一些,放過更多請求,反之則自動降低限流閾值,只使少量請求通過。

其實這就是一個很簡單的限流方式。但是因爲這些場景在我們開發的時候經常遇到,所以在這裏用 OpenResty 大概實現一些常見的限流方式。(此處使用OpenResty1.13.6.1版本自帶lua-resty-limit-traffic模塊 ,實現起來更爲方便)

限制接口總併發數

場景:
按照 ip 限制其併發連接數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
lua_shared_dict my_limit_conn_store 100m;
...
location /hello {
   access_by_lua_block {
       local limit_conn = require "resty.limit.conn"
       -- 限制一個 ip 客戶端最大 1 個併發請求
       -- burst 設置爲 0,如果超過最大的併發請求數,則直接返回503,
       -- 如果此處要允許突增的併發數,可以修改 burst 的值(漏桶的桶容量)
       -- 最後一個參數其實是你要預估這些併發(或者說單個請求)要處理多久,以便於對桶裏面的請求應用漏桶算法
       
       local lim, err = limit_conn.new("my_limit_conn_store", 1, 0, 0.5)              
       if not lim then
           ngx.log(ngx.ERR, "failed to instantiate a resty.limit.conn object: ", err)
           return ngx.exit(500)
       end

       local key = ngx.var.binary_remote_addr
       -- commit 爲true 代表要更新shared dict中key的值,
       -- false 代表只是查看當前請求要處理的延時情況和前面還未被處理的請求數
       local delay, err = lim:incoming(key, true)
       if not delay then
           if err == "rejected" then
               return ngx.exit(503)
           end
           ngx.log(ngx.ERR, "failed to limit req: ", err)
           return ngx.exit(500)
       end

       -- 如果請求連接計數等信息被加到shared dict中,則在ctx中記錄下,
       -- 因爲後面要告知連接斷開,以處理其他連接
       if lim:is_committed() then
           local ctx = ngx.ctx
           ctx.limit_conn = lim
           ctx.limit_conn_key = key
           ctx.limit_conn_delay = delay
       end

       local conn = err
       -- 其實這裏的 delay 肯定是上面說的併發處理時間的整數倍,
       -- 舉個例子,每秒處理100併發,桶容量200個,當時同時來500個併發,則200個拒掉
       -- 100個在被處理,然後200個進入桶中暫存,被暫存的這200個連接中,0-100個連接其實應該延後0.5秒處理,
       -- 101-200個則應該延後0.5*2=1秒處理(0.5是上面預估的併發處理時間)
       if delay >= 0.001 then
           ngx.sleep(delay)
       end
   }

   log_by_lua_block {
       local ctx = ngx.ctx
       local lim = ctx.limit_conn
       if lim then
           local key = ctx.limit_conn_key
           -- 這個連接處理完後應該告知一下,更新shared dict中的值,讓後續連接可以接入進來處理
           -- 此處可以動態更新你之前的預估時間,但是別忘了把limit_conn.new這個方法抽出去寫,
           -- 要不每次請求進來又會重置
           local conn, err = lim:leaving(key, 0.5)
           if not conn then
               ngx.log(ngx.ERR,
                       "failed to record the connection leaving ",
                       "request: ", err)
               return
           end
       end
   }
   proxy_pass http://10.100.157.198:6112;
   proxy_set_header Host $host;
   proxy_redirect off;
   proxy_set_header X-Real-IP $remote_addr;
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   proxy_connect_timeout 60;
   proxy_read_timeout 600;
   proxy_send_timeout 600;
}

說明:
其實此處沒有設置 burst 的值,就是單純的限制最大併發數,如果設置了 burst 的值,並且做了延時處理,其實就是對併發數使用了漏桶算法,但是如果不做延時處理,其實就是使用的令牌桶算法。參考下面對請求數使用漏桶令牌桶的部分,併發數的漏桶令牌桶實現與之相似

限制接口時間窗請求數

場景:
限制 ip 每分鐘只能調用 120 次 /hello 接口(允許在時間段開始的時候一次性放過120個請求)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
lua_shared_dict my_limit_count_store 100m;
...

init_by_lua_block {
   require "resty.core"
}
....

location /hello {
   access_by_lua_block {
       local limit_count = require "resty.limit.count"

       -- rate: 10/min
       local lim, err = limit_count.new("my_limit_count_store", 120, 60)
       if not lim then
           ngx.log(ngx.ERR, "failed to instantiate a resty.limit.count object: ", err)
           return ngx.exit(500)
       end

       local key = ngx.var.binary_remote_addr
       local delay, err = lim:incoming(key, true)
       -- 如果請求數在限制範圍內,則當前請求被處理的延遲(這種場景下始終爲0,因爲要麼被處理要麼被拒絕)和將被處理的請求的剩餘數
       if not delay then
           if err == "rejected" then
               return ngx.exit(503)
           end

           ngx.log(ngx.ERR, "failed to limit count: ", err)
           return ngx.exit(500)
       end
   }

   proxy_pass http://10.100.157.198:6112;
   proxy_set_header Host $host;
   proxy_redirect off;
   proxy_set_header X-Real-IP $remote_addr;
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   proxy_connect_timeout 60;
   proxy_read_timeout 600;
   proxy_send_timeout 600;
}

平滑限制接口請求數

場景:
限制 ip 每分鐘只能調用 120 次 /hello 接口(平滑處理請求,即每秒放過2個請求)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
lua_shared_dict my_limit_req_store 100m;
....

location /hello {
   access_by_lua_block {
       local limit_req = require "resty.limit.req"
       -- 這裏設置rate=2/s,漏桶桶容量設置爲0,(也就是來多少水就留多少水)
       -- 因爲resty.limit.req代碼中控制粒度爲毫秒級別,所以可以做到毫秒級別的平滑處理
       local lim, err = limit_req.new("my_limit_req_store", 2, 0)
       if not lim then
           ngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err)
           return ngx.exit(500)
       end

       local key = ngx.var.binary_remote_addr
       local delay, err = lim:incoming(key, true)
       if not delay then
           if err == "rejected" then
               return ngx.exit(503)
           end
           ngx.log(ngx.ERR, "failed to limit req: ", err)
           return ngx.exit(500)
       end
   }

   proxy_pass http://10.100.157.198:6112;
   proxy_set_header Host $host;
   proxy_redirect off;
   proxy_set_header X-Real-IP $remote_addr;
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   proxy_connect_timeout 60;
   proxy_read_timeout 600;
   proxy_send_timeout 600;
}

漏桶算法限流

場景:
限制 ip 每分鐘只能調用 120 次 /hello 接口(平滑處理請求,即每秒放過2個請求),超過部分進入桶中等待,(桶容量爲60),如果桶也滿了,則進行限流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
lua_shared_dict my_limit_req_store 100m;
....

location /hello {
   access_by_lua_block {
       local limit_req = require "resty.limit.req"
       -- 這裏設置rate=2/s,漏桶桶容量設置爲0,(也就是來多少水就留多少水)
       -- 因爲resty.limit.req代碼中控制粒度爲毫秒級別,所以可以做到毫秒級別的平滑處理
       local lim, err = limit_req.new("my_limit_req_store", 2, 60)
       if not lim then
           ngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err)
           return ngx.exit(500)
       end

       local key = ngx.var.binary_remote_addr
       local delay, err = lim:incoming(key, true)
       if not delay then
           if err == "rejected" then
               return ngx.exit(503)
           end
           ngx.log(ngx.ERR, "failed to limit req: ", err)
           return ngx.exit(500)
       end
       
       -- 此方法返回,當前請求需要delay秒後纔會被處理,和他前面對請求數
       -- 所以此處對桶中請求進行延時處理,讓其排隊等待,就是應用了漏桶算法
       -- 此處也是與令牌桶的主要區別既
       if delay >= 0.001 then
           ngx.sleep(delay)
       end
   }

   proxy_pass http://10.100.157.198:6112;
   proxy_set_header Host $host;
   proxy_redirect off;
   proxy_set_header X-Real-IP $remote_addr;
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   proxy_connect_timeout 60;
   proxy_read_timeout 600;
   proxy_send_timeout 600;
}

令牌桶算法限流

令牌桶其實可以看着是漏桶的逆操作,看我們對把超過請求速率而進入桶中的請求如何處理,如果是我們把這部分請求放入到等待隊列中去,那麼其實就是用了漏桶算法,但是如果我們允許直接處理這部分的突發請求,其實就是使用了令牌桶算法。

場景:
限制 ip 每分鐘只能調用 120 次 /hello 接口(平滑處理請求,即每秒放過2個請求),但是允許一定的突發流量(突發的流量,就是桶的容量(桶容量爲60),超過桶容量直接拒絕

這邊只要將上面漏桶算法關於桶中請求的延時處理的代碼修改成直接送到後端服務就可以了,這樣便是使用了令牌桶

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
lua_shared_dict my_limit_req_store 100m;
....

location /hello {
   access_by_lua_block {
       local limit_req = require "resty.limit.req"

       local lim, err = limit_req.new("my_limit_req_store", 2, 0)
       if not lim then
           ngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err)
           return ngx.exit(500)
       end

       local key = ngx.var.binary_remote_addr
       local delay, err = lim:incoming(key, true)
       if not delay then
           if err == "rejected" then
               return ngx.exit(503)
           end
           ngx.log(ngx.ERR, "failed to limit req: ", err)
           return ngx.exit(500)
       end
       
       -- 此方法返回,當前請求需要delay秒後纔會被處理,和他前面對請求數
       -- 此處忽略桶中請求所需要的延時處理,讓其直接返送到後端服務器,
       -- 其實這就是允許桶中請求作爲突發流量 也就是令牌桶桶的原理所在
       if delay >= 0.001 then
       --    ngx.sleep(delay)
       end
   }

   proxy_pass http://10.100.157.198:6112;
   proxy_set_header Host $host;
   proxy_redirect off;
   proxy_set_header X-Real-IP $remote_addr;
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   proxy_connect_timeout 60;
   proxy_read_timeout 600;
   proxy_send_timeout 600;
}

說明:
其實nginxngx_http_limit_req_module 這個模塊中的delaynodelay也就是類似此處對桶中請求是否做延遲處理的兩種方案,也就是分別對應的漏桶和令牌桶兩種算法


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