Off-CPU 分析實戰:CPU 使用率低但延遲高的四類根因與定位方法
Off-CPU 分析測量的是執行緒離開 CPU(被阻塞、被搶佔、或在等待)期間的時間和呼叫棧。它解決一類在 top 裡看不出病因的問題:CPU 使用率明明很低,延遲卻居高不下、吞吐量怎麼也上不去——請求在等,不在算。On-CPU profiling 與 off-CPU 分析互補:兩者合計覆蓋執行緒的 100% 時間。在一個真實的生產案例中,OpenResty XRay 把程序 99.8% 的 off-CPU 時間追蹤到了 io.popen 及其管道控制代碼上的讀取操作,修復後單核吞吐量從 126 RPS 提升到 18,537 RPS——150 倍。
Off-CPU 時間是甚麼?
當一個執行緒(或程序)正在 CPU 上執行指令時,我們稱它處於 on-CPU 狀態;當它因為某種原因被移出 CPU——網路 I/O 等待、鎖競爭、子程序等待、檔案/管道 I/O 阻塞、甚至排程器搶佔——而無法繼續執行時,它就處於 off-CPU 狀態。
上圖展示了一個執行緒在 on-CPU 與 off-CPU 之間交替切換的過程。off-CPU 時間是所有「暗區」的總和——執行緒想做事但被卡住的時間。
對於事件驅動的伺服器(如 Nginx/OpenResty),worker 執行緒唯一「應該」阻塞的位置是事件迴圈自身的等待操作(如 epoll_wait 系統呼叫)。任何其他位置的 off-CPU 時間都意味著事件迴圈被卡住了,所有該 worker 上的併發連線都在空等。
症狀:CPU 使用率上不去,請求卻在排隊
Off-CPU 瓶頸在監控面板上有一套典型的三聯證據:
top顯示 CPU 使用率卡在低位——程序持續執行,但 CPU 欄始終不漲。- access log 持續增長——請求確實在湧入,並非沒流量。
- 加壓也不漲——無論施加多大壓力,CPU 就是上不去。
這個模式在不同語言的應用中反覆出現:一個 Perl 程序卡在 13%、一個 Go 服務卡在 2%、一個 Python gunicorn worker 卡在 8%。CPU 使用率的具體數值各不相同,但根因一致:執行緒在等待,不在計算。
如果你在搜尋“為甚麼 CPU 使用率很低但延遲很高”——答案几乎都在 off-CPU 方向:某個程式碼路徑在執行同步阻塞操作(網路呼叫、子程序等待、檔案讀取……),執行緒被掛起,CPU 空轉,請求排隊。要找到具體阻塞在哪一行程式碼,需要 off-CPU 分析。
這樣的阻塞同樣會表現為 p99 尾延遲的週期性尖刺——在事件驅動的伺服器中,單個 worker 被阻塞意味著該 worker 上所有併發請求同時受影響。但並非每次尾延遲尖刺都是 off-CPU 問題:我們在一個 50 萬 QPS 的 OpenResty 閘道器中追蹤到了 244 毫秒的延遲尖刺,根因卻在 on-CPU 一側——正規表示式低效的回溯模式在持續消耗 CPU 時間,而不是阻塞等待。兩者從外部看症狀完全一樣;時間究竟花在等待還是計算上,正是 off-CPU 分析與 on-CPU 剖析各自要回答的問題。
四類 Off-CPU 根因的真實根因鏈
Off-CPU 時間的來源可以歸納為四大類。每類我們給出機制解釋和一條真實的根因鏈——從系統呼叫一路追蹤到業務程式碼的檔案和行號。
1. 同步網路 I/O
機制:執行緒發出網路請求後同步等待響應,作業系統把執行緒掛起直到資料到達。
真實根因鏈:在一個 Perl 程序 CPU 使用率僅 13% 的案例中,off-CPU 火焰圖展示的阻塞路徑是:
select系統呼叫 →Perl_pp_select→Net::HTTP::Methods::can_read→read_response_headers→ 業務函式remote_fetch
滑鼠懸停在火焰圖中的 remote_fetch 函式框上,彈出的工具提示顯示原始檔路徑和行號。跳轉到第 66 行,可以看到程式碼正在傳送 HTTP GET 請求並同步等待響應。
2. 子程序與 Shell 呼叫
機制:程式透過 exec/subprocess 等方式啟動外部程序,然後 waitpid 等待其退出。在等待期間,呼叫方執行緒被阻塞。
真實根因鏈一(Go):在一個 Go 服務 CPU 使用率僅 2% 的案例中,off-CPU 報告自動推匯出的阻塞路徑是:
Syscall6→Process.wait(底層為waitpid)→exec.Cmd.Run→ 業務函式chat.RateLimit
原始檔 chat/processor.go 第 46 行——RateLimit 函式呼叫了 exec.Command("/usr/bin/sleep", "0.01") 並執行 cmd.Run(),同步等待 shell 命令完成。
真實根因鏈二(Python):在一個 Python gunicorn worker CPU 使用率僅 8% 的案例中,排名第一的 C 級 off-CPU 路徑佔 96%,Python 級別的 #1 阻塞路徑佔了 92.4% 的阻塞時間:
poll系統呼叫 →subprocess.run→ 業務函式handle_by_script
原始檔 processor.py 第 12 行——呼叫 subprocess.run 執行外部 bash 指令碼並等待輸出。
3. 同步檔案與管道 I/O
機制:標準庫的同步檔案/管道 API(如 Lua 的 io.popen、file:read())會阻塞當前執行緒,直到 I/O 操作完成。在事件驅動框架中這尤其致命:阻塞的不是一個請求,而是整個事件迴圈。
真實根因鏈:在一個生產環境的 OpenResty 應用中,OpenResty XRay 發現了兩處阻塞呼叫點——cfg-utils.lua 第 8 行的 io.popen 呼叫和第 14 行的 file:read() 呼叫(後者讀取的是前者開啟的管道控制代碼)。OpenResty XRay 將目標程序 93.8% 的 CPU 時間和 99.8% 的 off-CPU 時間追蹤到了這兩處呼叫。改用 OpenResty 的非阻塞 lua-resty-shell 庫後,單核吞吐量從 126 RPS 提升到 859 RPS(7 倍);進一步用非阻塞 cosocket API 取代管道後,提升到 18,537 RPS(150 倍)。
此外,檔案 I/O 的延遲有時也不容忽視。在另一個案例中,APR(Apache Portable Runtime)庫函式經 ModSecurity 模組在 Nginx 程序內呼叫時,apr_generate_random_bytes 的單次讀取延遲最高達 1,494 微秒(接近 1.5 毫秒),apr_sdbm_fetch 最高達 953 微秒。對 Nginx 這樣以高併發低延遲著稱的平臺來說,即使一毫秒的同步阻塞也是嚴重問題。
4. CPU 爭用型:就緒但搶不到核
機制:大多數 off-CPU 教程只講“主動等待”——執行緒因 I/O 或鎖被掛起。但還有一種容易被忽略的情況:執行緒已經就緒、作業系統也標記它為可執行(runnable),但因為沒有空閒的 CPU 核可排程,它被迫等在就緒佇列裡。這是“非自願下車”,不是“主動停車”。
真實根因鏈:在一個 Nginx worker 程序的 C 級別 off-CPU 分析中,OpenResty XRay 發現除了正常的 epoll_wait 等待棧之外,相當多的 off-CPU 時間落在了 mpi_mul_hlp、free 這樣的純 CPU 計算函式上。
一個執行緒在執行純計算函式時出現 off-CPU 時間,說明它並非在等 I/O,而是已經就緒但拿不到 CPU 時間片——這是 CPU 資源爭用的經典症狀。進一步分析發現,根因是 Nginx 配置中缺少 worker_cpu_affinity 指令,導致多個 worker 程序被 Linux 核心在不同 CPU 核間頻繁排程,產生不必要的上下文切換開銷。該案例中單個 worker 的 RPS 只有 227–286,系統負載卻已逼近 4(等於邏輯 CPU 核數)。
這類 off-CPU 根因的識別方法與前三類不同:不是看阻塞在哪個 I/O 系統呼叫上,而是看 off-CPU 時間是否出現在本應是純 on-CPU 執行的函式上。
如何量化 Off-CPU 時間
定位 off-CPU 瓶頸需要兩層測量手段:
雙層呼叫棧分析:從系統呼叫到原始檔行號
Off-CPU 分析中最核心的手段是對執行緒阻塞期間的呼叫棧進行取樣。有效的分析通常需要兩個層級對照使用:
- C/系統層級:展示系統呼叫棧(如
select、poll、waitpid、read),幫助判斷阻塞的型別。 - 語言層級(Perl/Python/Go/Lua……):展示業務程式碼的呼叫棧,把系統呼叫對映到具體的業務函式、原始檔和行號。
單看 C 層級只能知道“卡在 poll 上”——但到底是哪一行業務程式碼觸發的 poll?需要語言層級的呼叫棧才能回答。在前述三個案例中,off-CPU 分析的終點都是一個可以開啟的原始檔行:Perl 原始檔第 66 行、Go processor.go 第 46 行、Python processor.py 第 12 行。這種“追蹤到行號”的可行動性正是 off-CPU 分析的核心價值。
事件迴圈阻塞延遲分佈:量化影響面
對於事件驅動的伺服器,僅知道“有阻塞”還不夠,還需要量化阻塞的嚴重程度。OpenResty XRay 可以取樣事件迴圈每次迭代的阻塞時長分佈。在一個真實案例中,20 秒取樣視窗內捕獲了 43,952 個阻塞樣本,單次最長阻塞達 75,165 微秒(75 毫秒)。這意味著每次事件迴圈被阻塞 75 毫秒時,該 worker 上的所有併發連線都在空等——這正是長尾延遲尖刺的直接成因。
On-CPU + Off-CPU = 掛鐘時間:全域性視角與工具生態
一個執行緒的掛鐘時間(wall-clock time)= on-CPU 時間 + off-CPU 時間。最佳化 on-CPU 時間提升吞吐量(單位時間做更多有用工作),最佳化 off-CPU 時間降低延遲(減少無謂等待)。兩個維度互補,缺一不可。
業界有多種 off-CPU 分析工具,但在生產環境中使用通常面臨一些門檻:需要安裝 agent 或核心模組、要求特定的核心版本、需要修改應用的啟動引數、或者只支援特定的語言執行時。
OpenResty XRay 提供了一條自動化路徑:透過 Guided Analysis 的“Low CPU usage and cannot go up”問題型別直接分析執行中的程序,無需安裝 perf/eBPF 工具、無需修改程式碼、無需重啟程序。它同時生成 C 級和語言級(Perl/Python/Go/Lua/Java……)兩個層級的 off-CPU 呼叫棧分析,並自動推匯出最顯著的阻塞程式碼路徑。Insights 頁面還會自動監控線上程序,生成日報和週報,持續追蹤 off-CPU 變化趨勢。
常見問題
甚麼是 off-CPU 時間?
Off-CPU 時間是指執行緒離開 CPU 後、到重新回到 CPU 之間的時間——執行緒在這段時間內無法執行任何指令。常見原因包括網路 I/O 等待、子程序等待、檔案/管道讀寫阻塞、鎖競爭和 CPU 排程爭用。On-CPU 時間與 off-CPU 時間之和等於執行緒的掛鐘時間。
為甚麼 CPU 使用率很低但延遲很高?
因為執行緒大部分時間被阻塞在等待上,而不是在 CPU 上執行有用的工作。典型的根因包括同步網路呼叫(如等待 HTTP 響應)、同步子程序呼叫(如 subprocess.run、exec.Cmd.Run)、同步檔案/管道 I/O(如 io.popen),以及 CPU 資源爭用(就緒執行緒搶不到核)。Off-CPU 分析可以把阻塞路徑追蹤到具體的原始檔和行號。
如何在不修改程式碼的情況下定位程式在等甚麼?
使用 OpenResty XRay 的 Guided Analysis 功能,選擇“Low CPU usage and cannot go up”問題型別,直接分析執行中的未修改程序。系統自動生成 off-CPU 呼叫棧分析並推匯出最顯著的阻塞程式碼路徑——滑鼠懸停即可看到原始檔和行號。全程不需要安裝額外工具、不需要修改程式碼或重啟程序。
Off-CPU 分析與掛鐘時間 profiling 有甚麼區別?
掛鐘時間(wall-clock)profiling 取樣執行緒的全部時間,不區分是在 CPU 上執行還是在等待。Off-CPU 分析只聚焦於執行緒被阻塞的時間及其呼叫棧——它是 on-CPU profiling 的互補視角。當你的問題是“CPU 上不去”或“延遲高但 CPU 不高”時,off-CPU 分析直接對準病因。
關於 OpenResty XRay
OpenResty XRay 是一個動態追蹤產品,它可以自動分析執行中的應用,以解決效能問題、行為問題和安全漏洞,並提供可行的建議。在底層實現上,OpenResty XRay 由我們的 Y 語言驅動,可以在不同環境下支援多種不同的執行時,如 Stap+、eBPF+、GDB 和 ODB。
關於作者
章亦春是開源 OpenResty® 專案創始人兼 OpenResty Inc. 公司 CEO 和創始人。
章亦春(Github ID: agentzh),生於中國江蘇,現定居美國灣區。他是中國早期開源技術和文化的倡導者和領軍人物,曾供職於多家國際知名的高科技企業,如 Cloudflare、雅虎、阿里巴巴, 是“邊緣計算”、“動態追蹤”和“機器程式設計”的先驅,擁有超過 22 年的程式設計及 16 年的開源經驗。作為擁有超過 4000 萬全球域名使用者的開源專案的領導者。他基於其 OpenResty® 開源專案打造的高科技企業 OpenResty Inc. 位於美國矽谷中心。其主打的兩個產品 OpenResty XRay(利用動態追蹤技術的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最適合微服務和分散式流量的全能型閘道器軟體),廣受全球眾多上市及大型企業青睞。在 OpenResty 以外,章亦春為多個開源專案貢獻了累計超過百萬行程式碼,其中包括,Linux 核心、Nginx、LuaJIT、GDB、SystemTap、LLVM、Perl 等,並編寫過 60 多個開源軟體庫。
關注我們
如果您喜歡本文,歡迎關注我們 OpenResty Inc. 公司的部落格網站 。也歡迎掃碼關注我們的微信公眾號:
翻譯
我們提供了英文版原文和中譯版(本文)。我們也歡迎讀者提供其他語言的翻譯版本,只要是全文翻譯不帶省略,我們都將會考慮採用,非常感謝!



















