Openresty的同步輸出與流式響應
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_buffers都是指定記憶體緩衝區的大小,預設為一頁的大小,proxy_buffers還可以指定這樣的緩衝區的個數
- proxy_busy_buffers_size 這個"busy"看得出,這個指令一定是用在比較繁忙的時候了。在比較繁忙的時候(高併發或者大檔案下載)時,就沒有必要等到上游響應全部來了再發給客戶端,可以來了一部分(proxy_busy_buffers_size)就發過去。於此同時,緩衝區的另外部分可以繼續讀。如果記憶體緩衝區不夠用了,還可以開啟檔案緩衝區
- proxy_temp_path 使用檔案作為接受上游請求的緩衝區buffer,當記憶體不夠用時啟用
openresty的怎麼做到過大響應的輸出呢? 《OpenResty 最佳實踐》 提到了兩種情況:
- 輸出內容本身體積很大,例如超過 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,實現效果都不理想。