使用 ngx_lua (openresty)正確讀取 HTTP 請求 body
之前用 ngx_lua(openresty) 寫了一個處理 HTTP 請求的程式,今天發現當傳送的 HTTP 請求 body 很大的時候,發現老是報錯,最後定位到ngx.req.get_body_data()
這個函式返回 nil ,而不是真正的 body。
於是我去 ngx_lua 的文件看ofollow,noindex" target="_blank">這個函式的文件 ,發現文件中說有三種情況下,這個函式的返回值會是 nil:
This function returns nil if
- the request body has not been read,
- the request body has been read into disk temporary files,
- or the request body has zero size.
第一條,我是讀了文件的,知道要 Nginx 預設是不會讀 body 的,要麼開啟讀 body 的開關,要麼顯示呼叫一下 read。所以程式碼中已經呼叫了ngx.req.read_body()
了,不會是這個原因。
第三條,也不可能,我通過 curl 傳送的,body 肯定是發出去了(可以抓包驗證)。
那麼基本上就確定是這個 request body 被讀到硬碟的臨時檔案裡了。看到這裡我猜應該是 Nginx 是將大的 HTTP 請求放到磁碟中而不是放到記憶體中。搜了一下文件 ,發現有這個引數:
Syntax:client_body_buffer_size size; Default: client_body_buffer_size 8k|16k; Context:http, server, location
當請求體的大小大禹 client_body_buffer_size 的時候,Nginx 將會把它存到一個臨時檔案中,而不是放到記憶體中。這個值的大小預設是記憶體頁的兩倍:32 位系統上是 8k,64位系統上是16k。
OK,問題找到了,現在解決方案有兩個:1)調大這個值,我覺得是不合理的,這樣會浪費記憶體。2)可以想辦法讀到臨時檔案中的大 body。ngx_lua 提供了配套的方法ngx_req.get_body_file ,注意這只是獲得檔案,還要在 lua 程式碼中開啟讀取檔案。
所以最後,處理一個請求且還能正確處理很大 body 的請求的程式碼是這樣的,其中高亮的部分,是核心的邏輯,先嚐試從記憶體中讀 body,如果讀不到,就去臨時檔案中讀。
location /request_body { content_by_lua ' function read_from_file(file_name) local f = assert(io.open(file_name, "r")) local string = f:read("*all") f:close() return string end ngx.req.read_body() local body_str = ngx.req.get_body_data() if nil == body_str then local body_file = ngx.req.get_body_file() if body_file then body_str = read_from_file(body_file) end end ngx.say(body_str) '; }
我讀這個實際的應用場景是,讀出 body,按照 json 解析出來當做路由規則(需求是需要動態設定 HTTP 的路由規則,所以我用 ngx_lua 在 nginx 上新開了個埠監控這樣的規則)。現在用很大的規則一測試,發現效能下降很厲害。存到檔案、再讀出檔案是一方面。另一方面是 ngx_lua 是為每一個 HTTP 請求開一個 lua 協程處理,不能共享變數,只能通過lua_shared_dict 來儲存持久的變數。但是 lua_shared_dict 的問題是,這個 dict 只支援 “Lua booleans, numbers, strings, or nil”,解析出來的 json 是一個 lua 的 table,不能儲存到 lua_shared_dict 裡面,我只好儲存一個字串,然後對每一個 HTTP 請求 json decode 這個字串了。不知道有沒有更好的方法。