2 分钟

Brendan Gregg 的延迟与性能剖析方法

学习 Brendan Gregg 的实用方法(USE、RED、火焰图等),用数据而非猜测调查延迟和生产瓶颈。

Brendan Gregg 的延迟与性能剖析方法

为什么 Brendan Gregg 的方法能减少猜测

Brendan Gregg 是系统性能领域最有影响力的人物之一,尤其在 Linux 生态中。他写了广泛使用的书籍、打造了实用工具,最重要的是分享了调查真实生产问题的清晰方法。团队采用他的思路,是因为它在高压场景下有效:当延迟飙升、人人都要答案时,你需要一种能把“也许是 X”变成“确定是 Y”的方法,并把戏剧性降到最低。

什么是真正的“性能方法论”

性能方法论不是单一工具或一个巧妙命令。它是一套可重复的调查流程:优先检查项、如何解读观察到的数据、以及接下来该如何决策。

这种可重复性能减少猜测。与其依赖最有直觉的人(或说话最大声的人),不如遵循一致的流程,流程会:

  • 将问题缩小到具体资源、服务或代码路径
  • 在与故障相同的时间窗口内衡量发生的情况
  • 在做改动前用证据确认瓶颈

常见失败模式:未测量就修复

许多延迟调查在前五分钟就出问题了。人们直接跳到修复:"加 CPU"、"重启服务"、"增大缓存"、"调 GC"、"一定是网络问题"。有时这些操作有帮助——但更常见的是这些操作掩盖了信号、浪费时间或引入新风险。

Gregg 的方法让你推迟“解决方案”,直到你能回答更简单的问题:哪一项资源被饱和?哪个环节出错?变慢的是吞吐、排队,还是单次操作?

本指南能帮助你做什么

本指南帮助你缩小范围、衡量正确的信号,并在优化前确认瓶颈。目标是为生产中的延迟与剖析问题提供结构化工作流,使结果不再依赖运气。

延迟基础:在调优前要度量的内容

延迟是一个症状:用户等待更久以完成工作。原因通常在别处——CPU 竞争、磁盘或网络等待、锁竞争、垃圾回收、排队或远端依赖延迟。单独度量延迟只能告诉你疼点存在,但不能告诉你起源。

吞吐、延迟与错误常常一起变化

这三类信号相互耦合:

  • 吞吐(requests/second)上升会增加排队,从而提高延迟
  • 错误可能降低观测到的延迟(快速失败),也可能增加延迟(重试与超时)。
  • 限制吞吐(限速、背压)能改善尾延迟,但会让成功请求变少。

在调优前,为同一时间窗口捕获这三类指标。否则你可能通过丢弃工作或更快失败的方式“修复”延迟。

不要信任平均值:关注分位数与尾延迟

平均延迟会掩盖用户记住的峰值。一个平均 50 ms 的服务仍可能频繁出现 2 s 的停顿。

跟踪分位数

  • p50:典型用户体验
  • p95/p99:尾延迟(大多数事故痛点所在)

还要观察延迟的形态:当 p50 稳定而 p99 上升时,通常表示间歇性停顿(例如锁竞争、I/O 突发、stop-the-world 暂停),而不是整体变慢。

延迟预算:时间可以如何分配

延迟预算是一个简单的记账模型:“如果请求必须在 300 ms 完成,时间可以怎么花?”把它拆成若干桶:

  • 在你服务内的时间(计算 + 等待)
  • 在下游服务的时间
  • 在数据库/缓存的时间
  • 网络传输与 TLS 时间
  • 排队时间(线程池、连接池、负载均衡器)

这个预算为首个测量任务定了框架:识别在峰值期间哪个桶增长了,然后调查区域,而不是盲目调优。

从明确的问题和范围开始

当“系统慢”被描述为问题时,调查容易偏离方向。Gregg 的方法从更早的阶段入手:把问题限定为一个具体、可检验的问题。

定义“慢”是什么意思(对谁而言)

在动手前用两句话写清楚:

  • 哪个东西慢?(页面加载、API 接口、批处理作业、登录、结账、某条 SQL 查询)
  • 在哪儿观察到慢?(客户浏览器、移动端、某个区域、某个 pod、某台主机、内部服务)

