在 Nginx 裏面,每一個 worker 進程都是平等的。可是有些時候,咱們須要給它們分配不一樣的角色,這時候就須要實現進程間通信的功能。nginx
一種簡單粗暴但卻被廣泛使用的方案,就是每一個進程劃分屬於本身的 list 類型的 shdict key,每隔一段時間查看是否有新消息。這種方式優勢在於實現簡單,缺點在於難以保證明時性。固然對於絕大多數須要進程間通信的場景,每 0.1 起一個 timer 來處理新增消息已經足夠了。畢竟 0.1 秒的延遲不算長,每秒起 10 個 timer 開銷也不大,應付通常的通訊量綽綽有餘。git
要是你以爲輪詢很搓,或者在你的環境下,輪詢確實很搓,也能夠考慮下引入外部依賴來改善實時性。好比在本地起一個 redis,監聽 unix socket,而後每一個進程經過 Pub/Sub 或者 stream 類型發佈/獲取最新的消息。這種方案實現起來也簡單,實時性和性能也足夠好,只是須要引入個 redis 服務。github
若是你是個極簡主義者,對引入外部依賴深惡痛絕,但願什麼東西都能在 Nginx 裏面實現的話,ngx_lua_ipc 是一個被普遍使用的選擇。redis
ngx_lua_ipc
是一個第三方 Nginx C 模塊,提供了一些 Lua API,可供在 OpenResty 代碼裏完成進程間通信(IPC)的操做。segmentfault
它會在 Nginx 的 init 階段建立 worker process + helper process 對 pipe fd。每對 fd 有一個做爲 read fd,負責接收數據,另外一個做爲 write fd,用於發送數據。當 Nginx 建立 worker 進程時,每一個 worker 進程都會繼承這些 pipe fd,因而就能經過它們來實現進程間通信。感興趣的讀者能夠 man 7 pipe
一下,瞭解基於 pipe 的進程間通信是怎麼實現的。網絡
固然 ngx_lua_ipc
還須要把 pipe 的 read fd 經過 ngx_connection_t
接入到 Nginx 的事件循環機制中,具體實現位於 ipc_channel_setup_conn
:socket
c = ngx_get_connection(chan->pipe[conn_type == IPC_CONN_READ ? 0 : 1], cycle->log); c->data = data; if(conn_type == IPC_CONN_READ) { c->read->handler = event_handler; c->read->log = cycle->log; c->write->handler = NULL; ngx_add_event(c->read, NGX_READ_EVENT, 0); chan->read_conn=c; } else if(conn_type == IPC_CONN_WRITE) { c->read->handler = NULL; c->write->log = cycle->log; c->write->handler = ipc_write_handler; chan->write_conn=c; } else { return NGX_ERROR; } return NGX_OK;
write fd 是由 Lua 代碼操做的,因此不須要加入到 Nginx 的事件循環機制中。tcp
有一點有趣的細節,pipe fd 只有在寫入數據小於 PIPE_BUF
時纔會保證寫操做的原子性。若是一條消息超過 PIPE_BUF
(在 Linux 上大於 4K),那麼它的寫入就不是原子的,可能寫入前面 PIPE_BUF
以後,有另外一個 worker 也正巧給同一個進程寫入消息。函數
爲了不不一樣 worker 進程的消息串在一塊兒,ngx_lua_ipc
定義了一個 packet 概念。每一個 packet 都不會大於 PIPE_BUF
,同時有一個 header 來保證單個消息分割成多個 packet 以後可以被從新打包回來。性能
在接收端,爲了能在收到消息以後執行對應的 Lua handler,ngx_lua_ipc
使用了 ngx.timer.at
來執行一個函數,這個函數會根據消息類型分發到對應的 handler 上。這樣有個問題,就是消息是否能完成投遞,取決於 ngx.timer.at
可否被執行。而 ngx.timer.at
是否被執行受限於兩個因素:
lua_max_pending_timer
不夠大,ngx.timer.at
可能沒法建立 timerlua_max_running_timer
不夠大,或者沒有足夠的資源運行 timer,ngx.timer.at
建立的 timer 可能沒法運行。事實上,若是 timer 沒法運行(消息沒法投遞),現階段的 OpenResty 可能不會記錄錯誤日誌。我以前提過一個記錄錯誤日誌的 PR:https://github.com/openresty/...,不過一直沒有合併。
因此嚴格意義上, ngx_lua_ipc
並不能保證消息可以被投遞,也不能在消息投遞失敗時報錯。不過這個鍋得讓 ngx.timer.at
來背。
ngx_lua_ipc
能不能不用 ngx.timer.at
那一套呢?這個就須要從 lua-nginx-module 裏複製一大段代碼,並偶爾同步一下。複製粘貼乃 Nginx C 模塊開發的奧義。
上面的方法中,除了 Redis 外援法,若是不在應用代碼里加日誌,要想在外部查看消息投遞的過程,只能依靠 gdb/systemtap/bcc 這些大招。若是走網絡鏈接,就能使用平民技術,如 tcpdump,來追蹤消息的流動。固然若是是 unix socket,還須要臨時搞個 TCP proxy 整一下,不過操做難度較前面的大招們已經大大下降了。
那有沒有辦法讓 IPC 走網絡,但又不須要藉助外部依賴呢?
回想起 Redis 外援法,之因此咱們不能直接走 Nginx 的網絡請求,是由於 Nginx 裏面每一個 worker 進程是平等的,你不知道你的請求會落到哪一個進程上,而請求 Redis 就沒這個問題。那咱們能不能讓不一樣的 worker 進程動態監聽不一樣的 unix socket?
答案是確定的。咱們能夠實現相似於這樣的接口:
ln = ngx.socket.listen(...) sock = ln.accept() sock:read(...)
曾經有人提過相似的 PR:https://github.com/openresty/...,我本身也在公司項目裏實現過差很少的東西。聲明下,不要用這個方法作 IPC。上面的實現有個致命的問題,就是 ln 和後面建立的全部的 sock,都是在同一個 Nginx 請求裏面的。
我曾經寫過,在一個 Nginx 請求裏作太多的事情,會有資源分配上的問題:https://segmentfault.com/a/11...
後面隨着 IPC 的次數的增長,這種問題會愈加明顯。
要想解決這個問題,咱們能夠把每一個 sock 放到獨立的 fake request 裏面跑,就像這樣:
ln = ngx.socket.listen(...) -- 相似於 ngx.timer.at 的處理風格 ln.register_handler(function(sock) sock:read(...) end)
可是還有個問題。若是用 worker id 做爲被監聽的 unix socket 的 ID, 因爲這個 unix socket 是在 worker 進程裏動態監聽的,而在 Nginx reload 或 binary upgrade 的狀況下,多個 worker 進程會有一樣的 worker id,嘗試監聽一樣的 unix socket,致使地址被佔用的錯誤。解決方法就是改用 PID 做爲被監聽的 unix socket 的 ID,而後在首次發送時初始化 PID 到 worker id 的映射。若是有支持在 reload 時正常發送消息的需求,還要記錄新舊兩組 worker,好比:
1111 => old worker ID 1 1123 => new worker ID 2
還有一種更爲巧妙的,藉助不一樣 worker 不一樣 unix socket 來實現進程間通信的方法。這種方法是如此地巧妙,我只恨不是我想出來的。該方法能夠淘汰掉上面動態監聽 unix socket 的方案。
咱們能夠在 Nginx 配置文件裏面聲明,listen unix:xxx.sock use_as_ipc_blah_blah
。而後修改 Nginx,讓它在看到 use_as_ipc_blah_blah
差很少這樣的一個標記時,讓特定的進程監聽特定的 unix sock,好比 xxx_1.sock
、xxx_2.sock
等。
它跟動態監聽 unix socket 方法比起來,實現更爲簡單,因此也更爲可靠。固然要想保證在 reload 或者 binary upgrade 時投遞消息到正確的 worker,記得用 PID 而不是 worker id 來做爲區分後綴,並維護好二者間的映射。
這個方法是由 datavisor 的同行提出來的,估計最近會開源出來。