戰(zhàn):從編譯到接入OpenResty全指南)
1. 需求場(chǎng)景為什么要在寶塔 Nginx 里用 lua-cjson如果你的業(yè)務(wù)里已經(jīng)有 Lua 腳本在跑或者你正在用 OpenResty 做接口網(wǎng)關(guān)那“JSON 編解碼”就是一個(gè)繞不開的基本功。很多流量處理邏輯需要在請(qǐng)求階段把 body 讀出來(lái)、轉(zhuǎn)成 table、做字段判斷再重新拼 JSON 返回這一套動(dòng)作如果沒有 cjson幾乎沒法體面地完成。最近我在寶塔 Nginx 環(huán)境里折騰了一遍 lua-cjson 的安裝與配置中間踩了幾個(gè)坑把完整過(guò)程記錄下來(lái)希望能幫你少走兩小時(shí)彎路。場(chǎng)景大致分兩類一類是直接裝寶塔自帶的 OpenResty它的 lualib 里其實(shí)已經(jīng)帶了 cjson可以 require 直接用另一類是堅(jiān)持用寶塔面板里的原版 Nginx又想跑 Lua 腳本這種情況下除了要編譯 Lua 模塊還要手動(dòng)把 cjson 編譯好、塞到正確位置。本文會(huì)把兩條線都講清楚重點(diǎn)放在編譯安裝和接入配置上。適合剛接觸 OpenResty-Lua 的運(yùn)維也適合被 cjson 加載報(bào)錯(cuò)折磨過(guò)幾次的老手。2. 方案選型先搞清楚自己的 Nginx 到底有沒有 Lua 環(huán)境2.1 一分鐘確認(rèn)當(dāng)前 Nginx 版本和編譯參數(shù)很多人在安裝 lua-cjson 時(shí)遇到的第一道坎并不是編譯失敗而是“裝完了依然不能用”。原因往往是當(dāng)前 Nginx 根本沒有 Lua 環(huán)境。這里先教你一個(gè)最直接的確認(rèn)方法。在 SSH 終端執(zhí)行/www/server/nginx/sbin/nginx -V如果輸出里出現(xiàn)了openresty相關(guān)的路徑或者出現(xiàn)了--add-module.../lua-nginx-module之類的編譯參數(shù)說(shuō)明 Lua 環(huán)境已經(jīng)就緒。如果輸出里只有標(biāo)準(zhǔn) nginx 編譯參數(shù)沒有任何 lua 相關(guān)模塊那基本可以判斷當(dāng)前 Nginx 并不支持content_by_lua_block后面即使把 cjson.so 放進(jìn)去也白搭。我?guī)团笥雅挪檫^(guò)一次類似問(wèn)題他花了一晚上把 cjson 編譯完、復(fù)制到 lualib然后寫了測(cè)試 location結(jié)果nginx -t直接報(bào)unknown directive content_by_lua_block。問(wèn)題根本不在 cjson而是 Nginx 主體根本沒編 Lua 模塊。所以第一步永遠(yuǎn)是確認(rèn)編譯參數(shù)不要急著編譯庫(kù)。2.2 三條技術(shù)路線怎么選路線環(huán)境cjson 是否可用維護(hù)成本路線 A寶塔 OpenResty 環(huán)境自帶require cjson 直接可用最低路線 B寶塔原版 Nginx 手動(dòng)編譯 Lua 模塊需要手動(dòng)編譯 cjson 并配置 cpah較高路線 C自行編譯 Nginx 或純 OpenResty 部署自帶或手動(dòng)編譯取決于自身維護(hù)習(xí)慣如果只是為了跑業(yè)務(wù)腳本我強(qiáng)烈建議路線 A一鍵裝上 OpenResty自帶 LuaJIT自帶 cjson省掉一大堆兼容性工作。但為什么還要寫這篇文章因?yàn)楝F(xiàn)實(shí)中經(jīng)常遇到兩類訴求第一類業(yè)務(wù)上線在即寶塔面板里已經(jīng)用了原版 Nginx不愿意整體切換第二類OpenResty 自帶的 cjson 版本需要升級(jí)或者項(xiàng)目希望所有服務(wù)器統(tǒng)一使用同一份編譯產(chǎn)物。這兩類訴求都繞不開手動(dòng)編譯。我的建議是新環(huán)境無(wú)腦選 OpenResty老環(huán)境能不切換就別切換但要把 lua-cjson 的手動(dòng)編譯能力掌握在手因?yàn)榭傆杏玫降囊惶臁?.3 手動(dòng)編譯 lua-cjson 到底在解決什么問(wèn)題cjson 是 C 語(yǔ)言寫的 Lua 擴(kuò)展編譯過(guò)程本質(zhì)上是把頭文件和 Lua 運(yùn)行時(shí)的鏈接信息對(duì)齊。Lua 5.1 指令集有多個(gè)實(shí)現(xiàn)最常見的是標(biāo)準(zhǔn) Lua 5.1 和 LuaJIT 2.x。OpenResty 使用的是 LuaJIT 2.x它對(duì)外暴露的 include 目錄通常是luajit-2.1而 cjson 源碼包里的 Makefile 默認(rèn)寫的是/usr/local/include/luajit-2.0。如果不改路徑編譯時(shí)只會(huì)找標(biāo)準(zhǔn) Lua 頭文件甚至頭文件都不存在然后直接報(bào)錯(cuò)。理解了這一點(diǎn)后面的編譯步驟本質(zhì)上就是四個(gè)字對(duì)齊路徑。只要你把自己環(huán)境里的 LuaJIT 頭文件路徑給對(duì)一切都會(huì)順利。這也是為什么網(wǎng)上教程里的命令不能直接照搬——每個(gè)人機(jī)器上的路徑都不一樣。3. 實(shí)操完整編譯安裝并接入 OpenResty/Nginx3.1 安裝或切換到 OpenResty 并拿到 LuaJIT 路徑如果還沒有 OpenResty在寶塔軟件商店里把 Nginx 換成 OpenResty 版本。安裝時(shí)間視機(jī)器情況而定可能需要幾分鐘到二三十分鐘耐心等待。安裝完成后確認(rèn)目錄/www/server/openresty/nginx/sbin/nginx -V看輸出里是否有--with-luajit之類的參數(shù)即可。寶塔的 OpenResty 一般安裝在/www/server/openresty但也不排除你在安裝時(shí)自定義了目錄所以實(shí)際操作時(shí)以nginx -V顯示的內(nèi)容為準(zhǔn)。接下來(lái)找到 LuaJIT 的 include 路徑通常是/www/server/openresty/luajit/include/luajit-2.1如果目錄名帶版本號(hào)后綴就以實(shí)際版本為準(zhǔn)。這一步的目的是拿到編譯 cjson 需要的頭文件路徑也就是后續(xù)要填進(jìn) Makefile 的那個(gè)關(guān)鍵路徑。3.2 下載并解壓 lua-cjson 源碼推薦直接使用 2.1.0 版本這是目前最穩(wěn)定、使用最廣的 release。操作如下cd /opt wget https://github.com/mpx/lua-cjson/archive/refs/tags/2.1.0.tar.gz tar -xzf 2.1.0.tar.gz cd lua-cjson-2.1.0下載后先打開 Makefile 看一眼內(nèi)容非常清晰LUA_INCLUDE_DIR /usr/local/include/luajit-2.0 LUA_MODULE_INSTALL_DIR /usr/local/lib/lua/5.1 LUA_VERSION 5.1這三個(gè)變量里L(fēng)UA_INCLUDE_DIR是編譯時(shí)的頭文件路徑LUA_MODULE_INSTALL_DIR是make install時(shí)的安裝目錄LUA_VERSION保持 5.1 即可。其他都不用動(dòng)。3.3 修改 Makefile 并完成編譯安裝路徑替換我用 sed 完成簡(jiǎn)單高效sed -i s#/usr/local/include/luajit-2.0#/www/server/openresty/luajit/include/luajit-2.1#g Makefile改完檢查一遍確認(rèn)替換沒有失誤grep LUA_INCLUDE_DIR Makefile然后執(zhí)行編譯和安裝make make install如果路徑對(duì)了編譯過(guò)程會(huì)非常快生成物是cjson.so。make install默認(rèn)會(huì)把 so 裝到/usr/local/lib/lua/5.1/。注意這個(gè)路徑不受 Nginx 控制所以后面還需要手動(dòng)復(fù)制到 OpenResty 的 lualib 目錄。很多人卡在這一步原因就是make install之后直接重啟 Nginx卻發(fā)現(xiàn) require 不到。因?yàn)?OpenResty 根本不會(huì)去/usr/local/lib找模塊它只會(huì)在自己的 lualib 和配置的 cpath 里找。3.4 復(fù)制 cjson.so 到加載路徑并配置 lua_package_cpath首選方案是放到 OpenResty 自帶的模塊目錄cp /usr/local/lib/lua/5.1/cjson.so /www/server/openresty/lualib/cjson.so chmod 755 /www/server/openresty/lualib/cjson.so我在生產(chǎn)環(huán)境更習(xí)慣把第三方 so 放到獨(dú)立目錄例如/www/server/lua_modules/原因后面講寶塔升級(jí)時(shí)會(huì)專門說(shuō)明。第一次跑通直接放 lualib 最省事。接著在 Nginx 全局配置的http{}塊里加兩行l(wèi)ua_package_path /www/server/openresty/lualib/?.lua;;; lua_package_cpath /www/server/openresty/lualib/?.so;;;如果你放到了獨(dú)立目錄就改成對(duì)應(yīng)的路徑。這里有個(gè)細(xì)節(jié)很多人會(huì)忽略末尾的;;表示保留默認(rèn)搜索路徑千萬(wàn)不要去掉。默認(rèn)路徑里可能還有 OpenResty 自帶的模塊去掉后會(huì)導(dǎo)致其他 Lua 模塊加載異常。3.5 寫一個(gè)最小測(cè)試接口驗(yàn)證完成上述配置后在某個(gè)站點(diǎn)的 server 塊里加一個(gè)測(cè)試 locationlocation /__cjson_test { default_type application/json; content_by_lua_block { local cjson require cjson local data { ok true, name hello, num 42 } ngx.say(cjson.encode(data)) } }保存后依次執(zhí)行/www/server/nginx/sbin/nginx -t /www/server/nginx/sbin/nginx -s reload然后 curl 驗(yàn)證curl http://127.0.0.1/__cjson_test正常會(huì)返回{ok:true,name:hello,num:42}。到這里lua-cjson 就已經(jīng)接入成功了。4. 業(yè)務(wù)接入encode/decode 實(shí)測(cè)與容易被忽略的細(xì)節(jié)4.1 encode 與 decode 的正確用法cjson 最常用的兩個(gè)函數(shù)是 encode 和 decode。encode 把 Lua table 轉(zhuǎn)成 JSON 字符串decode 把 JSON 字符串轉(zhuǎn)成 Lua table。代碼層面非常簡(jiǎn)單local cjson require cjson local json_str cjson.encode({ key value }) local ok, data pcall(cjson.decode, json_str)decode 遇到非法 JSON 會(huì)拋異常所以生產(chǎn)環(huán)境建議使用 pcall 包一層或者直接用cjson.safe這個(gè)安全接口。cjson.safe.decode在解析失敗時(shí)返回 nil 和錯(cuò)誤信息不會(huì)拋異常適合放在請(qǐng)求處理鏈路里。另外有一個(gè)非常容易被忽略的細(xì)節(jié)默認(rèn) decode 會(huì)把 JSON 里的數(shù)字轉(zhuǎn)成 Lua number如果數(shù)字超過(guò) 2^53會(huì)損失精度。比如訂單號(hào)、流水號(hào)這類 16 位以上的大整數(shù)JSON 解析出來(lái)可能與原文不一致。解決方法是開啟cjson.decode_keep_number_strings(true)加上這一行之后數(shù)字會(huì)保留原始字符串形態(tài)業(yè)務(wù)做字符串比對(duì)就不會(huì)出錯(cuò)。這個(gè)開關(guān)適合所有涉及透?jìng)鞔笳麛?shù) ID 的場(chǎng)景做數(shù)據(jù)對(duì)接時(shí)非常關(guān)鍵。4.2 在真實(shí)請(qǐng)求里解析 body 并做分流舉一個(gè)典型場(chǎng)景某個(gè)回調(diào)接口需要根據(jù) body 里的type字段決定走到哪個(gè)上游服務(wù)。參考代碼如下location /webhook { content_by_lua_block { local cjson require cjson ngx.req.read_body() local body ngx.req.get_body_data() if not body then ngx.exit(400) end local ok, data pcall(cjson.decode, body) if not ok or type(data) ~ table then ngx.exit(400) end if data.type order then ngx.exec(order_backend) else ngx.exec(other_backend) end } }這段只是思路示例具體業(yè)務(wù)里會(huì)涉及更復(fù)雜的參數(shù)校驗(yàn)、簽名、超時(shí)處理但核心就是 cjson.decode 之后對(duì) table 進(jìn)行操作。如果請(qǐng)求體超過(guò) Nginx 默認(rèn)的client_max_body_size要先調(diào)大體積限制否則get_body_data()可能拿不到內(nèi)容。4.3 性能與穩(wěn)定性方面的實(shí)操經(jīng)驗(yàn)LuaJIT 下 cjson 的編解碼性能很高但依然有幾個(gè)錦囊盡量復(fù)用局部變量避免在每次請(qǐng)求里創(chuàng)建超大 tableencode 長(zhǎng)內(nèi)容時(shí)注意cjson.encode_max_depth深嵌套結(jié)構(gòu)默認(rèn)上限較高但嵌套過(guò)深會(huì)直接報(bào)錯(cuò)對(duì)不可信的用戶輸入decode 前先判斷長(zhǎng)度避免超大 JSON 占滿內(nèi)存把 cjson.encode 的結(jié)果直接通過(guò) ngx.say 輸出不要先拼字符串再輸出省一次內(nèi)存拷貝這些屬于常規(guī)經(jīng)驗(yàn)寫代碼時(shí)多留個(gè)心眼線上就能少報(bào)幾個(gè) 500。5. 常見問(wèn)題與排查技巧實(shí)錄5.1 報(bào)錯(cuò)速查表現(xiàn)象常見原因解決方法module cjson not foundcjson.so 沒復(fù)制到 cpath 目錄確認(rèn) lua_package_cpath 路徑并復(fù)制 sounknown directive content_by_lua_blockNginx 未編譯 Lua 模塊換成 OpenResty 或重新編譯 Nginx編譯時(shí)報(bào) luajit.h 找不到LuaJIT include 路徑?jīng)]改對(duì)檢查 Makefile 中的 LUA_INCLUDE_DIRdecode 中文亂碼Lua 腳本文件編碼不對(duì)保證 Lua 文件保存為 UTF-8 無(wú) BOM大整數(shù)精度丟失默認(rèn) number 精度限制開啟 decode_keep_number_stringsreload 后沒有生效只改了配置沒測(cè)接口reload 后用 curl 實(shí)際請(qǐng)求并查 error.log5.2 從一條真實(shí)報(bào)錯(cuò)開始排查把典型報(bào)錯(cuò)記錄拿來(lái)說(shuō)2025/01/01 10:00:00 [error] 12345#0: *678 lua entry thread aborted: runtime error: ... module cjson not found遇到這類錯(cuò)誤原因基本集中在三個(gè)點(diǎn)cjson.so 文件位置不對(duì)lua_package_cpath 沒有生效文件存在但權(quán)限不足或依賴庫(kù)缺失。逐個(gè)排除。先看 Nginx 配置里是否有l(wèi)ua_package_cpath /www/server/openresty/lualib/?.so;;;再看目錄里有沒有 cjson.so。如果兩樣都滿足用 ldd 檢查依賴ldd /www/server/openresty/lualib/cjson.so如果關(guān)聯(lián)的庫(kù)都能找到基本排除了依賴問(wèn)題。再看文件權(quán)限ls -l /www/server/openresty/lualib/cjson.soNginx worker 進(jìn)程通常以 www 用戶運(yùn)行so 文件至少要是 644 權(quán)限。之前遇到過(guò)一種情況文件明明存在權(quán)限也沒問(wèn)題但加載一直失敗最后發(fā)現(xiàn)是復(fù)制文件時(shí) SELinux 上下文不對(duì)執(zhí)行chcon -t lib_t解決。如果沒開 SELinux基本就是上述三個(gè)原因。5.3 幾個(gè)容易踩的坑第一個(gè)坑OpenResty 自帶的 cjson 路徑在不同版本里不一致。很多網(wǎng)上教程寫的是/usr/local/openresty/lualib/cjson.so但寶塔環(huán)境并不相同。判斷方法很簡(jiǎn)單在 Nginx 配置里臨時(shí)打印package.path和package.cpath或者直接看 error log 里的搜索路徑。第二個(gè)坑make install裝到了/usr/local/lib但 OpenResty 查找 so 走的是自己的 cpath不會(huì)去/usr/local/lib找。所以復(fù)制那步一定不能省。我見過(guò)有人直接 make install 后重啟 Nginxrequire 一直 not found就是沒復(fù)制。第三個(gè)坑修改 Nginx 配置后只 reload不檢查 error log。有些錯(cuò)誤在 reload 時(shí)不明顯真正收到請(qǐng)求時(shí)才暴露。建議每次改動(dòng)配置后用 curl 打一次測(cè)試接口同時(shí)tail -f /www/server/nginx/logs/error.log實(shí)時(shí)看日志發(fā)現(xiàn)問(wèn)題能立刻定位。6. 后續(xù)維護(hù)與更換版本的經(jīng)驗(yàn)6.1 把第三方 so 放到獨(dú)立目錄更穩(wěn)妥前面提到推薦把 cjson.so 放到/www/server/lua_modules/這類獨(dú)立目錄而不是 OpenResty 自帶的 lualib。原因是寶塔通過(guò)軟件商店升級(jí) OpenResty 時(shí)很可能把 lualib 一起覆蓋或內(nèi)部路徑發(fā)生變化。升級(jí)后自帶 cjson 版本可能改變?nèi)绻阍偈謩?dòng) require 自己的舊 so容易出現(xiàn)兼容性問(wèn)題。獨(dú)立目錄操作如下mkdir -p /www/server/lua_modules cp /usr/local/lib/lua/5.1/cjson.so /www/server/lua_modules/cjson.so chown www:www /www/server/lua_modules/cjson.so配置改成lua_package_cpath /www/server/lua_modules/?.so;;;這樣升級(jí) OpenResty、Nginx 都不影響這個(gè)文件路徑穩(wěn)定維護(hù)省心。6.2 其他 JSON 庫(kù)要不要換Lua 生態(tài)里 JSON 編解碼方案還有純 Lua 寫的 json.lua、以及 lua-resty-jwt 內(nèi)置的 JSON 處理。簡(jiǎn)單對(duì)比lua-cjsonC 語(yǔ)言實(shí)現(xiàn)性能高使用最廣坑最少推薦純 Lua 實(shí)現(xiàn)便于改造但性能差不適合高并發(fā)resty.jwt 內(nèi)置 JSON適合已經(jīng)接入 JWT 的項(xiàng)目復(fù)用減少依賴如果只做網(wǎng)關(guān)層輕量解析lua-cjson 完全夠用如果項(xiàng)目里已經(jīng)把 resty.jwt 接進(jìn)來(lái)那內(nèi)部已有 JSON 依賴可以共用不必再安裝第二個(gè)庫(kù)少一處維護(hù)點(diǎn)。6.3 換版本或升級(jí) LuaJIT 時(shí)的注意點(diǎn)以后遇到 OpenResty 升級(jí)或換 LuaJIT 版本真正需要關(guān)心的是 LuaJIT include 路徑是否變化。升級(jí)后重新編譯 cjson 是重新對(duì)齊路徑最快的方式。另外把make clean放在重新make之前否則舊的 object 文件可能參與編譯出現(xiàn)奇怪的版本混淆。make clean如果 clean 目標(biāo)不存在隨手執(zhí)行rm -f *.o也不會(huì)有損失。按照我的個(gè)人經(jīng)驗(yàn)生產(chǎn)環(huán)境最安心的配置是獨(dú)立目錄 固定版本 編譯記錄。把版本號(hào)和編譯日期寫入部署腳本的注釋里so 文件用md5sum記錄指紋。這樣一旦線上出現(xiàn)問(wèn)題可以快速判斷文件是否被意外替換。如果只是在寶塔下做常規(guī)部署第一選擇永遠(yuǎn)是寶塔 OpenResty 版本自帶 cjson 能解決百分之九十的 JSON 場(chǎng)景只有當(dāng)版本太舊或需要定制時(shí)才按照上面的步驟手動(dòng)編譯。整個(gè)流程最核心的只有三個(gè)點(diǎn)LuaJIT include 路徑對(duì)齊、cjson.so 放到 Nginx 的 cpath 搜索范圍內(nèi)、配置后 reload。只要這三步不出錯(cuò)剩下的都是小事。遇到cjson not found先別急著重新編譯多看一眼 error log基本五分鐘內(nèi)能定位。