Lua 的 io.popen 及其 IPC1 管道控制代碼上的讀取/關閉操作是同步的:它們會在 CPU 和 off-CPU 兩個維度同時阻塞 OpenResty/Nginx 的事件迴圈,worker 程序在等待期間無法執行任何其他工作。在一個生產環境案例中,OpenResty XRay 把目標程序 93.8% 的 CPU 時間和 99.8% 的 off-CPU 時間追蹤到了 io.popen 及其管道控制代碼上的讀取操作。改用 OpenResty 自身的非阻塞 lua-resty-shell 庫後,吞吐量提升近七倍;進一步用非阻塞 cosocket API 替換管道後,提升達 150 倍

下文展示 OpenResty XRay 如何在網路安全行業客戶的生產環境中自動定位這個瓶頸——無需修改程式碼,也無需重啟程序。

柱狀圖:單個 CPU 核心的每秒請求數從使用 io.popen 時的 126 提升到 lua-resty-shell 後的 859,改進 7 倍

柱狀圖:單個 CPU 核心的每秒請求數從 126 提升到改用 cosocket 後的 18537,改進 150 倍

OpenResty XRay 是一個動態追蹤產品,它可以自動分析正在執行中的應用程式,以排除效能問題、行為問題和安全漏洞,並提供可行的建議。在底層實現上,OpenResty XRay 由我們的 Y 語言驅動,可以在不同環境下支援多種不同的執行時,如 Stap+, eBPF+, GDB 和 ODB。

問題:每秒 130 個請求,CPU 卻閒置過半

客戶在他們的線上 OpenResty 應用程式中遭遇了嚴重的效能問題。在他們的伺服器上,每秒最大請求數非常低,僅有 130 左右。這糟糕到讓服務幾乎不可用。此外,儘管他們的伺服器上配有高階 CPU,他們的 Nginx worker 程序只能利用不到一半的 CPU 資源。

OpenResty XRay 如何定位事件迴圈阻塞

OpenResty XRay 對客戶的線上程序進行了深入分析。它不需要客戶的應用程式進行任何協作。

  • 沒有額外的外掛、模組或庫。
  • 沒有程式碼注入或補丁。
  • 沒有特殊的編譯或啟動選項。
  • 甚至不需要重新啟動應用程式程序。

分析完全是以“事後”的方式進行的。這得益於 Openresty XRay 採用的動態追蹤技術。

這類效能問題對於 OpenResty XRay 來說很容易分析。它發現 CPU 和 off-CPU 操作一起阻塞了 OpenResty/Nginx 的事件迴圈。

CPU 操作:io.popen 與 file:read() 熱點

OpenResty XRay 的自動分析報告中,我們可以看到 io.popen 及其相關的 file:close() 操作讓 CPU 使用率非常高。

io.popen

在 CPU 類別下,我們可以找到 io.popen 問題。它佔據了目標程序所消耗的總 CPU 時間的 93.8%

OpenResty XRay CPU 報告:io.popen 是最熱的 Lua 程式碼路徑,佔 CPU 時間的 93.8%

