如何應對 OpenResty 為支援 ARM64 引入的 break change
本文不是關於新版 OpenResty 如何支援 ARM64 的,而是關於如何應對這一過程引入的 break change。
另外,如果你沒用 OpenResty 自己的 LuaJIT 分支,那麼可以直接關掉這個頁面了,因為這些 break change 只有在使用了 OpenResty 自己的 LuaJIT 分支才會出現。
一切的根源在於,新版本的 OpenResty 把當前請求的ngx_http_request_t
放到了luaState
的exdata
屬性裡面,不再使用getfenv(0).__ngx_req
這種方式了。exdata
是 OpenResty 自己的 LuaJIT 分支加的屬性,所以如果不用 OpenResty 自己的 LuaJIT 分支,依舊還是得走getfenv(0).__ngx_req
這種方式。同樣被移除的還有getfenv(0).__ngx_cycle
和給每個 main thread 準備的全域性環境表。接下來我們談談如何應對這幾個變化。
getfenv(0).__ngx_req
新版 OpenResty 使用resty.core.base
裡面的get_request()
代替了getfenv(0).__ngx_req
。在你的程式碼裡,可以這麼寫:
local base = require "resty.core.base" local get_request = base.get_request if not get_request then get_request = function() return getfenv(0).__ngx_req end end ... local r = get_request()
但是!get_request()
並非 100% 相容getfenv(0).__ngx_req
。前者返回的是一個cdata,而後者返回的是一個 lightuserdata。cdata 和 lightuserdata 在語義上有些微妙的不同。當你用 lightuserdata 作為 table 的 hash key 時,如果 lightuserdata 指向的地址相同,那麼 hash 值會相同。但如果是 cdata,即使是指向同一地址的指標型別的 cdata,由於計算 hash 時用的是 cdata 的地址,而非其內部的值,所以不同的 cdata 的 hash 值會不一樣。舉個例子:
local r = get_request() local h = {} h[r] = 1 ngx.say(h[get_request()])
在之前的版本里,兩次get_request()
會返回同一個地址(都是同一個請求嘛),所以會輸出 1。而新的 OpenResty 裡,你會發現輸出結果是 nil。這是因為兩次get_request()
會創造兩個 cdata 物件,這兩個物件雖然值一樣,但是記憶體地址不一樣,所以 hash 值不一樣。
那怎麼解決呢?我們可以實現一個轉換函式,把 cdata 的值變成某種可用作 hash key 的型別。一個簡單的解決方法是加上 tostring。tostring(cdata)
的輸出中會包含 cdata 指向的地址,這樣同一個請求對應的 key 就會相同。
考慮到 LuaJIT 建立字串的開銷比較大,作為一種優化手段,在某些架構下我們可以用tonumber(ffi_cast("intptr_t", cdata))
代替。之所以限定在某些架構,是因為 LuaJIT 的 Number 其實是 double,而不是 int64,所以對於某些 64 位的架構,不一定能得到正確的輸出。好在 x64 的使用者態空間地址不會超過 48 位,所以我們可以在主流的 x64 伺服器上採用該優化。當然前提是你沒有啟用 5 級頁表。考慮到只有數百 TB 記憶體的機器才會有開啟 5 級頁表的需要,大體上你可以放心地認為你的 x64 環境不會遇到這樣的問題。即使開啟了 5 級頁表,現階段 48 位以上的記憶體地址也不是預設可用的。關於 5 級頁表的更多上下文,可以看下這兩個連結:
getfenv(0).__ngx_cycle
有些 Lua 程式碼會通過getfenv(0).__ngx_cycle
獲取ngx_cycle
, 然後通過 FFI 呼叫傳給 C 函式。其實直接在 C 函式裡面訪問ngx_cycle
就可以了,不需要經過 Lua 這一層。
你可能會問,reload 的時候,init
階段下ngx_cylce
應該會指向舊的ngx_cycle
吧?這裡 OpenResty 做了點手腳。它會把舊的ngx_cycle
放到saved_ngx_cycle
裡面來,讓ngx_cycle
指向新構建的ngx_cycle_t *cycle
。所以並不需要特殊的對待。
每個 main thread 準備的全域性環境表
為了放下getfenv(0).__ngx_req
,過去的 OpenResty 需要給每個 main thread 準備獨立的全域性環境表,這樣每個請求的getfenv(0)
才會返回不同的 table。既然新的 OpenResty 已經不需要getfenv(0).__ngx_req
,這些全域性環境表就能幹掉了。
不過讓它們下崗,還有點副作用。過去在rewrite
/access
/content
等階段裡定義了全域性變數(通常是手誤引入的),不會汙染到其他的請求。那是因為 OpenResty 設定了全域性環境表,這些全域性變數只會影響到它們所在的全域性環境表。但是移除了全域性環境表的保護後,這些全域性變數就能肆無忌憚地跑來跑去。為此新版 OpenResty 加了個 guard,如果在這些階段裡遇到全域性變數的定義,會列印這樣的錯誤資訊:
2019/04/30 11:01:18 [warn] 26843#26843: *240 [lua] _G write guard:12: __newindex(): writing a global lua variable ('xxx') which may lead to race conditions between concurrent requests, so prefer the use of 'local' variables stack traceback: ...
這種錯誤資訊對程式的流程沒有影響,但對效能有影響。解決辦法?把全域性變數一個個都揪出來解決掉。
當然如果你是在init
或init_worker
階段定義全域性變數,並不會觸發這個 guard。畢竟這麼做的人一般是故意的,在過去的 OpenResty 裡這也是標準的“使用”全域性變數的方式。雖然我個人不推薦這麼做。用全域性變數,遲早都要還的。