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_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的怎么做到過大響應的輸出呢? 《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,實現效果都不理想。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM