一般而言,OpenResty做的都是反向代理的工作,负责流量治理类的工作。很常见的限速一般都是下载限速Nginx也提供了类似的指令:limit_rate。但是说巧不巧,业务方昨天提到了由网关来做针对客户端的上传文件时的限速功能,这就很曹丹了,一般都是为了用户体验度,都是提高上传的速度还有主动限制上传速度的;反正不管了,现将功能集成进去再说。
但是问题来了,TMD,怎么做呢?OpenResty并没有提供类似的API来做这件事,唯一看到的一个Nginx的第三方模块:limit_upload_rate。了解后试着在OpenResty上进行打补丁,但是很曹丹的是自己对Nginx的源码并不熟悉而且这个第三方的模块是在饱经风霜,太老了~~。遇到了很多的问题,调试C代码花费了太多的精力。算了,换方案~
接着就去google上搜了一番,就看到春哥有这么一句回复:
随即就去google论坛上发了一个帖子,好在大佬总是在你最绝望的时候给你安慰。就有人回复了。
于是就有了下文,这里就将大佬给的解决方案,发放给大家希望能帮助到更多的人~~。
不过结果主要是 limit_rate 这个指令, 这个指令是用来限制回复的速度的. 另一个介绍的比较多的是 client_max_body_size, 这个指令是用来限制客户端上传的文件大小. 和功能描述距离比较远. 最后找到了一个已经不活跃的模块, limit_upload_rate, 这个 nginx 的 c 模块提供了一些指令达到限速的目的, 瞄了一眼源码, 了解到应该是在 input filter
阶段做上传限速这个功能.
static ngx_http_input_body_filter_pt ngx_http_next_input_body_filter;
static ngx_int_t
ngx_http_limit_upload_input_body_filter(ngx_http_request_t *r, ngx_buf_t *buf)
{
off_t excess;
ngx_int_t rc;
ngx_msec_t delay;
ngx_http_core_loc_conf_t *clcf;
ngx_http_limit_upload_ctx_t *ctx;
ngx_http_limit_upload_conf_t *llcf;
rc = ngx_http_next_input_body_filter(r, buf);
if (rc != NGX_OK) {
return rc;
}
......
}
知道应该在什么阶段做这个事情之后, 应该就去找 openresty 提供的 api 了, 在 openresty 邮件列表找到了这篇帖子, 春哥在这篇帖子里面解释了由于 ngx_lua 的内在机制, 并没有实现跟 nginx 完全相同的 input filter
阶段, 但是也提供了另外的思路, 就是利用 ngx.req.socket 与 ngx.req.init_body(), ngx.req.append_body(), 和 ngx.req.finish_body() 这几个 api 结合, 可以模拟出 input filter
相同的功能, 知道了相关的 api 之后, 实现的话, 就没有太大的难度了. 具体的 api 介绍在 lua-nginx-module , 我不再介绍重复的内容.
附上我的实现代码, 如果你想要试一下的话可以看我的 github仓库, 我在这里阐述一下 api 文档里面找不到的东西.
这个示例是我通过在 lua-nginx-module 仓库搜索 ngx.req.init_body 得到的信息, 044-req-body.t 这是一个测试文件, 你可以在 1216
行找到这个判断, 然后我通过 postman, wireshark 抓包, 验证了这个 err 应该就是 openresty 提示请求体读取完毕, 因为我在测试过程中, 并没有发现 postman 有关闭写端, 发送 FIN 包, 这个连接也是 keepalive 的, 所以这个用法应该是正确的.
这个限流算法比较简单, 累加接收到的数据大小, 然后计算, 这个数据量, 在当前的限速条件下, 应该需要传输多少秒, 这是期待值, 用期待值与真正流逝的时间作差, 这样的话, 我们判断两者的差值, 当期待值大于当前当前花费的时间时, 就应该直接挂起差值这么久, 而期待值小于流逝的时间时, 代表客户端并没有跑满我们的限速, 可以继续读取.
sock:receive 的参数是 1 * 1024, 也就是每次读取 1kb, 可能会导致 ngx.req.append_body 的调用次数比较多, 应该可以根据精度要求调整这个值.
ngx.sleep 事实上在需要 sleep 时, 挂起时间可以比期待值与实际时间的差值, 也就是代码中的 interval
稍大, 因为客户端的速度已经快于我们的限速了, 我们可以适当的加上一点惩罚值, 来平衡它之后的速度, 如果我们只是挂起一个恰当好的时间, 它之后还是会因为超速被挂起. 加上惩罚值之后, 从性能方面考虑的话, 对于一个大文件, 应该可以节省很多次 ngx.sleep 的调用.
ngx.now 返回的是 nginx 缓存的时间, 可能也会对精度有一定的影响. 如果想要更精确, 可以在 ngx.now 之前, 强制 nginx 更新时间. 不过个人感觉没有必要.
# nginx.conf
worker_processes 1;
events {
worker_connections 1024;
}
http {
server {
listen 8888;
client_max_body_size 100M;
client_body_buffer_size 10K;
access_by_lua_block {
local sock = ngx.req.socket()
-- limit upload rate in ? kb/second
local function limit_recv_body(rate)
local rate_in_byte = rate * 1024
local function next_data_chunk()
local start = ngx.now()
local size = 0
while true do
local chunk, err = sock:receive(1 * 1024)
if not chunk then
if err == "closed" then
break
else
ngx.say( "failed to read body: ", err)
break
end
else
size = size + #chunk
coroutine.yield(chunk)
-- rate limit here
-- real time goes by
local delta = ngx.now() - start
-- how long we should take to receive `size` of bytes
local expected = size / rate_in_byte
-- if expected larger than delta, we should sleep for a while
local interval = expected - delta
if interval > 0 then
ngx.sleep(interval)
end
end
end
end
return coroutine.wrap(
function() next_data_chunk() end
)
end
ngx.req.init_body(128 * 1024)
-- iter req body, limit upload speed in 200kb/s
for chunk in limit_recv_body(200) do
ngx.req.append_body(chunk)
end
ngx.req.finish_body()
}
location / {
content_by_lua_block {
ngx.say("hello world");
}
}
}
}