Openresty的同步输出与流式响应

默认情况下, ngx.say和ngx.print都是异步输出的,先来看一个例子:

location /test {
    content_by_lua_block {
        ngx.say("hello")
        ngx.sleep(3)
        ngx.say("the world")
    }
}

执行测试,可以发现首先, /test 响应内容是在触发请求 3s 后一起接收到响应体,第一个ngx.say好像是被“绕过”,先执行sleep,然后和最后一个ngx.say的内容一起输出。

location /test {
    content_by_lua_block {
        ngx.say("hello")
        ngx.flush() -- 显式的向客户端刷新响应输出
        ngx.sleep(3)
        ngx.say("the world")
    }
}

首先输出”hello”,然后停顿3秒,最后输出”the world”——正如我们想象的那样。ngx.flush执行显示的输出,前一个ngx.say被“阻塞”住,执行完输出后方往下执行。

再看一个例子:

server {
    listen 80;
    lua_code_cache off;
    location /test {
        content_by_lua_block {
            ngx.say(string.rep("hello", 4000))
            ngx.sleep(3)
            ngx.say("the world")
        }
    }
}

这个例子和第一个例子相比,唯一不同就是ngx.say输出内容长了不少,我们发现浏览器先收到所有的hello,接着又收到了”the world” 。然而如果我们把4000改为小一点的值如2000(不同配置这个相对大小或有不同),那么仍然会出现先停顿3s,然后所有”hello”连同最后”the world”一起输出的情况。

通过以上三个例子,我们可以得出下面的结论:

ngx.say和ngx.print的同步和异步

  • nginx有个输出缓冲(system send buffer),如16k。ngx.say和ngx.print默认是向这个输出缓冲写入数据,如果没有显示的调用ngx.flush,那么在content阶段结束后输出缓冲会写入客户端;
  • 如果没有ngx.flush也没有到结束阶段,但如果输出缓冲区满了,那么也会输出到客户端;

因此ngx.say和ngx.print的默认向客户端的输出都是异步的,非实时性的,改变这一行为的是ngx.flush,可以做到同步和实时输出。这在流式输出,比如下载大文件时非常有用。

ngx.flush的同步和异步

lua-nginx也提到了ngx.flush的同步和异步。某一个ngx.say或者ngx.print调用后,这部分输出内容会写到输出缓冲区,同步的方式ngx.flush(true)会等到内容全部写到缓冲区再输出到客户端,而异步的方式ngx.flush()会将内容一边写到缓冲区,而缓冲区则一边将这些内容输出到客户端。

openresty和nginx流式输出的比较

流式输出,或者大文件的下载,nginx的upstream模块已经做得非常好,可以通过proxy_buffering|proxy_buffer_size|proxy_buffers 等指令精细调控,而且这些指令的默认值已经做了妥善处理。我们来看看这些指令以及默认值:

proxy_buffering on;
proxy_buffer_size 4k|8k; 
proxy_buffers 8 4k|8k; 
proxy_busy_buffers_size 8k|16k;
proxy_temp_path proxy_temp;
  • proxy_buffering on表示内存做整体缓冲,内存不够时多余的存在由proxy_temp_path指定的临时文件中,off表示每次从上游接收proxy_buffer_size响应的内容然后直接输出给客户端,不会试图缓冲整个响应
  • proxy_buffer_size和proxy_buffers都是指定内存缓冲区的大小,proxy_buffer_size通常缓冲响应头,proxy_buffers缓冲响应内容,默认为一页的大小,proxy_buffers还可以指定这样的缓冲区的个数
  • proxy_busy_buffers_size nginx在试图缓冲整个响应过程中,可以让缓冲区proxy_busy_buffers_size大小的已经写满的部分先行发送给客户端。于此同时,缓冲区的另外部分可以继续读。如果内存缓冲区不够用了,还可以写在文件缓冲区
  • proxy_temp_path 使用文件作为接受上游请求的缓冲区buffer,当内存缓冲区不够用时启用

openresty的怎么做到过大响应的输出呢? (https://moonbingbing.gitbooks.io/openresty-best-practices/content/index.html) 提到了两种情况:

  • 输出内容本身体积很大,例如超过 2G 的文件下载
  • 输出内容本身是由各种碎片拼凑的,碎片数量庞大

前面一种情况非常常见,后面一种情况比如上游已经开启Chunked的传输方式,而且每片chunk非常小。笔者就遇到了一个上游服务器通过Chunked分片传输日志,而为了节省上游服务器的内存将每片设置为一行日志,一般也就几百字节,这就太“碎片”了,一般日志总在几十到几百M,这么算下来chunk数量多大10w+。笔者用了resty.http来实现文件的下载,文件总大小48M左右。

local http = require "resty.http"
local httpc = http.new()

httpc:set_timeout(6000)
httpc:connect(host, port)

local client_body_reader, err = httpc:get_client_body_reader()

local res, err = httpc:request({
    version = 1.1,
    method = ngx.var.request_method,
    path = ngx.var.app_uri,
    headers = headers,
    query = ngx.var.args,
    body = client_body_reader
})

if not res then
    ngx.say("Failed to request ".. ngx.var.app_name .." server: ", err)
    return
end

-- Response status
ngx.status = res.status

-- Response headers
for k, v in pairs(res.headers) do
    if k ~= "Transfer-Encoding" then  --必须删除上游Transfer-Encoding响应头
        ngx.header[k] = v
    end
end

-- Response body
local reader = res.body_reader
repeat
    local chunk, err = reader(8192)
    if err then
        ngx.log(ngx.ERR, err)
        break
    end

    if chunk then
        ngx.print(chunk)
        ngx.flush(true)  -- 开启ngx.flush,实时输出
    end
until not chunk

local ok, err = httpc:set_keepalive()
if not ok then
    ngx.say("Failed to set keepalive: ", err)
    return
end

多达10w+的”碎片”的频繁的调用ngx.pirnt()和ngx.flush(true),使得CPU不堪重负,出现了以下的问题:

  • CPU轻轻松松冲到到100%,并保持在80%以上
  • 由于CPU的高负荷,实际的下载速率受到显著的影响
  • 并发下载及其缓慢。笔者开启到第三个下载连接时基本就没有反应了

这是开启了ngx.flush(true)的情况(ngx.flush()时差别不大),如果不开启flush同步模式,则情况会更糟糕。CPU几乎一直维持在100%左右:

可见,在碎片极多的流式传输上,以上官方所推荐的openresty使用方法效果也不佳。

于是,回到nginx的upstream模块,改content_by_lua_file为proxy_pass再做测试,典型的资源使用情况为:

无论是CPU还是内存占用都非常低,开启多个下载链接后并无显著提升,偶尔串升到30%但迅速下降到不超过10%。

因此结论是,涉及到大输出或者碎片化响应的情况,最好还是采用nginx自带的upstream方式,简单方便,精确控制。而openresty提供的几种方式,无论是异步的ngx.say/ngx.print还是同步的ngx.flush,实现效果都不理想。

标签:Openresty 发布于:2019-10-18 17:24:27