定時任務
在 OpenResty 中,有時候需要在后台定期地執行某些任務,比如同步數據、清理日志等。最容易想到的方法,便是對外提供一個 API 接口,在接口中完成這些任務;
然后用系統的 crontab 定時調用 curl,來訪問這個接口,進而曲線地實現這個需求。
不過,這樣會給運維帶來更高的復雜度。所以, OpenResty 提供了 ngx.timer 來解決這類需求。可以把ngx.timer看作是 OpenResty 模擬的客戶端請求,用以觸發對應的回調函數。
OpenResty 的定時任務可以分為下面兩種:
- ngx.timer.at,用來執行一次性的定時任務;
- ngx.time.every,用來執行固定周期的定時任務。
使用ngx.timer可以突破 init_worker_by_lua 中不能使用 cosocket 的限制。
下面這段代碼,啟動了一個延時為0的定時任務。它啟動了回調函數 handler,並在這個函數中,用 cosocket 去訪問一個網站:
init_worker_by_lua_block { local function handler() local sock = ngx.socket.tcp() local ok, err = sock:connect(“www.baidu.com", 80) end local ok, err = ngx.timer.at(0, handler) }
這樣,就繞過了 cosocket 在這個階段不能使用的限制。
ngx.timer.at 並沒有解決周期性運行這個需求,在上面的示例中,它是一個一次性的任務。
基於 ngx.timer.at 這個API,如果要做到周期性運行:
- 可以在回調函數中,使用一個 while true 的死循環,執行完任務后 sleep 一段時間,自己來實現周期任務;
- 你還可以在回調函數的最后,再創建另外一個新的 timer。
timer 的本質是一個請求,雖然這個請求不是終端發起 的;而對於請求來講,在完成自己的任務后它就要退出,不能一直常駐,否則很容易造成各種資源的泄漏。
第一種使用 while true 來自行實現周期任務的方案並不靠譜。第二種方案雖然是可行的,但遞歸地創建 timer ,並不容易理解。
OpenResty 后面新增的 ngx.time.every API,就是專門為了解決這個問題而出現的,它是更加接近 crontab 的解決方案。
在啟動了一個 timer 之后,就再也沒有機會來取消這個定時任務了,畢竟 ngx.timer.cancel 還是一個 todo 的功能。
就會面臨一個問題:定時任務是在后台運行的,並且無法取消;如果定時任務的數量很多,就很容易耗盡系統資源。
所以,OpenResty 提供了 lua_max_pending_timers 和 lua_max_running_timers 這兩個指令,來對其進行限制。前者代表等待執行的定時任務的最大值,后者代表當前正在運行的定時任務的最大值。
可以通過 Lua API,來獲取當前等待執行和正在執行的定時任務的值:
content_by_lua_block { ngx.timer.at(3, function() end) ngx.say(ngx.timer.pending_count()) }
這段代碼會打印出 1,表示有 1 個計划任務正在等待被執行。
content_by_lua_block { ngx.timer.at(0.1, function() ngx.sleep(0.3) end) ngx.sleep(0.2) ngx.say(ngx.timer.running_count()) }
這段代碼會打印出 1,表示有 1 個計划任務正在運行中。
特權進程
Nginx 主要分為 master 進程和 worker 進程,其中,真正處理用戶請求的是 worker 進程。可以通過 lua-resty-core 中提供的 process.type API ,獲取到進程的類型。
運行下面這個函數:
resty -e 'local process = require "ngx.process" ngx.say("process type:", process.type())'
返回的結果不是 worker, 而是single。這意味 resty 啟動的 Nginx 只有 worker 進程, 沒有 master 進程。在 resty 的實現中,下面這樣的一行配置, 關閉了 master 進程:
master_process off;
而OpenResty 在 Nginx 的基礎上進行了擴展,增加了特權進程:privileged agent。特權進程很特別:
它不監聽任何端口,這就意味着不會對外提供任何服務;
它擁有和 master 進程一樣的權限,一般來說是 root 用戶的權限,這就讓它可以做很多 worker 進程不可能完成的任務;
特權進程只能在 init_by_lua 上下文中開啟;
特權進程只有運行在 init_worker_by_lua 上下文中才有意義,因為沒有請求觸發,也就不會走到content、access 等上下文去。
來看一個開啟特權進程的示例:
init_by_lua_block { local process = require "ngx.process" local ok, err = process.enable_privileged_agent() if not ok then ngx.log(ngx.ERR, "enables privileged agent failed error:", err) end }
通過這段代碼開啟特權進程后,再去啟動 OpenResty 服務,可以看到,Nginx 的進程中多了特權進程:
nginx: master process nginx: worker process nginx: privileged agent process
特權進程不監聽端口,也就不能被終端請求觸發,那就只有使用gx.timer ,來周期性地觸發:
init_worker_by_lua_block { local process = require "ngx.process" local function reload(premature) local f, err = io.open(ngx.config.prefix() .. "/logs/nginx.pid", "r") if not f then return end local pid = f:read() f:close() os.execute("kill -HUP " .. pid) end if process.type() == "privileged agent" then local ok, err = ngx.timer.every(5, reload) if not ok then ngx.log(ngx.ERR, err) end end }
上面這段代碼,實現了每 5 秒給 master 進程發送 HUP信號量的功能
非阻塞的 ngx.pipe
上面示例中,使用了 Lua 的標准庫,來執行外部命令行,把信號發送給了 master 進程,這種操作是會阻塞的。
os.execute("kill -HUP " .. pid)
lua-resty-shell 庫來調用命令行就是非阻塞的:
$ resty -e 'local shell = require "resty.shell" > local ok, stdout, stderr, reason, status = > shell.run([[echo "hello, world"]]) > ngx.say(stdout)' hello, world
這段代碼可以算是 hello world 的另外一種寫法了,它調用系統的 echo 命令來完成輸出。類似的,可以 用 resty.shell ,來替代 Lua 中的 os.execute 調用。
lua-resty-shell 的底層實現,依賴了 lua-resty-core 中的 [ngx.pipe] API,所以,這個 使用 lua-resty-shell 打印出 hello wrold 的示例,改用 ngx.pipe ,可以寫成下面這樣:
$ resty -e 'local ngx_pipe = require "ngx.pipe" > local proc = ngx_pipe.spawn({"echo", "hello world"}) > local data, err = proc:stdout_read_line() > ngx.say(data)' hello world
OpenResty 在做一個更好用的 Nginx 的前提下,也在嘗試往通用平台的方向上靠攏,希望開發者能夠盡量統一技術棧,都用 OpenResty 來解決開發需求。這對於運維來說是相當友好的,因為只要部署一個 OpenResty 就可以了,維護成本更低。