# 熱裝載代碼
在Openresty中,提及熱加載代碼,估計大家的第一反應是[lua_code_cache](http://wiki.nginx.org/HttpLuaModule#lua_code_cache)這個開關。在開發階段我們把它配置成lua_code_cache off,是很方便、有必要的,修改完代碼,肯定都希望自動加載最新的代碼(否則我們就要噩夢般的reload服務,然后再測試腳本)。
禁用 Lua 代碼緩存(即配置 lua_code_cache off)只是為了開發便利,一般不應以高于 1 并發來訪問,否則可能會有race condition等等問題。同時因為它會有帶來嚴重的性能衰退,所以不應在生產上使用此種模式。生產上應當總是啟用Lua代碼緩存,即配置lua_code_cache on。
那么我們是否可以在生產環境中完成熱加載呢?
- 代碼有變動時,自動加載最新lua代碼,但是nginx本身,不做任何reload
- 自動加載后的代碼,享用lua_code_cache on帶來的高效特性
這里有多種玩法([引自Openresty討論組](https://groups.google.com/forum/#!searchin/openresty/package.loaded/openresty/-MZ9AzXaaG8/TeXTyLCuoYUJ)):
- 使用 HUP reload 或者 binary upgrade 方式動態加載 nginx 配置或重啟 nginx。這不會導致中間有請求被 drop 掉。
- 當 content_by_lua_file 里使用 nginx 變量時,是可以動態加載新的 Lua 腳本的,不過要記得對 nginx 變量的值進行基本的合法性驗證,以免被注入攻擊。
~~~
location ~ '^/lua/(\w+(?:\/\w+)*)$' {
content_by_lua_file $1;
}
~~~
- 自己從外部數據源(包括文件系統)加載 Lua 源碼或字節碼,然后使用 loadstring() “eval”進 Lua VM. 可以通過 package.loaded 自己來做緩存,畢竟頻繁地加載源碼和調用 loadstring(),以及頻繁地 JIT 編譯還是很昂貴的(類似 lua_code_cache off 的情形)。比如在 CloudFlare 我們從 modsecurity 規則編譯出來的 Lua 代碼就是通過 KyotoTycoon 動態分發到全球網絡中的每一個 nginx 服務器的。無需 reload 或者 binary upgrade.
### 自定義module的動態裝載
對于已經裝載的module,我們可以通過package.loaded.* = nil的方式卸載。
不過,值得提醒的是,因為 require 這個內建函數在標準 Lua 5.1 解釋器和 LuaJIT 2 中都被實現為 C 函數,所以你在自己的 loader 里可能并不能調用 ngx_lua 那些涉及非阻塞 IO 的 Lua 函數。因為這些 Lua 函數需要 yield 當前的 Lua 協程,而 yield 是無法跨越 Lua 調用棧上的 C 函數幀的。細節見
[https://github.com/openresty/lua-nginx-module#lua-coroutine-yieldingresuming](https://github.com/openresty/lua-nginx-module#lua-coroutine-yieldingresuming)
所以直接操縱 package.loaded 是最簡單和最有效的做法。我們在 CloudFlare 的 Lua WAF 系統中就是這么做的。
不過,值得提醒的是,從 package.loaded 解注冊的 Lua 模塊會被 GC 掉。而那些使用下列某一個或某幾個特性的 Lua 模塊是不能被安全的解注冊的:
- 使用 FFI 加載了外部動態庫
- 使用 FFI 定義了新的 C 類型
- 使用 FFI 定義了新的 C 函數原型
這個限制對于所有的 Lua 上下文都是適用的。
這樣的 Lua 模塊應避免手動從 package.loaded 卸載。當然,如果你永不手工卸載這樣的模塊,只是動態加載的話,倒也無所謂了。但在我們的 Lua WAF 的場景,已動態加載的一些 Lua 模塊還需要被熱替換掉(但不重新創建 Lua VM)。
### 自定義lua script的動態裝載實現
> [引自Openresty討論組](https://groups.google.com/forum/#!searchin/openresty/%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BDlua%E8%84%9A%E6%9C%AC/openresty/-MZ9AzXaaG8/TeXTyLCuoYUJ)
一方面使用自定義的環境表 [1],以白名單的形式提供用戶腳本能訪問的 API;另一方面,(只)為用戶腳本禁用 JIT 編譯,同時使用 Lua 的 debug hooks [2] 作腳本 CPU 超時保護(debug hooks 對于 JIT 編譯的代碼是不會執行的,主要是出于性能方面的考慮)。
下面這個小例子演示了這種玩法:
~~~
local user_script = [[
local a = 0
local rand = math.random
for i = 1, 200 do
a = a + rand(i)
end
ngx.say("hi")
]]
local function handle_timeout(typ)
return error("user script too hot")
end
local function handle_error(err)
return string.format("%s: %s", err or "", debug.traceback())
end
-- disable JIT in the user script to ensure debug hooks always work:
user_script = [[jit.off(true, true) ]] .. user_script
local f, err = loadstring(user_script, "=user script")
if not f then
ngx.say("ERROR: failed to load user script: ", err)
return
end
-- only enable math.*, and ngx.say in our sandbox:
local env = {
math = math,
ngx = { say = ngx.say },
jit = { off = jit.off },
}
setfenv(f, env)
local instruction_limit = 1000
debug.sethook(handle_timeout, "", instruction_limit)
local ok, err = xpcall(f, handle_error)
if not ok then
ngx.say("failed to run user script: ", err)
end
debug.sethook() -- turn off the hooks
~~~
這個例子中我們只允許用戶腳本調用 math 模塊的所有函數、ngx.say() 以及 jit.off(). 其中 jit.off()是必需引用的,為的是在用戶腳本內部禁用 JIT 編譯,否則我們注冊的 debug hooks 可能不會被調用。
另外,這個例子中我們設置了腳本最多只能執行 1000 條 VM 指令。你可以根據你自己的場景進行調整。
這里很重要的是,不能向用戶腳本暴露 pcall 和 xpcall 這兩個 Lua 指令,否則惡意用戶會利用它故意攔截掉我們在 debug hook 里為中斷腳本執行而拋出的 Lua 異常。
另外,require()、loadstring()、loadfile()、dofile()、io._、os._ 等等 API 是一定不能暴露給不被信任的 Lua 腳本的。
- 序
- Lua簡介
- Lua環境搭建
- 基礎數據類型
- 表達式
- 控制結構
- if/else
- while
- repeat
- 控制結構for的使用
- break,return
- Lua函數
- 函數的定義
- 函數的參數
- 函數的返回值
- 函數回調
- 模塊
- String庫
- Table庫
- 日期時間函數
- 數學庫函數
- 文件操作
- 元表
- 面向對象編程
- FFI
- LuaRestyRedisLibrary
- select+set_keepalive組合操作引起的數據讀寫錯誤
- redis接口的二次封裝(簡化建連、拆連等細節)
- redis接口的二次封裝(發布訂閱)
- pipeline壓縮請求數量
- script壓縮復雜請求
- LuaCjsonLibrary
- json解析的異常捕獲
- 稀疏數組
- 空table編碼為array還是object
- 跨平臺的庫選擇
- PostgresNginxModule
- 調用方式簡介
- 不支持事務
- 超時
- 健康監測
- SQL注入
- LuaNginxModule
- 執行階段概念
- 正確的記錄日志
- 熱裝載代碼
- 阻塞操作
- 緩存
- sleep
- 定時任務
- 禁止某些終端訪問
- 請求返回后繼續執行
- 調試
- 調用其他C函數動態庫
- 我的lua代碼需要調優么
- 變量的共享范圍
- 動態限速
- shared.dict 非隊列性質
- 如何添加自己的lua api
- 正確使用長鏈接
- 如何引用第三方resty庫
- 使用動態DNS來完成HTTP請求
- 緩存失效風暴
- Lua
- 下標從1開始
- 局部變量
- 判斷數組大小
- 非空判斷
- 正則表達式
- 不用標準庫
- 虛變量
- 函數在調用代碼前定義
- 抵制使用module()函數來定義Lua模塊
- 點號與冒號操作符的區別
- 測試
- 單元測試
- API測試
- 性能測試
- 持續集成
- 灰度發布
- web服務
- API的設計
- 數據合法性檢測
- 協議無痛升級
- 代碼規范
- 連接池
- c10k編程
- TIME_WAIT問題
- 與Docker使用的網絡瓶頸
- 火焰圖
- 什么時候使用
- 顯示的是什么
- 如何安裝火焰圖生成工具
- 如何定位問題