这能防止你在错误层面做优化——比如当痛点只在某个端点或某个下游时却去调主机 CPU。

选择时间窗口和范围

挑选一个与投诉匹配的窗口,并尽量包含一个“正常”对比期。

明确你的调查范围:

  • 主机 vs 服务 vs 端点:一台 Kubernetes 节点和一个 API 路由是不同的范围。
  • 哪一片流量:按区域、客户等级、仅出错请求或全部请求切片。
  • 是什么信号触发报告:p95 延迟、超时、队列深度或用户端时序。

越精确,后续(USE、RED、剖析)的速度越快,因为你更清楚如果假设成立,哪些数据会发生变化。

把最近的变更当作假设,而非结论

记录部署、配置改动、流量变化与基础设施事件——但不要假设因果。把它们写成“如果 X,那么我们会期待 Y”,以便快速验证或否定。

保持轻量的调查日志

一个小日志能避免团队重复劳动并让接手更顺畅。

Time | Question | Scope | Data checked | Result | Next step

即使只有五行,这也能把紧张的事故变成可重复的流程。

USE 方法:快速扫描资源瓶颈的清单

USE 方法(Utilization, Saturation, Errors)是 Gregg 用来快速检查“重要四项”资源——CPU、内存、磁盘(存储)和网络——的速查清单,帮助你停止猜测、开始缩小问题范围。

它是什么:按资源的三项检查

与其盯着几十个仪表板,不如对每种资源问同样的三个问题:

  • 利用率(Utilization):现在有多忙?
  • 饱和度(Saturation):工作是否在堆积(队列、等待时间),即使利用率没到极限?
  • 错误(Errors):是否有失败或重试以致产生延迟?

一致地应用它,会很快给出“压力”在哪里的清单。

如何在实践中应用

对于 CPU,利用率是 CPU 忙碌百分比,饱和度表现为运行队列压力或线程等待运行,错误可能包括容器内的限流或不当的中断行为。

对于 内存,利用率是已用内存,饱和度常表现为换页或频繁 GC,错误包括分配失败或 OOM 事件。

对于 磁盘,利用率是设备忙碌时间,饱和度是队列深度和读写等待时间,错误是 I/O 错误或超时。

对于 网络,利用率是吞吐量,饱和度是丢包/队列/延迟,错误是重传、重置或丢包。

延迟事故中最有用的信号

当用户报告变慢时,饱和度信号通常最具揭示力:队列、等待时间与竞争往往比原始利用率更直接关联延迟。

USE 与服务级指标互补(而非替代)

服务级指标(如请求延迟与错误率)告诉你影响。USE 告诉你下一步该往哪儿看,识别是哪个资源在承压。

一个实用循环是:

  1. 确认用户影响(时长/错误)
  2. 运行 USE 清单
  3. 用更深层工具(剖析、追踪、内核统计)放大可疑资源

RED 方法:以服务为先的信号指向影响面

RED 方法让你在钻入主机图表前保持以用户体验为锚点。

  • Rate:服务或端点的请求速率
  • Errors:失败请求数(以及对你应用来说“失败”如何定义)
  • Duration:成功请求的耗时(按分位数而非均值)

为什么 RED 帮你优先排序

RED 防止你去追逐那些对用户没有影响的“有趣”系统指标。它强制建立更紧的循环:哪个端点慢、影响哪些用户、从什么时候开始? 如果只有单路由的 Duration 激增而总体 CPU 平稳,你已经有了更明确的起点。

一个有用的习惯是:把 RED 按服务和关键端点细分(或按关键 RPC 方法),以便轻松区分广泛降级和局部回归。

将 RED 症状映射到 USE 检查

RED 告诉你痛点在哪,USE 帮你测试哪个资源负责

示例:

  • Duration 上升 + Rate 稳定 → 检查饱和/排队:CPU 运行队列、存储延迟、数据库连接池。
  • Errors 上升 + Duration 上升 → 检查超时与重试:下游过载、线程池、网络丢包。
  • Rate 上升 + Duration 上升 → 检查容量限制:CPU 利用、负载均衡行为、自动扩缩容延迟。

