OpenResty+Lua限流實戰
當業務量越來越大的時候,為了能保證服務的執行,限流是必不可少的!OpenResty是一個高效能閘道器
OpenResty® is a dynamic web platform based on NGINX and LuaJIT。
OpenResty = Nginx + Lua,Lua是高效能指令碼語言,有著C語言的執行效率但是又比C簡單,能很方便的擴充套件OpenResty 的功能。
Lua 是由巴西里約熱內盧天主教大學(Pontifical Catholic University of Rio de Janeiro)裡的一個研究小組於1993年開發的一種輕量、小巧的指令碼語言,用標準 C 語言編寫,其設計目的是為了嵌入應用程式中,從而為應用程式提供靈活的擴充套件和定製功能。
官網:http://www。lua。org/
實戰環境
docker + CentOS8 + Openresty 1。17。8。2
Lua限流模組
https://github。com/openresty/lua-resty-limit-traffic
Lua的庫一般都是小巧輕便且功能都具備,這個限流庫核心檔案一共就四個,幾百行程式碼就能實現限流功能,Lua的其他庫也是這樣,比如redis的庫還是Http的庫,麻雀雖小五臟俱全!
環境準備
docker run -dit ——name gw ——privileged centos /usr/sbin/initdocker exec -it gw bash
在gw中
# 安裝openrestyyum install -y yum-utilsyum-config-manager ——add-repo https://openresty。org/package/centos/openresty。repoyum install -y openresty# 安裝工具等yum install -y net-tools vim telnet git httpd# Openresty自帶了lua-resty-limit-traffic元件,如果沒有帶,下載到/usr/local/openresty/lualib/resty/limit/資料夾即可# 下載lua-resty-limit-traffic元件[ `ls /usr/local/openresty/lualib/resty/limit/ | wc -l` = 0 ] && echo ‘請安裝限速元件’ || echo ‘已經安裝限速元件’# 安裝了請忽略cd ~ && git clone https://github。com/openresty/lua-resty-limit-traffic。gitmkdir -p /usr/local/openresty/lualib/resty/limit/cp lua-resty-limit-traffic/lib/resty/limit/*。lua /usr/local/openresty/lualib/resty/limit/# 啟動openresyopenresty
限併發
場景:按照 ip 限制其併發連
參考: https://moonbingbing。gitbooks。io/openresty-best-practices/content/ngx_lua/lua-limit。html https://github。com/openresty/lua-resty-limit-traffic/blob/master/lib/resty/limit/conn。md https://developer。aliyun。com/article/759299
原理:lua_share_dict是nginx所有woker和lua runtime共享的,當一個請求來,往lua_share_dict記錄鍵值對
ip地址:1
,當請求完成時再-1,再來一個在+1,設定一個上限5,當超過5時則拒絕請求,一定要注意
內部重定向
的問題!
OpenResty執行階段 tag:lua執行流程;執行階段;openresty執行流程
為啥access_by_lua執行兩次
環境搭建
新建utils/limit_conn。lua模組
mkdir -p /usr/local/openresty/lualib/utilscat > /usr/local/openresty/lualib/utils/limit_conn。lua <
重點在於這句話
local limit, limit_err = limit_conn。new(“limit_conn_store”, 8, 2, 0。05)
,允許的最大併發為常規的8個,突發的2個,一共8+2=10個併發,詳情參考https://github。com/openresty/lua-resty-limit-traffic/blob/master/lib/resty/limit/conn。md#new
被拒絕的請求直接返回503
if err == “rejected” then return ngx。exit(503) —— 超過的請求直接返回503end
修改nginx配置檔案
# 備份一下配置檔案cd /usr/local/openresty/nginx/conf/ && \cp nginx。conf nginx。conf。bak# 新增配置echo ‘’ > /usr/local/openresty/nginx/conf/nginx。confvim /usr/local/openresty/nginx/conf/nginx。conf
新增如下內容
worker_processes 1;events { worker_connections 1024;}http { include mime。types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; lua_code_cache on; # 注意 limit_conn_store 的大小需要足夠放置限流所需的鍵值。 # 每個 $binary_remote_addr 大小不會超過 16 位元組(IPv6 情況下),算上 lua_shared_dict 的節點大小,總共不到 64 位元組。 # 100M 可以放 1。6M 個鍵值對 lua_shared_dict limit_conn_store 100M; server { listen 80; location / { access_by_lua_block { local limit_conn = require “utils。limit_conn” —— 對於內部重定向或子請求,不進行限制。因為這些並不是真正對外的請求。 if ngx。req。is_internal() then ngx。log(ngx。INFO,“>> 內部重定向”) return end limit_conn。incoming() ngx。log(ngx。INFO,“>>> 請求進來了!”) } content_by_lua_block { —— 模擬請求處理時間,很重要,不加可能測試不出效果 —— 生產中沒有請求是隻返回一個靜態的index。html的! ngx。sleep(0。5) } log_by_lua_block { local limit_conn = require “utils。limit_conn” limit_conn。leaving() ngx。log(ngx。INFO,“>>> 請求離開了!”) } } }}
重點在於這句話,模擬每個請求0。5秒處理完成
content_by_lua_block { ngx。sleep(0。5)}
注意在限制連線的程式碼裡面,我們用
ngx。ctx
來儲存
limit_conn_key
。這裡有一個坑。內部重定向(比如呼叫了
ngx。exec
)會銷燬
ngx。ctx
,導致
limit_conn:leaving()
無法正確呼叫。 如果需要限連業務裡有用到
ngx。exec
,可以考慮改用
ngx。var
而不是
ngx。ctx
,或者另外設計一套儲存方式。只要能保證請求結束時能及時呼叫
limit:leaving()
即可。
重新載入配置檔案
openresty -s reload
測試
上面的配置是每個請求處理0。5秒,併發是10
10個請求,併發為1
ab -n 10 -c 1 127。0。0。1/# 請求全部成功,用時5s左右Concurrency Level: 1Time taken for tests: 5。012 seconds Complete requests: 10 Failed requests: 0
10個請求,併發為10
ab -n 10 -c 10 127。0。0。1/# 請求全部成功,用時1。5s左右Concurrency Level: 10Time taken for tests: 1。505 secondsComplete requests: 10Failed requests: 0
20個請求,併發為10,併發為10並不會觸發限制條件,所以能成功!注意和下面併發11的區別!
ab -n 20 -c 10 127。0。0。1/# 請求全部成功,用時2s左右Concurrency Level: 10Time taken for tests: 2。005 secondsComplete requests: 20Failed requests: 0
22個請求,併發為11 重點解釋一下:併發不是qps,併發11不是說第一秒發11個請求,然後第二秒再發送11個請求,而是發完第一波緊接著發第二波,每一波的間隔時間不一定是1秒,下面的1。506 seconds就能看出來,按理應該是2s但是並不是第一波11個請求傳送過去了,但是隻能處理10個,所以成功了10個,緊接著第二波11個請求發過去了,但是第一波大部分未處理完成所以第二波的都失敗了,也有處理完成了的可以接著處理,所以至少會成功10個,下面顯示的是11個此處的大量失敗應該是併發超過了10,觸發了限制條件讓nginx worker執行緒睡眠了,所以導致後面的請求大量失敗
—— 觸發限制條件
if
delay
>=
0。001
then
ngx。sleep
(
delay
)
—— ngx worker睡眠
end
ab -n 22 -c 11 127。0。0。1/# 11個成功,11個失敗Concurrency Level: 11Time taken for tests: 1。506 secondsComplete requests: 22Failed requests: 11Non-2xx responses: 11 # HTTP狀態非2xx的有11個,說明限併發成功(只有有非2xx的返回才會顯示這句話)
反向代理
上面測試的是
content_by_lua
,也就是內容直接在lua中生成,但是實際中內容有可能是後端伺服器生成的,所以可以設定反向代理或者負載均衡,如下為反向代理配置
location / { access_by_lua_block { local limit_conn = require “utils。limit_conn” —— 對於內部重定向或子請求,不進行限制。因為這些並不是真正對外的請求。 if ngx。req。is_internal() then return end limit_conn。incoming() } log_by_lua_block { local limit_conn = require “utils。limit_conn” limit_conn。leaving() } # 反向代理 proxy_pass http://172。17。0。3:8080; 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;}
內部重定向
location / { access_by_lua_block {。。。} content_by_lua_block {。。。} log_by_lua_block {。。。}}
nginx是按照階段來執行指令的,和配置檔案順序沒有關係,nginx是先執行
access_by_lua_block
,再執行
content_by_lua_block
,最後執行
log_by_lua_block
的,當在訪問
curl 127。0。0。1/
時,如果沒有
content_by_lua_block
,這裡有一個
內部重定向
,會將127。0。0。1/的請求重定向到
127。0。0。1/index。html
,所以會按順序再次執行
access_by_lua_block
,所以
access_by_lua_block
執行了兩次,
log_by_lua_block
卻執行了一次,當時的我十分懵逼,而
加上content_by_lua或者proxy_pass則不會導致重定向
,總之有內容來源時不會重定向,沒有則會去找
index。html
導致重定向!
測試
vim /usr/local/openresty/nginx/conf/nginx。conf# 修改成如下內容server { listen 80; location / { access_by_lua_block { ngx。log(ngx。ERR,“>>> access”) } log_by_lua_block { ngx。log(ngx。ERR,“>>> log”) } }}# 檢視日誌tail -f /usr/local/openresty/nginx/logs/error。log
測試
curl 127。0。0。1
日誌輸出如下
access_by_lua_block
執行了兩次,並且頁面上的內容是
index。html
的內容,說明發生了重定向 如果加上
index。html
,即
curl 127。0。0。1/index。html
,則不會發生重定向
。。。[lua] access_by_lua(nginx。conf:24):2: >>> access, client: 127。0。0。1, server: , request: “GET / HTTP/1。1”, host: “127。0。0。1”。。。[lua] access_by_lua(nginx。conf:24):2: >>> access, client: 127。0。0。1, server: , request: “GET / HTTP/1。1”, host: “127。0。0。1”。。。[lua] log_by_lua(nginx。conf:27):2: >>> log while logging request, client: 127。0。0。1, server: , request: “GET / HTTP/1。1”, host: “127。0。0。1”
加上content_by_lua則訪問http://127。0。0。1不會發生重定向
lua初始化
這句話
local limit_conn = require “utils。limit_conn”
,
limit_conn
中的
local limit, limit_err = limit_conn。new(“limit_conn_store”, 8, 2, 0。05)
只會初始化一次,之後都是用的都一個例項,不會每個請求進來都要new一個
limit_conn
有點浪費效能而且還把引數都重置了,是不可取的,所以封裝到了
utils。limit_conn
中!
限制介面時間窗請求數(非平滑)
場景:限制 ip 每1s只能呼叫 10 次(允許在時間段開始的時候一次性放過10個請求)也就是說,速率不是固定的
也可以設定成別的,比如120/min,只需要修改個數和時間視窗(
resty。limit。count
和
resty。limit。req
區別在於:前者傳入的是個數,後者傳入的是速率)
新建utils/limit_count。lua模組
mkdir -p /usr/local/openresty/lualib/utilscat > /usr/local/openresty/lualib/utils/limit_count。lua < 修改nginx配置檔案 echo ‘’ > /usr/local/openresty/nginx/conf/nginx。confvim /usr/local/openresty/nginx/conf/nginx。conf 新增如下內容 worker_processes 1;events { worker_connections 1024;}http { include mime。types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; lua_code_cache on; lua_shared_dict my_limit_count_store 100M; # resty。limit。count 需要resty。core init_by_lua_block { require “resty。core” } server { listen 80; location / { access_by_lua_block { local limit_count = require “utils。limit_count” —— 對於內部重定向或子請求,不進行限制。因為這些並不是真正對外的請求。 if ngx。req。is_internal() then return end limit_count。incoming() } content_by_lua_block { ngx。sleep(0。1) ngx。say(‘Hello’) } # 如果內容源是反向代理 #proxy_pass http://172。17。0。3:8080; #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; } }} 重新載入配置檔案 openresty -s reload 測試 上面的配置是10/s,不疊加 10個請求,併發為10,1s內完成 ab -n 10 -c 10 127。0。0。1/# 請求全部成功Concurrency Level: 10Time taken for tests: 0。202 secondsComplete requests: 10Failed requests: 0 20個請求,併發為20,1s內完成 ab -n 20 -c 20 127。0。0。1/# 請求成功10個,其餘全部失敗Concurrency Level: 20Time taken for tests: 0。202 secondsComplete requests: 20Failed requests: 10 (Connect: 0, Receive: 0, Length: 10, Exceptions: 0)Non-2xx responses: 10 檢視請求頭 curl -I 127。0。0。1 ,可以看到介面限流資訊 HTTP/1。1 200 OKServer: openresty/1。17。8。2Date: Sat, 12 Sep 2020 09:46:06 GMTContent-Type: application/octet-streamConnection: keep-aliveX-RateLimit-Limit: 10 # 當前限制10個X-RateLimit-Remaining: 9 # 剩餘9個 限制介面時間窗請求數(平滑) 桶(無容量) 場景:限制 ip 每1min只能呼叫 120次(平滑處理請求,即每秒放過2個請求),速率是固定的,並且桶沒有容量(容量為0) 新建utils/limit_req_bucket。lua模組 mkdir -p /usr/local/openresty/lualib/utilscat > /usr/local/openresty/lualib/utils/limit_req_bucket。lua < 修改nginx配置檔案 echo ‘’ > /usr/local/openresty/nginx/conf/nginx。confvim /usr/local/openresty/nginx/conf/nginx。conf 新增如下內容 worker_processes 1;events { worker_connections 1024;}http { include mime。types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; lua_code_cache on; lua_shared_dict my_limit_req_store 100M; server { listen 80; location / { access_by_lua_block { local limit_count = require “utils。limit_req_bucket” —— 對於內部重定向或子請求,不進行限制。因為這些並不是真正對外的請求。 if ngx。req。is_internal() then return end limit_count。incoming() } content_by_lua_block { ngx。sleep(0。1) ngx。say(‘Hello’) } # 如果內容源是反向代理 #proxy_pass http://172。17。0。3:8080; #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; } }} 重新載入配置檔案 openresty -s reload 測試 上面的配置是2/s即為120/min 請求時間限制為1s ab -t 1 127。0。0。1/# 實際請求1。1s,成功3個請求,符合預期Time taken for tests: 1。100 secondsComplete requests: 8656Failed requests: 8653 (Connect: 0, Receive: 0, Length: 8653, Exceptions: 0)Non-2xx responses: 8653 請求時間限制為5s ab -t 5 127。0。0。1/# 實際請求5。1s,成功11個請求,符合預期Concurrency Level: 1Time taken for tests: 5。100 secondsComplete requests: 40054Failed requests: 40043 (Connect: 0, Receive: 0, Length: 40043, Exceptions: 0)Non-2xx responses: 40043 漏桶(有桶容量) 場景:限制 ip 每1min只能呼叫 120次(平滑處理請求,即每秒放過2個請求),速率是固定的,並且桶的容量有容量(設定burst) 新建utils/limit_req_leaky_bucket。lua模組 只需要在 桶(無容量) 的基礎之上增加burst的值即可,並且增加delay的處理 mkdir -p /usr/local/openresty/lualib/utilscat > /usr/local/openresty/lualib/utils/limit_req_leaky_bucket。lua < 修改nginx配置檔案 echo ‘’ > /usr/local/openresty/nginx/conf/nginx。confvim /usr/local/openresty/nginx/conf/nginx。conf 新增如下內容 worker_processes 1;events { worker_connections 1024;}http { include mime。types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; lua_code_cache on; lua_shared_dict my_limit_req_store 100M; server { listen 80; location / { access_by_lua_block { local limit_count = require “utils。limit_req_leaky_bucket” —— 對於內部重定向或子請求,不進行限制。因為這些並不是真正對外的請求。 if ngx。req。is_internal() then return end limit_count。incoming() } content_by_lua_block { —— 模擬每個請求的耗時 ngx。sleep(0。1) ngx。say(‘Hello’) } # 如果內容源是反向代理 #proxy_pass http://172。17。0。3:8080; #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; } }} 重新載入配置檔案 openresty -s reload 測試 上面的配置是2/s,漏桶容量為1/s,即總共3/s,模擬的每個請求耗時為0。1s,那麼1s內能處理至少10個請求 請求時間限制為1s ab -t 1 127。0。0。1/# 實際請求1。102s,成功3個請求,1s兩個請求,一個是delay,符合預期Time taken for tests: 1。103 secondsComplete requests: 3Failed requests: 0 令牌桶 場景:限制 ip 每1min只能呼叫 120次(平滑處理請求,即每秒放過2個請求),但是允許一定的突發流量(突發的流量,就是桶的容量(桶容量為60),超過桶容量直接拒絕 令牌桶其實可以看著是漏桶的 逆操作 ,看我們對把超過請求速率而進入桶中的請求如何處理,如果是我們把這部分請求放入到 等待佇列 中去,那麼其實就是用了漏桶演算法,但是如果我們允許 直接處理這部分的突發請求 ,其實就是使用了令牌桶演算法。 這邊只要將上面漏桶演算法關於桶中請求的延時處理的程式碼修改成直接送到後端服務就可以了,這樣便是使用了令牌桶 新建utils/limit_req_token_bucket。lua模組 mkdir -p /usr/local/openresty/lualib/utilscat > /usr/local/openresty/lualib/utils/limit_req_token_bucket。lua < 修改nginx配置檔案 echo ‘’ > /usr/local/openresty/nginx/conf/nginx。confvim /usr/local/openresty/nginx/conf/nginx。conf 新增如下內容 worker_processes 1;events { worker_connections 1024;}http { include mime。types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; lua_code_cache on; lua_shared_dict my_limit_req_store 100M; server { listen 80; location / { access_by_lua_block { local limit_count = require “utils。limit_req_token_bucket” —— 對於內部重定向或子請求,不進行限制。因為這些並不是真正對外的請求。 if ngx。req。is_internal() then return end limit_count。incoming() } content_by_lua_block { —— 模擬每個請求的耗時 ngx。sleep(0。1) ngx。say(‘Hello’) } # 如果內容源是反向代理 #proxy_pass http://172。17。0。3:8080; #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; } }} 重新載入配置檔案 openresty -s reload 測試 上面模擬的每個請求耗時為0。1s,那麼1s內能處理至少10個請求 時間限制為1s ab -n 10 -c 10 -t 1 127。0。0。1/# 實際請求1s,成功13個請求,可以看到是遠遠超過2個請求的,多餘就是在處理突發請求Concurrency Level: 10Time taken for tests: 1。000 secondsComplete requests: 12756Failed requests: 12743 (Connect: 0, Receive: 0, Length: 12743, Exceptions: 0)Non-2xx responses: 12743 組合各種limter 上面的三種限速器conn、count、req可以進行各種組合,比如一個限速器是限制主機名的,一個是限制ip的,可以組合起來使用 參考:https://github。com/openresty/lua-resty-limit-traffic/blob/master/lib/resty/limit/traffic。md