注意高亮顯示的問題中的 [builtin#io.popen] Lua 函式幀。

問題文字顯示了完整的 Lua 程式碼路徑,包括虛擬機器內的 LuaJIT 原語,因此使用者可以在對應 Lua 原始碼中快速定位。如果我們將滑鼠游標懸停在 Lua 函式 run() 的綠框上,就會彈出一個工具提示,其中有 Lua 原始檔名和行號等細節。

OpenResty XRay CPU 報告的工具提示:io.popen 呼叫位於 cfg-utils.lua 第 8 行

我們可以看到,io.popen 呼叫的位置在 Lua 原始檔 /usr/local/openresty/site/lualib/cfg-utils.lua 的第 8 行。

file:read()

在 CPU 類別下,我們發現了 file:read() 問題。它佔據了目標程序所消耗的總 CPU 時間的 26.3%

OpenResty XRay CPU 報告:file:read() 經 builtin#io.method.read 幀佔 CPU 時間的 26.3%

注意高亮顯示的問題中的 [builtin#io.method.read] 函式幀。

問題文字顯示了完整的 Lua 程式碼路徑,包括虛擬機器內的 LuaJIT 原語,因此使用者可以在對應 Lua 原始碼中快速定位。如果我們將滑鼠游標懸停在 Lua 函式 run() 的綠框上,就會彈出一個工具提示,其中有 Lua 原始檔名和行號等細節。

OpenResty XRay CPU 報告的工具提示:file:read() 呼叫位於 cfg-utils.lua 第 14 行

我們可以看到,file:read() 的呼叫位置在 Lua 原始檔 /usr/local/openresty/site/lualib/cfg-utils.lua 的第 14 行。客戶檢查了該行,並確認它是在先前的 io.popen 呼叫所開啟的檔案柄上。

off-CPU 操作:file:read() 在哪裡阻塞等待

這裡的 “off-CPU” 指的是作業系統執行緒被阻塞並處於等待狀態,不能執行後面的程式碼。

手繪圖解:程序在兩段 on-CPU 之間的 off-CPU 間隙中休眠等待

這類 off-CPU 阻塞是高長尾延遲的經典根因。我們在另一個案例中展示了如何在 50 萬 QPS 的 OpenResty 閘道器中定位 244 毫秒的效能異常。而量化事件迴圈阻塞嚴重程度的完整取證過程——20 秒內捕獲 43952 個阻塞樣本、單次最長 75 毫秒——參見死活不肯超過 300 RPS 的 Nginx worker 程序

file:read()

在診斷報告中,我們看到 file:read() 的呼叫在 off-CPU 時間方面很熱。它佔用了目標程序所消耗的總 off-CPU 時間的 99.8%,而唯一會阻塞的應該是 Nginx 事件迴圈的事件等待操作(如 epoll_wait 系統呼叫)。

OpenResty XRay off-CPU 報告:file:read() 佔最熱 Lua 程式碼路徑 off-CPU 時間的 99.8%

注意高亮顯示的問題中的 [builtin#io.method.read] Lua 函式幀。

問題文字顯示了完整的 Lua 程式碼路徑,包括虛擬機器內的 LuaJIT 原語,因此使用者可以在其 Lua 原始碼中快速定位。如果我們將滑鼠游標懸停在 Lua 函式 run() 的綠框上,就會彈出一個工具提示,其中有 Lua 原始檔名和行號等細節。

OpenResty XRay off-CPU 報告的工具提示:阻塞的 file:read() 呼叫位於 cfg-utils.lua 第 14 行

我們可以看到,file:read() 的呼叫位置在 Lua 原始檔 /usr/local/openresty/site/lualib/cfg-utils.lua 的第 14 行。客戶檢查了該行,確認它也是在先前的 io.popen 呼叫所開啟的檔案控制代碼上。

io.popen 的非阻塞替代方案:lua-resty-shell、ngx.pipe 與 cosocket

根據上述分析,罪魁禍首是管道的 Lua API,包括 io.popen 和對管道檔案控制代碼的讀取和關閉操作。它在 CPU 和 off-CPU 時間方面都嚴重阻塞了 Nginx 的事件迴圈。因此,解決方案也是直截了當的。

  1. 在 OpenResty 應用程式中避免使用 io.popen Lua API。使用 OpenResty 的 lua-resty-shell 庫或低階別的 Lua API ngx.pipe 來代替。
  2. 完全避免系統命令和 IPC 管道。使用 OpenResty 提供的更有效的 cosocket API 或建立在它之上的更高階別的庫。

結果:lua-resty-shell 提升 7 倍,cosocket 提升 150 倍

客戶聽從了我們的建議,在他們的閘道器應用中從標準的 Lua API io.popen遷移到 OpenResty 的 lua-resty-shell 庫。然後他們立即看到了大約七倍的改進。

單核每秒請求數對比:從 io.popen 的 126 提升到 lua-resty-shell 的 859

我們進一步建議他們應該完全避免昂貴的系統命令呼叫。然後他們修整了業務邏輯,並利用 OpenResty 的非阻塞 cosocket API 來獲取後設資料。這一變化帶來了 150 倍的改進,讓1個 CPU 核心達到每秒數以萬計的請求。

單核每秒請求數對比:從 126 提升到改用 cosocket 後的 18537

客戶對現在的效能表現很滿意。

常見問題

io.popen 為甚麼會阻塞 Nginx 事件迴圈?

io.popen 及其管道檔案控制代碼上的讀取、關閉操作是同步的 Lua API:作業系統執行緒會阻塞等待、無法執行後續程式碼,而事件迴圈中唯一應該阻塞的只有事件等待操作(如 epoll_wait 系統呼叫)。在本文案例中,這些呼叫佔了目標程序 93.8% 的 CPU 時間和 99.8% 的 off-CPU 時間。

在 OpenResty 中應該用甚麼替代 io.popen?

使用 OpenResty 的非阻塞 lua-resty-shell 庫或更底層的 ngx.pipe API——僅此一項在本案例中就帶來了 7 倍吞吐量提升。更進一步,完全避免呼叫系統命令、改用非阻塞 cosocket API 獲取資料,帶來了 150 倍的提升。

如何知道自己的 Nginx worker 程序是否被 io.popen 阻塞?

OpenResty XRay 可以自動分析正在執行的程序——無需額外外掛或庫、無需程式碼注入、無需重啟程序。它的診斷報告會給出每個阻塞操作的完整 Lua 程式碼路徑,精確到原始檔名和行號,正如本案例中定位到 cfg-utils.lua 第 8 行的 io.popen 呼叫。

關於作者

章亦春是開源 OpenResty® 專案創始人兼 OpenResty Inc. 公司 CEO 和創始人。

章亦春(Github ID: agentzh),生於中國江蘇,現定居美國灣區。他是中國早期開源技術和文化的倡導者和領軍人物,曾供職於多家國際知名的高科技企業,如 Cloudflare、雅虎、阿里巴巴, 是 “邊緣計算“、”動態追蹤 “和 “機器程式設計 “的先驅,擁有超過 22 年的程式設計及 16 年的開源經驗。作為擁有超過 4000 萬全球域名使用者的開源專案的領導者。他基於其 OpenResty® 開源專案打造的高科技企業 OpenResty Inc. 位於美國矽谷中心。其主打的兩個產品 OpenResty XRay(利用動態追蹤技術的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最適合微服務和分散式流量的全能型閘道器軟體),廣受全球眾多上市及大型企業青睞。在 OpenResty 以外,章亦春為多個開源專案貢獻了累計超過百萬行程式碼,其中包括,Linux 核心、Nginx、LuaJITGDBSystemTapLLVM、Perl 等,並編寫過 60 多個開源軟體庫。

關注我們

如果您喜歡本文,歡迎關注我們 OpenResty Inc. 公司的部落格網站

我們也在 B 站上也有 OpenResty 官方的影片分享空間,歡迎訂閱。

同時歡迎掃碼關注我們的微信公眾號:

我們的微信公眾號


  1. IPC 是指程序間通訊。 ↩︎