一个最小化的“发生了什么变化?”仪表板

保持布局聚焦:

  1. RED 概览:服务的 Rate、Errors 与 p50/p95/p99 Duration。
  2. Top endpoints:按流量或最差 p95 排序的端点 RED 指标。
  3. 依赖项:主要下游(DB、缓存、外部 API)的 RED 风格面板。
  4. 一个关联行:一组系统指标(CPU、内存压力、磁盘 I/O 延迟、网络重传),便于从服务视图快速跳到根因测试。

如果你想要一致的事故工作流,把这一节与 /blog/use-method-overview 中的 USE 清单配对,这样你可以更少折腾地从“用户感受到影响”走到“哪个资源是瓶颈”。

优先级:选择下一个最有价值的问题

几分钟内绘制热点路径
在聊天中勾勒热点请求路径,尽早讨论锁与队列的选择。

性能调查很容易在分钟内冒出数十张图表和无数假设。Gregg 的思路是保持窄口径:你的工作不是“收集更多数据”,而是提出下一个最能快速排除不确定性的疑问。

对瓶颈的 80/20 规则

大多数延迟问题由单一或少数成本主导:一把热点锁、一个慢依赖、一个过载的磁盘、一个 GC 暂停模式。优先寻找主导成本,因为在五处各削减 5% 很少能显著改善用户可感知的延迟。

一个实用测试是:“什么能解释我们所见延迟变化的大头?”如果某个假设只能解释很小一部分,则优先级较低。

自上而下 vs 自下而上:从哪开始

当你回答“用户是否受影响?”时用自上而下。从端点(RED 风格信号)出发:延迟、吞吐、错误。这能避免你去优化并非关键路径的东西。

当主机明显异常时用自下而上(USE 风格症状):CPU 饱和、内存失控、I/O 等。如果某台节点被顶满,你盯着端点分位数也会摸不着头脑。

防止折腾的简单决策树

当告警触发,选一条分支并坚持到确认或否定为止:

  • 延迟飙升 + 错误飙升 → “这是依赖还是容量事件?”(超时、连接池耗尽、下游 5xx)
  • 延迟飙升 + CPU 飙升 → “CPU 是在做有用工作还是被卡住?”(on-CPU vs off-CPU)
  • 延迟飙升 + 高 I/O wait → “哪个设备或文件系统队列在积压?”
  • 延迟飙升但资源无明显波动 → “时间花在哪儿了?”(锁、调度器、网络、远端调用)

避免指标过载,保持系统性

先限制在一小组起始信号,然后只有在某项移动时再下钻。如果你需要清单保持专注,把步骤链接到像 /blog/performance-incident-workflow 的运行手册,让每个新增指标都有目的:回答一个特定问题。

在不让系统下线的情况下在生产中做剖析

生产剖析看起来有风险,因为它接触的是线上系统——但它通常是把争论变成证据的最快方式。日志与仪表盘能告诉你出了慢,剖析能告诉你时间花在哪儿:哪个函数占时、哪个线程在等待、哪个代码路径在故障期间占主导。

剖析真正回答的问题

剖析是把时间做成预算的工具。与其争论“是数据库还是 GC”,不如得到诸如“45% 的 CPU 样本花在 JSON 解析上”或“大多数请求被 mutex 阻塞”这样的证据。那会把下一步缩小到一两个具体修复。

常见的生产可用剖析类型

  • CPU 剖析:显示哪些代码在 on-CPU 时执行。
  • Off-CPU(等待)剖析:显示线程被阻塞的地方(I/O 等待、调度延迟、sleep、网络、磁盘)。
  • 锁剖析:显示竞争——等待锁、互斥量与读写闩锁的时间损失。

每种剖析回答不同的问题。低 CPU 但高延迟通常指向 off-CPU 或锁等待,而非 CPU 热点。

