基于OpenResty上传限速的实现

问题背景

一般而言,OpenResty做的都是反向代理的工作,负责流量治理类的工作。很常见的限速一般都是下载限速Nginx也提供了类似的指令:limit_rate。但是说巧不巧,业务方昨天提到了由网关来做针对客户端的上传文件时的限速功能,这就很曹丹了,一般都是为了用户体验度,都是提高上传的速度还有主动限制上传速度的;反正不管了,现将功能集成进去再说。

      

解决方案

但是问题来了,TMD,怎么做呢?OpenResty并没有提供类似的API来做这件事,唯一看到的一个Nginx的第三方模块:limit_upload_rate。了解后试着在OpenResty上进行打补丁,但是很曹丹的是自己对Nginx的源码并不熟悉而且这个第三方的模块是在饱经风霜,太老了~~。遇到了很多的问题,调试C代码花费了太多的精力。算了,换方案~

       接着就去google上搜了一番,就看到春哥有这么一句回复:

基于OpenResty上传限速的实现_第1张图片额~,不会  -.-!。

随即就去google论坛上发了一个帖子,好在大佬总是在你最绝望的时候给你安慰。就有人回复了。

基于OpenResty上传限速的实现_第2张图片

于是就有了下文,这里就将大佬给的解决方案,发放给大家希望能帮助到更多的人~~。

 

不过结果主要是 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 文档里面找不到的东西.

  • 关于 sock:receive 返回的 err == “closed” 判断

这个示例是我通过在 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");
            }
        }
    }
}

 

 

 

你可能感兴趣的:(openresty)