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

Brendan Gregg 是系统性能领域最有影响力的人物之一,尤其在 Linux 生态中。他写了广泛使用的书籍、打造了实用工具,最重要的是分享了调查真实生产问题的清晰方法。团队采用他的思路,是因为它在高压场景下有效:当延迟飙升、人人都要答案时,你需要一种能把“也许是 X”变成“确定是 Y”的方法,并把戏剧性降到最低。
性能方法论不是单一工具或一个巧妙命令。它是一套可重复的调查流程:优先检查项、如何解读观察到的数据、以及接下来该如何决策。
这种可重复性能减少猜测。与其依赖最有直觉的人(或说话最大声的人),不如遵循一致的流程,流程会:
许多延迟调查在前五分钟就出问题了。人们直接跳到修复:"加 CPU"、"重启服务"、"增大缓存"、"调 GC"、"一定是网络问题"。有时这些操作有帮助——但更常见的是这些操作掩盖了信号、浪费时间或引入新风险。
Gregg 的方法让你推迟“解决方案”,直到你能回答更简单的问题:哪一项资源被饱和?哪个环节出错?变慢的是吞吐、排队,还是单次操作?
本指南帮助你缩小范围、衡量正确的信号,并在优化前确认瓶颈。目标是为生产中的延迟与剖析问题提供结构化工作流,使结果不再依赖运气。
延迟是一个症状:用户等待更久以完成工作。原因通常在别处——CPU 竞争、磁盘或网络等待、锁竞争、垃圾回收、排队或远端依赖延迟。单独度量延迟只能告诉你疼点存在,但不能告诉你起源。
这三类信号相互耦合:
在调优前,为同一时间窗口捕获这三类指标。否则你可能通过丢弃工作或更快失败的方式“修复”延迟。
平均延迟会掩盖用户记住的峰值。一个平均 50 ms 的服务仍可能频繁出现 2 s 的停顿。
跟踪分位数:
还要观察延迟的形态:当 p50 稳定而 p99 上升时,通常表示间歇性停顿(例如锁竞争、I/O 突发、stop-the-world 暂停),而不是整体变慢。
延迟预算是一个简单的记账模型:“如果请求必须在 300 ms 完成,时间可以怎么花?”把它拆成若干桶:
这个预算为首个测量任务定了框架:识别在峰值期间哪个桶增长了,然后调查该区域,而不是盲目调优。
当“系统慢”被描述为问题时,调查容易偏离方向。Gregg 的方法从更早的阶段入手:把问题限定为一个具体、可检验的问题。
在动手前用两句话写清楚:
这能防止你在错误层面做优化——比如当痛点只在某个端点或某个下游时却去调主机 CPU。
挑选一个与投诉匹配的窗口,并尽量包含一个“正常”对比期。
明确你的调查范围:
越精确,后续(USE、RED、剖析)的速度越快,因为你更清楚如果假设成立,哪些数据会发生变化。
记录部署、配置改动、流量变化与基础设施事件——但不要假设因果。把它们写成“如果 X,那么我们会期待 Y”,以便快速验证或否定。
一个小日志能避免团队重复劳动并让接手更顺畅。
Time | Question | Scope | Data checked | Result | Next step
即使只有五行,这也能把紧张的事故变成可重复的流程。
USE 方法(Utilization, Saturation, Errors)是 Gregg 用来快速检查“重要四项”资源——CPU、内存、磁盘(存储)和网络——的速查清单,帮助你停止猜测、开始缩小问题范围。
与其盯着几十个仪表板,不如对每种资源问同样的三个问题:
一致地应用它,会很快给出“压力”在哪里的清单。
对于 CPU,利用率是 CPU 忙碌百分比,饱和度表现为运行队列压力或线程等待运行,错误可能包括容器内的限流或不当的中断行为。
对于 内存,利用率是已用内存,饱和度常表现为换页或频繁 GC,错误包括分配失败或 OOM 事件。
对于 磁盘,利用率是设备忙碌时间,饱和度是队列深度和读写等待时间,错误是 I/O 错误或超时。
对于 网络,利用率是吞吐量,饱和度是丢包/队列/延迟,错误是重传、重置或丢包。
当用户报告变慢时,饱和度信号通常最具揭示力:队列、等待时间与竞争往往比原始利用率更直接关联延迟。
服务级指标(如请求延迟与错误率)告诉你影响。USE 告诉你下一步该往哪儿看,识别是哪个资源在承压。
一个实用循环是:
RED 方法让你在钻入主机图表前保持以用户体验为锚点。
RED 防止你去追逐那些对用户没有影响的“有趣”系统指标。它强制建立更紧的循环:哪个端点慢、影响哪些用户、从什么时候开始? 如果只有单路由的 Duration 激增而总体 CPU 平稳,你已经有了更明确的起点。
一个有用的习惯是:把 RED 按服务和关键端点细分(或按关键 RPC 方法),以便轻松区分广泛降级和局部回归。
RED 告诉你痛点在哪,USE 帮你测试哪个资源负责。
示例:
保持布局聚焦:
如果你想要一致的事故工作流,把这一节与 /blog/use-method-overview 中的 USE 清单配对,这样你可以更少折腾地从“用户感受到影响”走到“哪个资源是瓶颈”。
性能调查很容易在分钟内冒出数十张图表和无数假设。Gregg 的思路是保持窄口径:你的工作不是“收集更多数据”,而是提出下一个最能快速排除不确定性的疑问。
大多数延迟问题由单一或少数成本主导:一把热点锁、一个慢依赖、一个过载的磁盘、一个 GC 暂停模式。优先寻找主导成本,因为在五处各削减 5% 很少能显著改善用户可感知的延迟。
一个实用测试是:“什么能解释我们所见延迟变化的大头?”如果某个假设只能解释很小一部分,则优先级较低。
当你回答“用户是否受影响?”时用自上而下。从端点(RED 风格信号)出发:延迟、吞吐、错误。这能避免你去优化并非关键路径的东西。
当主机明显异常时用自下而上(USE 风格症状):CPU 饱和、内存失控、I/O 等。如果某台节点被顶满,你盯着端点分位数也会摸不着头脑。
当告警触发,选一条分支并坚持到确认或否定为止:
先限制在一小组起始信号,然后只有在某项移动时再下钻。如果你需要清单保持专注,把步骤链接到像 /blog/performance-incident-workflow 的运行手册,让每个新增指标都有目的:回答一个特定问题。
生产剖析看起来有风险,因为它接触的是线上系统——但它通常是把争论变成证据的最快方式。日志与仪表盘能告诉你出了慢,剖析能告诉你时间花在哪儿:哪个函数占时、哪个线程在等待、哪个代码路径在故障期间占主导。
剖析是把时间做成预算的工具。与其争论“是数据库还是 GC”,不如得到诸如“45% 的 CPU 样本花在 JSON 解析上”或“大多数请求被 mutex 阻塞”这样的证据。那会把下一步缩小到一两个具体修复。
每种剖析回答不同的问题。低 CPU 但高延迟通常指向 off-CPU 或锁等待,而非 CPU 热点。
许多团队从按需开始,建立信任后再迁移到常开。
在生产中安全地做剖析就是控制成本。优先使用采样(而不是跟踪每个事件),保持捕获窗口短(例如 10–30 秒),并先在金丝雀上衡量开销。如果不确定,从低频采样开始,只有在信号太噪时再加频率。
火焰图可视化在某个剖析窗口内采样到的时间去向。每个“方块”代表一个函数(或栈帧),每条栈展示执行如何到达该函数。它们非常适合快速发现模式——但并不会自动告诉你“错误就在这里”。
火焰图通常表示on-CPU 样本:程序实际在 CPU 核心上运行的时间。它能突出 CPU 密集的代码路径、低效解析、过度序列化或确实烧 CPU 的热点。
它并不直接展示磁盘、网络、调度器延迟或互斥等待(那是 off-CPU 时间,需要不同剖析)。它也不会在没有关联具体场景的情况下证明对用户可见延迟的因果关系。
最宽的方块很容易被责怪,但要问:这是能改变的热点,还是仅仅因为上游问题而在 malloc、GC 或日志中耗时?还要注意缺失上下文(JIT、内联、符号),这些会让某个方块看起来像罪魁祸首而它只是传话的媒介。
把火焰图当作对限定问题的答案:哪个端点、哪个时间窗口、哪些主机、发生了什么变化。对比“之前 vs 之后”(或“健康 vs 降级”)的火焰图,针对同一路径,这样能避开剖析噪声。
当延迟飙升时,很多团队先看 CPU%。这可以理解——但它常常指向错误方向。一个服务即使“只有 20% CPU”,如果线程大部分时间不在运行,也会变得痛苦地慢。
CPU% 回答“处理器有多忙?”但并不回答“我的请求时间到底花到哪儿了?”请求在等待、被阻塞或被调度器挂起时也会消耗墙钟时间。
一个关键思想:请求的墙钟时间包括 on-CPU 工作和 off-CPU 等待。
off-CPU 时间通常隐藏在依赖与竞争后面:
一些信号常与 off-CPU 瓶颈相关:
这些症状告诉你“我们在等”,但并不说明在等什么。
off-CPU 剖析会把时间归因到你未运行的原因:在系统调用中阻塞、等待锁、sleep 或被去调度。这对延迟工作非常有用,因为它把模糊的慢变成可操作的分类:“在 mutex X 上阻塞”、“等待来自磁盘的 read()”或“在 connect() 阻塞等待上游”。一旦能说出在等什么,就能度量它、验证它并修复它。
性能工作常在同一个时刻失败:有人看到可疑指标便断定“问题在这里”并开始调优。Gregg 的方法要求你放慢脚步,用证据证明什么在限制系统,然后再改动任何东西。
瓶颈是当前限制吞吐或驱动延迟的资源或组件。如果你缓解它,用户会看到改进。
热点是时间被花的地方(例如出现在剖析中的函数)。热点可能是真正的瓶颈,也可能只是不会影响慢路径的忙碌工作。
噪声是看起来有意义但并非如此的东西:后台任务、一次性峰值、采样伪影、缓存效应或与用户可感知问题无关的“top talkers”。
先抓取清晰的前快照:用户可见的症状(延迟或错误率)和领先候选信号(CPU 饱和、队列深度、磁盘 I/O、锁竞争等)。然后做一个受控改动,该改动应只影响你怀疑的原因。
因果验证示例:
相关只是暗示,不是结论。如果“延迟上升时 CPU 也上升”,通过改变 CPU 可用性或减少 CPU 工作量来验证延迟是否随之变化。
写明:测得了什么、做了什么具体改动、前后结果、观察到的改进。这会把一次性胜利变成可复用的演练,并防止事后被“直觉”改写历史。
性能事故感觉紧急,这正是猜测容易溜进来的时候。一个轻量且可重复的工作流能帮助你从“某处慢”走到“我们知道发生了什么”,并避免折腾。
检测: 对用户可见的延迟与错误率告警,而不是仅仅对 CPU。对 p95/p99 延迟在持续窗口超过阈值时进行告警。
分级: 立刻回答三问:谁的哪个东西慢、何时开始、谁受影响? 如果你不能说清范围(服务、端点、区域、用户群),你还没准备好去优化。
测量: 收集能缩小瓶颈的证据。优先时限性的抓取(如 60–180 秒),以便比较“坏”与“好”。
修复: 每次只改一件事,然后重新测量相同信号以确认改进并排除安慰剂效应。
保持共享仪表板供事故期间所有人使用。让它乏味且一致:
目标不是把一切都画出来,而是缩短“到达首个事实”的时间。
给最重要的端点(结账、登录、搜索)打好埋点,而不是对每个端点都做同样工作。为每个端点约定:期望的 p95、最大错误率与关键依赖(DB、缓存、第三方)。
在下次故障前,约定一个抓取套件:
把它记录在简短的运行手册(例如 /runbooks/latency),包含谁能运行抓取与工件存放位置。
Gregg 的方法本质上是关于受控变更与快速验证。如果团队用 Koder.ai(一个用于生成与迭代 web、后端与移动应用的聊天驱动平台)构建服务,两个功能与该思路契合:
即便在事故中你不是在写新代码,这些习惯——小改动、可度量结果、快速可逆——正是 Gregg 推崇的。
10:15am,仪表板显示 API 的 p99 从 ~120ms 升到 ~900ms。错误率平稳,但客户反馈请求“变慢”。
从服务先行:Rate、Errors、Duration。
按端点切片 Duration,发现一条路由占据 p99:POST /checkout。Rate 增加 2×,错误率正常,但当并发上升时 Duration 特别飙升,指向排队或竞争,而非彻底失败。
接着检查延迟是计算时间还是等待时间:比较应用“处理器耗时”与总请求时间(或若有追踪比较上游/下游跨度)。处理器耗时很低、总耗时很高——请求在等待。
清点可能的瓶颈:CPU、内存、磁盘、网络的 利用率、饱和度、错误。
CPU 利用率只有 ~35%,但 CPU 运行队列与上下文切换上升。磁盘与网络看起来正常。这种不匹配(低 CPU%,高等待)是经典信号:线程不是在烧 CPU,而是在被阻塞。
在峰值期抓取的 off-CPU 剖析显示大量时间耗在一个围绕共享“promotion validation”缓存的互斥锁上。
将全局锁替换为按键级锁(或引入无锁的读路径),部署后观测到 p99 在 Rate 保持高位时回到基线。
事后清单: