1. 程式人生 > >Openresty的同步輸出與流式響應

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,實現效果都不理想。