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 的计算不涉及平均运算。它是纯粹的排序取位:

  1. 收集一批请求的响应时间
  2. 按从小到大排序
  3. 取第 99% 位置的值

注意:P99 不做任何平均——第 99% 位置的值是多少,它就是多少。这让它不会像平均值那样被中间大量"正常"请求稀释,而是直接反映尾部体验。

P50 vs P95 vs P99:每个百分位告诉你什么

不同的百分位延迟回答不同的问题。下面的对照表帮助你选择正确的指标:

百分位含义回答的问题典型用途
P50(中位数)50% 的请求快于此值典型用户体验如何?日常监控基线、容量规划
P9595% 的请求快于此值大多数用户的最差体验是什么?告警阈值、性能回归检测
P9999% 的请求快于此值最慢的 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、LuaJITGDBSystemTapLLVM、Perl 等,并编写过 60 多个开源软件库。

关注我们

如果您喜欢本文,欢迎关注我们 OpenResty Inc. 公司的博客网站 。也欢迎扫码关注我们的微信公众号:

我们的微信公众号

翻译

我们提供了英文版原文和中译版(本文)。我们也欢迎读者提供其他语言的翻译版本,只要是全文翻译不带省略,我们都将会考虑采用,非常感谢!