常开与按需

  • 常开剖析(持续、低开销)有助于解决“凌晨 3 点发生但没人知道为什么”的谜题,因为你可以回溯查看。
  • 按需剖析 是在峰值期间的有针对性抓取。它更容易采用,但你必须准备好快速触发它。

许多团队从按需开始,建立信任后再迁移到常开。

安全性:开销、采样与短窗口

在生产中安全地做剖析就是控制成本。优先使用采样(而不是跟踪每个事件),保持捕获窗口短(例如 10–30 秒),并先在金丝雀上衡量开销。如果不确定,从低频采样开始,只有在信号太噪时再加频率。

火焰图:如何读图并避免错误结论

快速测试性能变更
部署测试版本,在触及生产前测量 p95 和 p99。

火焰图可视化在某个剖析窗口内采样到的时间去向。每个“方块”代表一个函数(或栈帧),每条栈展示执行如何到达该函数。它们非常适合快速发现模式——但并不会自动告诉你“错误就在这里”。

火焰图展示什么(和不展示什么)

火焰图通常表示on-CPU 样本:程序实际在 CPU 核心上运行的时间。它能突出 CPU 密集的代码路径、低效解析、过度序列化或确实烧 CPU 的热点。

它并直接展示磁盘、网络、调度器延迟或互斥等待(那是 off-CPU 时间,需要不同剖析)。它也不会在没有关联具体场景的情况下证明对用户可见延迟的因果关系。

宽度与栈深的含义

  • 宽度:该栈帧在样本中出现的频率。更宽通常表示“更多 CPU 时间”,但仅限于所选的时间窗口。
  • 栈深:调用深度。深栈并非必然有问题,关键在于哪些路径占主导以及它们是否与关心的工作匹配。

常见陷阱

最宽的方块很容易被责怪,但要问:这是能改变的热点,还是仅仅因为上游问题而在 malloc、GC 或日志中耗时?还要注意缺失上下文(JIT、内联、符号),这些会让某个方块看起来像罪魁祸首而它只是传话的媒介。

将火焰图与明确的问题配对

把火焰图当作对限定问题的答案:哪个端点哪个时间窗口哪些主机发生了什么变化。对比“之前 vs 之后”(或“健康 vs 降级”)的火焰图,针对同一路径,这样能避开剖析噪声。

Off-CPU 时间:被忽视的延迟源

当延迟飙升时,很多团队先看 CPU%。这可以理解——但它常常指向错误方向。一个服务即使“只有 20% CPU”,如果线程大部分时间不在运行,也会变得痛苦地慢。

为什么单看 CPU% 会误导

CPU% 回答“处理器有多忙?”但并不回答“我的请求时间到底花到哪儿了?”请求在等待、被阻塞或被调度器挂起时也会消耗墙钟时间。

一个关键思想:请求的墙钟时间包括 on-CPU 工作和 off-CPU 等待。

常见的 off-CPU 罪魁祸首

off-CPU 时间通常隐藏在依赖与竞争后面:

  • 磁盘 I/O:同步读/写、fsync、慢存储、页面缓存未命中。
  • 网络等待:DNS 查找、TCP 重传、上游服务过载。
  • 锁与互斥竞争:线程在锁、读写锁、分配器竞争上阻塞。
  • 排队:在线程池、连接池或内部工作队列中等待。

值得关注的症状

一些信号常与 off-CPU 瓶颈相关:

  • 上升的 队列时间(请求在开始执行前等待)
  • 增加的 可运行线程(CPU 竞争加剧)
  • 升高的 I/O wait 与更长的磁盘/网络延迟

这些症状告诉你“我们在等”,但并不说明在等什么。

off-CPU 剖析如何展示“时间被花在哪儿”

off-CPU 剖析会把时间归因到你未运行的原因:在系统调用中阻塞、等待锁、sleep 或被去调度。这对延迟工作非常有用,因为它把模糊的慢变成可操作的分类:“在 mutex X 上阻塞”、“等待来自磁盘的 read()”或“在 connect() 阻塞等待上游”。一旦能说出在等什么,就能度量它、验证它并修复它。

用证据而非直觉来确认瓶颈

