在應(yīng)用開(kāi)發(fā)中,經(jīng)常會(huì)有對(duì)請(qǐng)求進(jìn)行限速的需求。
通常意義上的限速,其實(shí)可以分為以下三種:
接下來(lái)讓我們看看,這三種限速在 OpenResty 中分別怎么實(shí)現(xiàn)。
Nginx 有一個(gè) $limit_rate
,這個(gè)變量反映的是當(dāng)前請(qǐng)求每秒能響應(yīng)的字節(jié)數(shù)。該字節(jié)數(shù)默認(rèn)為配置文件中 limit_rate
指令的設(shè)值。
一如既往,通過(guò) OpenResty,我們可以直接在 Lua 代碼中動(dòng)態(tài)設(shè)置它。
access_by_lua_block {
-- 設(shè)定當(dāng)前請(qǐng)求的響應(yīng)上限是 每秒 300K 字節(jié)
ngx.var.limit_rate = "300K"
}
對(duì)于連接數(shù)和請(qǐng)求數(shù)的限制,我們可以求助于 OpenResty 官方的 lua-resty-limit-traffic
需要注意的是,lua-resty-limit-traffic
要求 OpenResty 版本在 1.11.2.2
以上(對(duì)應(yīng)的 lua-nginx-module
版本是 0.10.6
)。
如果要配套更低版本的 OpenResty 使用,需要修改源碼。比如把代碼中涉及 incr(key, value, init)
方法,改成 incr(key, value)
和 set(key, init)
兩步操作。這么改會(huì)增大有潛在 race condition 的區(qū)間。
lua-resty-limit-traffic
這個(gè)庫(kù)是作用于所有 Nginx worker 的。
由于數(shù)據(jù)同步上的局限,在限制請(qǐng)求數(shù)的過(guò)程中 lua-resty-limit-traffic
有一個(gè) race condition 的區(qū)間,可能多放過(guò)幾個(gè)請(qǐng)求。誤差大小取決于 Nginx worker 數(shù)量。
如果要求“寧可拖慢一千,不可放過(guò)一個(gè)”的精確度,恐怕就不能用這個(gè)庫(kù)了。你可能需要使用 lua-resty-lock
或外部的鎖服務(wù),只是性能上的代價(jià)會(huì)更高。
lua-resty-limit-traffic
的限速實(shí)現(xiàn)基于漏桶原理。
通俗地說(shuō),就是小學(xué)數(shù)學(xué)中,蓄水池一邊注水一邊放水的問(wèn)題。
這里注水的速度是新增請(qǐng)求/連接的速度,而放水的速度則是配置的限制速度。
當(dāng)注水速度快于放水速度(表現(xiàn)為池中出現(xiàn)蓄水),則返回一個(gè)數(shù)值 delay。調(diào)用者通過(guò) ngx.sleep(delay)
來(lái)減慢注水的速度。
當(dāng)蓄水池滿時(shí)(表現(xiàn)為當(dāng)前請(qǐng)求/連接數(shù)超過(guò)設(shè)置的 burst 值),則返回錯(cuò)誤信息 rejected
。調(diào)用者需要丟掉溢出來(lái)的這部份。
下面是限制連接數(shù)的示例:
# nginx.conf
lua_code_cache on;
# 注意 limit_conn_store 的大小需要足夠放置限流所需的鍵值。
# 每個(gè) $binary_remote_addr 大小不會(huì)超過(guò) 16K,算上 lua_shared_dict 的節(jié)點(diǎn)大小,總共不到 64 字節(jié)。
# 100M 可以放 1.6M 個(gè)鍵值對(duì)
lua_shared_dict limit_conn_store 100M;
server {
listen 8080;
location / {
access_by_lua_file src/access.lua;
content_by_lua_file src/content.lua;
log_by_lua_file src/log.lua;
}
}
-- utils/limit_conn.lua
local limit_conn = require "resty.limit.conn"
-- new 的第四個(gè)參數(shù)用于估算每個(gè)請(qǐng)求會(huì)維持多長(zhǎng)時(shí)間,以便于應(yīng)用漏桶算法
local limit, limit_err = limit_conn.new("limit_conn_store", 10, 2, 0.05)
if not limit then
error("failed to instantiate a resty.limit.conn object: ", limit_err)
end
local _M = {}
function _M.incoming()
local key = ngx.var.binary_remote_addr
local delay, err = limit: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
if limit:is_committed() then
local ctx = ngx.ctx
ctx.limit_conn_key = key
ctx.limit_conn_delay = delay
end
if delay >= 0.001 then
ngx.log(ngx.WARN, "delaying conn, excess ", delay,
"s per binary_remote_addr by limit_conn_store")
ngx.sleep(delay)
end
end
function _M.leaving()
local ctx = ngx.ctx
local key = ctx.limit_conn_key
if key then
local latency = tonumber(ngx.var.request_time) - ctx.limit_conn_delay
local conn, err = limit:leaving(key, latency)
if not conn then
ngx.log(ngx.ERR,
"failed to record the connection leaving ",
"request: ", err)
end
end
end
return _M
-- src/access.lua
local limit_conn = require "utils.limit_conn"
-- 對(duì)于內(nèi)部重定向或子請(qǐng)求,不進(jìn)行限制。因?yàn)檫@些并不是真正對(duì)外的請(qǐng)求。
if ngx.req.is_internal() then
return
end
limit_conn.incoming()
-- src/log.lua
local limit_conn = require "utils.limit_conn"
limit_conn.leaving()
注意在限制連接的代碼里面,我們用 ngx.ctx
來(lái)存儲(chǔ) limit_conn_key
。這里有一個(gè)坑。內(nèi)部重定向(比如調(diào)用了 ngx.exec
)會(huì)銷(xiāo)毀 ngx.ctx
,導(dǎo)致 limit_conn:leaving()
無(wú)法正確調(diào)用。
如果需要限連業(yè)務(wù)里有用到 ngx.exec
,可以考慮改用 ngx.var
而不是 ngx.ctx
,或者另外設(shè)計(jì)一套存儲(chǔ)方式。只要能保證請(qǐng)求結(jié)束時(shí)能及時(shí)調(diào)用 limit:leaving()
即可。
限制請(qǐng)求數(shù)的實(shí)現(xiàn)差不多,這里就不贅述了。