P99 延遲:為甚麼平均值會騙人,以及如何診斷最慢的 1%
P99 延遲(P99 latency)是將所有請求按響應時間從快到慢排序後,第 99 百分位處的值——99% 的請求快於此值,只有最慢的 1% 超過它。在規模化系統中,這"最慢的 1%“意味著每秒成百上千個真實使用者的體驗。平均延遲可以掩蓋這些尖刺,P99 不會。
你負責一個線上服務。客服轉來投訴:“支付偶爾轉圈,重試一次就好了。“你開啟監控:平均延遲平穩,錯誤率為零,CPU 不高,儀表盤一片綠。你按經驗排查慢查詢、外部依賴、併發配置,全部正常;想壓測復現,壓不出來。與此同時投訴還在零星進來,上下游拿著 SLA 找上門,而你手裡沒有一個指標能證明問題存在。
這就是尾延遲問題的典型處境。它難纏不是因為技術深奧,而是因為它同時踩中了四個排查盲區:監控給的是聚合值,慢請求被平均稀釋掉;它偶發,測試環境裡不存在;根因往往只在生產真實負載下才觸發——一個 O(n) 查詢只在真實量級下爆炸,一個正則只對特定輸入回溯;而生產環境恰恰是你最不能改程式碼、加埋點、重啟服務的地方。這時你該看的第一個指標就是 P99。本文從定義出發,展開完整的根因譜系與診斷路徑,文中案例數字均來自真實生產環境。
甚麼是 P99 延遲?
P99 延遲(99th percentile latency)刻畫的是一組請求中最慢 1% 的響應時間邊界。它在資料上長甚麼樣?假設你採集了 100 個請求的響應時間,從快到慢排序:
#1 … #98 : ≤ 10ms
#99 : 300ms ← P99 = 300ms
#100 : 450ms
前 98 個請求都在 10ms 以內完成,但第 99 個是 300ms,P99 就是 300ms。這不只是理論構造,我們在一個金融科技閘道器案例中看到的正是這種形態,下文會完整展開。
計算方式
P99 的計算不涉及平均運算。它是純粹的排序取位:
- 收集一批請求的響應時間
- 按從小到大排序
- 取第 99% 位置的值
注意:P99 不做任何平均——第 99% 位置的值是多少,它就是多少。這讓它不會像平均值那樣被中間大量"正常"請求稀釋,而是直接反映尾部體驗。
P50 vs P95 vs P99:每個百分位告訴你甚麼
不同的百分位延遲迴答不同的問題。下面的對照表幫助你選擇正確的指標:
| 百分位 | 含義 | 回答的問題 | 典型用途 |
|---|---|---|---|
| P50(中位數) | 50% 的請求快於此值 | 典型使用者體驗如何? | 日常監控基線、容量規劃 |
| P95 | 95% 的請求快於此值 | 大多數使用者的最差體驗是甚麼? | 告警閾值、效能回歸檢測 |
| P99 | 99% 的請求快於此值 | 最慢的 1% 有多糟? | SLO/SLA 定義、尾部延遲治理 |
舉個例子,假設同一批 1,000 個請求算出三個讀數:P50 = 8ms、P95 = 40ms、P99 = 300ms。三個數字都是真的,但只有 P99 暴露了尾部——只盯著前兩個,你會以為系統很健康。
P50 告訴你"正常情況”,P95 告訴你"大多數時候的上限”,P99 告訴你"最糟糕的真實體驗”。在高流量系統中,1% 並不是一個可以忽略的數字——如果你的服務每秒處理 10,000 個請求,P99 描述的是每秒 100 個使用者的實際遭遇。
選擇哪個百分位作為 SLO 取決於業務場景。支付閘道器和交易系統通常以 P99 甚至 P99.9 為標準;內部工具可能只需要關注 P95。關鍵原則是:如果你只看 P50,你對系統的理解是樂觀的;如果你只看平均值,你的理解可能是錯誤的。
為甚麼平均延遲會騙人
打個比方:機場安檢平均每人 30 秒,但排在你前面那位被開箱檢查的乘客花了 10 分鐘。“平均 30 秒"這個數字沒有騙人,但它對你此刻的排隊體驗毫無解釋力——延遲指標也是同樣的道理。
延遲分佈有一個數學特性:它有下限(不可能低於 0ms)但沒有上限(一次 GC 停頓或鎖競爭可以把單個請求的延遲推到任意高)。這種右偏分佈意味著少數極慢的請求幾乎拉不動平均值,但它們會把 P99 頂到驚人的高度。
兩個真實案例可以說明這有多隱蔽:
- 監控全綠,SLA 擊穿。 一個峰值 500,000 QPS 的金融科技閘道器,P50 穩定在 10ms 以內,監控儀表盤一切正常,但 OpenResty XRay 的深度取樣發現最慢的 1% 請求超過 300ms,已經威脅到關鍵交易路徑的 SLA,再惡化下去就是閘道器側的 HTTP 504 超時。根因是甚麼,下一節的譜系裡會講到。
- 錯誤率、吞吐、均值全正常,P99 單獨飆升。 一個 D 語言訂單服務,常規監控完全健康,P99 卻從 120ms 基線飆到 350ms;團隊先後排查資料庫、外部依賴、併發配置,全部撲空。OpenResty XRay 火焰圖最終定位到兩個服務自身的內部根因——同樣收錄在下一節的譜系裡。
這兩個案例說明同一件事:平均延遲是"大多數人沒問題"的證明,不是"沒有人有問題"的證明。 如果你的告警只看平均值,你會在使用者投訴之後才知道尾部出了問題。
P99 延遲為甚麼高?根因譜系
排查 P99 之前,先問自己一個問題:尖刺出現的時候,CPU 忙嗎?
這個問題把 P99 的根因沿一條軸線切成兩大類:CPU 在忙著做不該做的事(on-CPU 根因)和 CPU 在等(off-CPU 根因)。這個分類不是學術性的——它直接決定你應該用哪種工具去診斷。
CPU 在忙錯事(on-CPU 根因)
當 CPU 利用率高且 P99 也高時,時間正在被浪費在無用的計算上。以下六類都來自我們在真實生產環境定位過的根因:
① 正則回溯。 巢狀量詞模式(類似 (a+)+$ 這種形態)在特定輸入下會退化為指數級回溯。前文閘道器案例的 300ms 尖刺正是它造成的:一個字串匹配函式在特定輸入下單次執行耗時 244.64ms。
② 每請求重複編譯。 日誌階段的一個正規表示式在每次請求時都被重新編譯,“低開銷"階段反而佔了 26.5% 的 CPU。啟用編譯快取後,這部分 CPU 直接釋放。(同一案例)
③ 構建缺陷。 根因不在程式碼裡,在構建流水線裡:一個案例中基礎映象遺漏 --with-pcre-jit 編譯引數,整個叢集的 PCRE JIT 加速從未生效(完整案例);另一個案例中除錯用的 -O0 編譯選項忘了改回,實測吃掉 10% 的吞吐量(keepalive 案例)。
④ 連線風暴。 upstream 配置塊缺一行 keepalive,每個請求都是一次完整的 TCP 握手和揮手:
upstream backend {
server 10.0.0.2:8080;
# 此處缺少 keepalive 指令,connect()/close() 會在 CPU 火焰圖上撐出兩塊寬幀
}
核心把時間花在建立和拆除連線上,而不是處理資料。補上一行 keepalive 64; 後,吞吐從 6,301 QPS 回升至 21,923 QPS,提升 3.48 倍。
⑤ GC 停頓。 D 語言的保守式 GC 無法區分指標和"像指標的整數”,必須逐字掃描整個堆;高頻分配下形成正反饋——堆越大、掃描越久、停頓越長。火焰圖中 GC 佔了 26.4% 的 CPU。(D 語言案例)
⑥ O(n) 演算法在量級下爆炸。 一個訂單查詢遍歷全量陣列做線性過濾,低負載下毫無異樣,真實流量下佔了 59.4% 的 CPU——程式碼審查抓不到它,只有生產負載下的火焰圖能看到。(同一案例)
定位 on-CPU 根因的方法是取樣 + CPU 火焰圖。火焰圖的讀法與實戰技巧,可參閱我們的火焰圖解讀指南。
CPU 在等待(off-CPU 根因)
如果你的 P99 很高但 CPU 使用率卻不高,時間花在了等待而非計算上:鎖競爭、磁碟/網路 I/O 阻塞、上下游依賴響應慢、排程排隊(執行緒已就緒卻搶不到核)。這類問題在 top 裡看不出病因,CPU 火焰圖上也不會有顯著的寬塊——執行緒離開 CPU 的時間對 on-CPU 取樣是不可見的,取樣再久也採不到"等待"本身。
這類根因對尾延遲的殺傷力在事件驅動的伺服器上尤其大:單個 worker 被阻塞,該 worker 上的所有併發請求同時空等。在一個生產環境的 OpenResty 應用中,程序 99.8% 的 off-CPU 時間被追蹤到 Lua 程式碼裡的 io.popen 及其管道讀取上——同步 I/O 卡住了整個事件迴圈,換用非阻塞 API 後單核吞吐從 126 RPS 提升到 18,537 RPS。另一個案例量化了阻塞的嚴重程度:事件迴圈單次迭代最長被阻塞 75 毫秒——這 75 毫秒直接成為該 worker 上每個併發請求的延遲尖刺。
要診斷這類根因,需要 off-CPU 分析——它測量執行緒離開 CPU 期間的時間和呼叫棧,與 on-CPU 火焰圖正好互補:後者回答"CPU 在忙甚麼”,前者回答"程序在等甚麼"。具體方法、工具與 Perl、Go、Python、Nginx 等多種執行時下的真實根因鏈,可參閱我們的 off-CPU 分析指南。
請求分配不均(配置類根因)
⑦ reuseport 未啟用。 Nginx 監聽埠未開啟 reuseport 時,請求無法均勻分配到各個 worker 程序——部分 worker 打滿、其餘接近空閒,打滿的那部分製造了尾部延遲。這類根因藏在作業系統的請求分配層,top 裡的總 CPU 使用率看不出異常。配置調優後整體效能提升 20–30%。
根因不在你的伺服器上(客戶端)
有時 P99 的元兇根本不在服務端。一個旅遊行業客戶發現部分 API 請求多出 200ms 延遲,OpenResty XRay 的智慧抓包只在慢速 TCP 連線上做針對性捕獲,最終發現延遲來自客戶端——他們的 Android 應用故意在傳送請求頭之後延遲 200ms 才傳送請求體。伺服器端無論怎麼最佳化都不會解決這個問題。
如何診斷高 P99 延遲
回到開頭那個處境:投訴在手、監控全綠、復現失敗。這時你需要的不是更多猜測,而是先弄清楚延遲花在了哪裡。不診斷就最佳化,等於按運氣最佳化。 上面的 D 語言案例就是現成的論據:團隊先後排查資料庫、外部依賴、併發配置,全部撲空——因為根因在服務自身的執行時行為裡,而所有"經驗之談"都指向了外部。
以下是一條經過多個生產案例驗證的診斷路徑:
第一步:確認尾部是真的
在低流量下,P99 可能不可靠——樣本量太小,一兩個偶發慢請求就能把 P99 頂高。確認你觀察到的 P99 抖動是在足夠的請求量下持續出現的,而非統計噪音。
第二步:抓住慢請求本身,而非聚合指標
聚合指標(均值、P95、錯誤率)告訴你"有問題",但不告訴你"問題在哪"。你需要捕獲具體的慢請求:它們命中了哪個 URI?走了哪條程式碼路徑?在哪一步花了最多時間?在客戶端延遲案例中,正是針對慢速連線的選擇性抓包,把 200ms 的延遲精確歸因到客戶端傳送行為,而非服務端處理。
第三步:分流——忙還是等?
這一步決定你接下來用哪種工具:
| 觀察 | 結論 | 該用的工具 |
|---|---|---|
| CPU 高 + P99 高 | 時間花在計算上 | CPU 火焰圖定位熱點函式,參閱火焰圖讀圖實操 |
| CPU 低 + P99 高 | 時間花在等待上 | off-CPU 分析定位阻塞點,參閱 off-CPU 分析指南 |
第四步:在生產環境定位到函式和行級
到這一步,你撞上的是開頭四個盲區裡最硬的那個:根因只在生產環境、真實負載下才出現,你需要函式級甚至程式碼行級的效能資料,但又不能改程式碼、不能加埋點、不能重啟服務——尤其對金融核心系統來說,這是不可逾越的紅線。傳統方法論在這裡走到了盡頭。
OpenResty XRay 透過非侵入式的動態追蹤解決這個問題:直接對執行中的程序取樣,無需修改程式碼、加埋點或重啟服務,自動生成火焰圖並定位到具體函式。前文的兩個案例裡,模糊的"P99 偶爾超標"正是這樣在幾分鐘內被還原成具體函式和具體百分比的。
如何降低 P99 延遲:修你查出來的問題
診斷之後,修復的方向由根因決定——不是按清單逐條嘗試,而是直接修你在火焰圖或抓包中看到的問題。下面的表格彙總了我們在真實案例中定位的根因、對應的修法和實測收益:
| 根因 | 修復方向 | 實測收益 |
|---|---|---|
| ① 正則回溯 + ② 重複編譯 + ③ PCRE JIT 缺失 | 替換低效匹配函式;啟用編譯快取;修正映象構建引數 | 300ms 延遲尖刺消除,CPU 下降約 30%(診斷全過程) |
| ⑤ 保守式 GC + ⑥ O(n) 查詢 | 削減熱路徑記憶體分配;改寫查詢演算法 | P99 從 350ms 降至 95ms,降幅 73%(定位與修復全過程) |
④ keepalive 缺失 + ③ -O0 編譯 | 啟用 upstream keepalive;恢復最佳化編譯 | QPS 從 6,301 回升至 21,923,提升 3.48 倍(完整分析過程) |
⑦ reuseport 未啟用 | 所有監聽埠啟用 reuseport | 整體效能提升 20–30%(調優全過程) |
同步 io.popen 阻塞事件迴圈(off-CPU) | 用非阻塞 API 改寫阻塞呼叫點 | 單核吞吐從 126 RPS 提升至 18,537 RPS(火焰圖定位過程) |
| 客戶端延遲傳送請求體 | 修復客戶端應用的傳送邏輯 | 200ms 延遲消除(抓包分析過程) |
上表只是結論。每個根因是如何在監控全綠的前提下被一步步鎖定、修復後又如何驗證生效的——火焰圖怎麼讀、證據鏈怎麼閉合——完整過程在各案例原文裡,那部分才是可複用的方法。
注意一個關鍵點:每一步最佳化之後必須重新取樣。火焰圖是特定負載下的快照,最佳化會改變系統的熱點分佈——D 語言案例中,GC 壓力降低後,原本被 GC 停頓"抬高"的其他熱點的取樣比例才回歸真實權重。依賴第一張火焰圖規劃所有後續步驟是常見錯誤。
找到根因是昂貴的,修復往往很便宜——一條 keepalive 指令、一個編譯引數、一處快取開關。這正是前面那條診斷路徑存在的意義。
小結
把全文壓成四句話:
- P99 是尾部體驗的標尺,量化的是最慢 1% 請求的真實遭遇。
- 平均值證明不了尾部:延遲分佈有下限無上限,尖刺拉不動均值。
- 尾部的根因沿一條軸線分成兩類:CPU 在忙錯事,或 CPU 在等。
- 診斷先於最佳化:先用火焰圖或抓包看到根因,再修你看到的那一個。
常見問題
甚麼是好的 P99 延遲?
沒有一個適用於所有場景的數字。P99 的目標值取決於你的 SLO 和業務場景:面向使用者的關鍵交易路徑(如支付閘道器)通常要求嚴格得多,內部批處理任務則寬鬆得多。正確的判斷框架是:在當前流量下,P99 是否滿足你對終端使用者體驗或上下游 SLA 的承諾?如果滿足且穩定,它就是"好的"。
為甚麼平均延遲正常但 P99 很高?
因為延遲分佈有下限無上限——少數慢請求拉不動均值,但會把 P99 頂高。前文的金融科技閘道器就是典型:P50 不到 10ms、監控全綠,最慢的 1% 卻超過 300ms。平均值把這類尖刺稀釋成了統計噪音。
為甚麼 P99 延遲高但 CPU 使用率低?
CPU 不高但延遲高,說明時間花在了等待而非計算上——鎖競爭、磁碟/網路 I/O 阻塞、上下游依賴響應慢、排程排隊都屬於這一類。這類時間對 CPU 火焰圖是不可見的:執行緒已經離開了 CPU,on-CPU 取樣自然採不到它。診斷這類問題需要 off-CPU 分析——測量執行緒離開 CPU 期間的時間和呼叫棧。具體方法參閱我們的 off-CPU 分析指南。
P99 延遲和尾延遲有甚麼區別?
尾延遲(tail latency)是一個泛稱,指延遲分佈尾部——最慢的那批請求。P99 是尾延遲的一種量化方式:它取第 99 百分位作為刻畫尾部的標尺。P99.9 和 P99.99 是更極端的尾延遲指標。在實踐中,“P99 延遲"和"尾延遲"經常互換使用,但嚴格來說 P99 是一個確切的統計量,而尾延遲是對分佈形狀的定性描述。
如何在生產環境測量 P99 延遲?
兩種常見方法:一是在應用或閘道器層收集每個請求的響應時間並計算百分位(大多數 APM 和監控系統都支援);二是透過非侵入式的動態追蹤工具(如 OpenResty XRay)直接對執行中的程序取樣,無需改程式碼或加埋點。關鍵區別在於:前者告訴你 P99 是多少,後者還能告訴你 P99 為甚麼高——它把延遲還原為函式級火焰圖,讓你看到慢請求的時間到底花在了哪個函式、哪行程式碼上。
關於 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. 公司的部落格網站 。也歡迎掃碼關注我們的微信公眾號:
翻譯
我們提供了英文版原文和中譯版(本文)。我們也歡迎讀者提供其他語言的翻譯版本,只要是全文翻譯不帶省略,我們都將會考慮採用,非常感謝!


















