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,实现效果都不理想。
原文出处:cnblogs -> https://www.cnblogs.com/minirice/p/9951320.html