性能工作常在同一个时刻失败:有人看到可疑指标便断定“问题在这里”并开始调优。Gregg 的方法要求你放慢脚步,用证据证明什么在限制系统,然后再改动任何东西。

瓶颈、热点与噪声

瓶颈是当前限制吞吐或驱动延迟的资源或组件。如果你缓解它,用户会看到改进。

热点是时间被花的地方(例如出现在剖析中的函数)。热点可能是真正的瓶颈,也可能只是不会影响慢路径的忙碌工作。

噪声是看起来有意义但并非如此的东西:后台任务、一次性峰值、采样伪影、缓存效应或与用户可感知问题无关的“top talkers”。

用对比与受控变更来证明

先抓取清晰的快照:用户可见的症状(延迟或错误率)和领先候选信号(CPU 饱和、队列深度、磁盘 I/O、锁竞争等)。然后做一个受控改动,该改动应只影响你怀疑的原因。

因果验证示例:

  • 给怀疑的资源增加容量(多一个 worker、更多 CPU 权重、更大的连接池),看延迟是否改善。
  • 暂时减少负载(限制嘈杂端点、回放更小工作量),看怀疑的约束是否放松。

相关只是暗示,不是结论。如果“延迟上升时 CPU 也上升”,通过改变 CPU 可用性或减少 CPU 工作量来验证延迟是否随之变化。

记录你所证明的内容

写明:测得了什么、做了什么具体改动、前后结果、观察到的改进。这会把一次性胜利变成可复用的演练,并防止事后被“直觉”改写历史。

为性能事故建立可重复工作流

一次只运行一项变更
使用快照让单变量实验在排查瓶颈时保持安全。

性能事故感觉紧急,这正是猜测容易溜进来的时候。一个轻量且可重复的工作流能帮助你从“某处慢”走到“我们知道发生了什么”,并避免折腾。

事故循环:检测 → 分级 → 测量 → 修复

检测: 对用户可见的延迟与错误率告警,而不是仅仅对 CPU。对 p95/p99 延迟在持续窗口超过阈值时进行告警。

分级: 立刻回答三问:谁的哪个东西慢、何时开始、谁受影响? 如果你不能说清范围(服务、端点、区域、用户群),你还没准备好去优化。

测量: 收集能缩小瓶颈的证据。优先时限性的抓取(如 60–180 秒),以便比较“坏”与“好”。

修复: 每次只改一件事,然后重新测量相同信号以确认改进并排除安慰剂效应。

标准化一小组图表

保持共享仪表板供事故期间所有人使用。让它乏味且一致:

  • 延迟:p50 / p95 / p99(按关键端点)
  • RED 信号:Rate、Errors、Duration(以服务为先)
  • 若干 USE 指标:CPU、磁盘、网络的利用率、饱和度、错误

目标不是把一切都画出来,而是缩短“到达首个事实”的时间。

为关键端点定义“黄金信号”

给最重要的端点(结账、登录、搜索)打好埋点,而不是对每个端点都做同样工作。为每个端点约定:期望的 p95、最大错误率与关键依赖(DB、缓存、第三方)。

决定在事故期间捕获什么

在下次故障前,约定一个抓取套件:

  • 剖析(CPU 与 off-CPU),以及火焰图
  • 慢端点的追踪
  • 错误/超时日志(抽样)

把它记录在简短的运行手册(例如 /runbooks/latency),包含谁能运行抓取与工件存放位置。

Koder.ai 在 Gregg 风格工作流中的角色

Gregg 的方法本质上是关于受控变更与快速验证。如果团队用 Koder.ai(一个用于生成与迭代 web、后端与移动应用的聊天驱动平台)构建服务,两个功能与该思路契合:

  • 规划模式(Planning Mode) 帮你把“可能是 X”变成明确的假设和可测试的小改动集,然后再触碰生产。
  • 快照与回滚 支持安全的单变量实验:做一处改动,重新测量 RED/USE 信号,若证据显示无效则快速回滚。

即便在事故中你不是在写新代码,这些习惯——小改动、可度量结果、快速可逆——正是 Gregg 推崇的。

一个实用演练:从延迟飙升到验证修复

