日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

在 OpenResty 里实现进程间通讯

發布時間:2024/3/26 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 在 OpenResty 里实现进程间通讯 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

在 Nginx 里面,每個 worker 進程都是平等的。但是有些時候,我們需要給它們分配不同的角色,這時候就需要實現進程間通訊的功能。

輪詢

一種簡單粗暴但卻被普遍使用的方案,就是每個進程劃分屬于自己的 list 類型的 shdict key,每隔一段時間查看是否有新消息。這種方式優點在于實現簡單,缺點在于難以保證實時性。當然對于絕大多數需要進程間通訊的場景,每 0.1 起一個 timer 來處理新增消息已經足夠了。畢竟 0.1 秒的延遲不算長,每秒起 10 個 timer 開銷也不大,應付一般的通信量綽綽有余。

redis外援

要是你覺得輪詢很搓,或者在你的環境下,輪詢確實很搓,也可以考慮下引入外部依賴來改善實時性。比如在本地起一個 redis,監聽 unix socket,然后每個進程通過 Pub/Sub 或者 stream 類型發布/獲取最新的消息。這種方案實現起來也簡單,實時性和性能也足夠好,只是需要引入個 redis 服務。

ngx_lua_ipc

如果你是個極簡主義者,對引入外部依賴深惡痛絕,希望什么東西都能在 Nginx 里面實現的話,ngx_lua_ipc?是一個被廣泛使用的選擇。

ngx_lua_ipc?是一個第三方 Nginx C 模塊,提供了一些 Lua API,可供在 OpenResty 代碼里完成進程間通訊(IPC)的操作。

它會在 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:

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 的事件循環機制中。

有一點有趣的細節,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?可能無法創建 timer
  • 如果?lua_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 模塊開發的奧義。

    動態監聽 unix socket

    上面的方法中,除了 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

    還有一種更為巧妙的,借助不同 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 的同行提出來的,估計最近會開源出來。

    總結

    以上是生活随笔為你收集整理的在 OpenResty 里实现进程间通讯的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。