场景:峰值流量时 p99 跳高

10:15am,仪表板显示 API 的 p99 从 ~120ms 升到 ~900ms。错误率平稳,但客户反馈请求“变慢”。

步骤 1 — 先用 RED 找用户影响

从服务先行:Rate、Errors、Duration

按端点切片 Duration,发现一条路由占据 p99:POST /checkout。Rate 增加 2×,错误率正常,但当并发上升时 Duration 特别飙升,指向排队或竞争,而非彻底失败。

接着检查延迟是计算时间还是等待时间:比较应用“处理器耗时”与总请求时间(或若有追踪比较上游/下游跨度)。处理器耗时很低、总耗时很高——请求在等待。

步骤 2 — 对疑似主机应用 USE

清点可能的瓶颈:CPU、内存、磁盘、网络的 利用率、饱和度、错误

CPU 利用率只有 ~35%,但 CPU 运行队列与上下文切换上升。磁盘与网络看起来正常。这种不匹配(低 CPU%,高等待)是经典信号:线程不是在烧 CPU,而是在被阻塞。

步骤 3 — 根据症状选择剖析方式

  • 若 CPU 很高:用 CPU 剖析(on-CPU 火焰图)看时间花在哪儿。
  • 若请求在等待:用 off-CPU 剖析 看线程在哪儿阻塞(锁、I/O、调度)。

在峰值期抓取的 off-CPU 剖析显示大量时间耗在一个围绕共享“promotion validation”缓存的互斥锁上。

步骤 4 — 修复并验证

将全局锁替换为按键级锁(或引入无锁的读路径),部署后观测到 p99 在 Rate 保持高位时回到基线。

事后清单:

  • 记录准确的 RED 症状与缩小到的端点。
  • 保存剖析与时间窗口。
  • 为特定饱和信号(如锁等待 / 运行队列)新增告警。
  • 写下“如果再发生,下一个要问的问题”。

常见问题

我应该如何开始排查延迟峰值?

先从一个明确的症状入手,例如某个端点的 p99 延迟,并确定它发生的时间窗口。寻找原因前,先衡量对用户的影响。

应用延迟高通常由什么导致?

延迟升高意味着用户等待得更久。CPU 争用、锁等待、排队、磁盘 I/O、网络延迟、垃圾回收或缓慢的依赖项都可能导致这种等待。

为什么应该跟踪 p95 和 p99,而不是平均延迟?

平均值会掩盖短暂但令人痛苦的卡顿。用 p50 衡量典型体验,用 p95 或 p99 捕捉事故期间用户会注意到的慢请求。

什么是 RED 方法?

RED 指速率、错误和持续时间。用它找出影响用户的服务或端点、它处理的流量,以及请求是否失败或变慢。

什么是 USE 方法?

USE 指利用率、饱和度和错误。检查 CPU、内存、磁盘和网络的这三个信号,找出处于压力下的资源。

RED 和 USE 如何配合使用?

先用 RED 确定受影响的路由和时间窗口。接着运行 USE,检验 CPU、内存、存储或网络压力是否能解释变慢,然后分析可疑区域。

什么时候该使用 CPU 性能分析,什么时候该使用 off-CPU 性能分析?

当 CPU 使用率上升、需要查看哪些代码运行最频繁时,使用 CPU 性能分析。当 CPU 使用率不高但请求仍在等待时,使用 off-CPU 性能分析,尤其适合排查锁、I/O、队列或远程调用。

火焰图中宽的方块代表什么?

宽度表示函数在采集期间的样本中出现的频率。宽的栈帧可能消耗大量 CPU 时间,但仍需确认它是否影响慢请求路径。

如何证明我找到了真正的瓶颈?

为同一端点和时间窗口采集变更前后的指标。做出一项有针对性的改动,然后确认延迟有所改善,同时吞吐量和错误率仍处于可接受范围。

Koder.ai 如何支持性能实验?

规划模式可以把怀疑的原因转化为一项可测试的小改动,而快照和回滚让这个实验更容易撤销。每次改动后,都重新检查 RED 和 USE 信号。

Related posts