30 秒看懂
Bryan Cantrill 探讨如何调试仍在运行但性能异常的生产系统。他回顾数据库服务器配置错误、内存回收、磁盘固件和内核页表隔离等事件,说明微小原因如何在技术栈的远处产生影响。 他主张先理解系统再修改,通过提问与观察不断迭代,并结合静态、动态埋点与可视化。一次 Cassandra 基准测试说明,他的状态图如何揭示工作与空闲交替出现的现象。
- 1系统不崩溃也会失败服务可能仍在运行,却慢到无法接受。没有崩溃或明确错误会让原因更难定位。
- 2微小原因,巨大影响高层的小错误可能使底层工作量成倍增加,而底层延迟也可能在上层放大。
- 3昂贵的意外路由器实验室路由器重启后,配置错误的数据库机器尝试转发数据包,导致基准测试停滞。这个小配置错误花了两周才查明。
- 4调试,而非单纯调优根据经验传闻修改设置可能看似有效,却无法说明原因。Cantrill 呼吁工程师理解系统并收集证据。
- 5聚合掩盖时间信息聚合使海量数据便于处理,却会去掉时间维度,掩盖短暂出现的模式。有些问题需要未聚合的数据。
- 6状态图揭示空闲时段Cassandra 基准测试的平均 I/O 利用率约为 60%,但时间线显示饱和和空闲时段交替出现。问题涉及内存压力和路由表项回收。
要点建议
- 将异常性能视为调试问题,而不是盲目调整设置。
- 一个层次的微小缺陷可能放大整个系统的工作量或延迟。
- 通过连续提问缩小可能性,再确定原因。
- 结合静态和动态埋点,让系统能够回答意料之外的问题。
- 聚合能减少数据量,但可能掩盖解释问题所需的时间模式。
- 可视化有助于发现模式和提出后续问题;工具不能替你思考。
- 坚持和谨慎收集证据比看似轻松的直觉更重要。
按章节阅读整理后的全文
为什么异常性能难以排查
Cantrill 区分崩溃、明确错误和隐性故障。仍在运行却突然变慢的系统尤其难以调查。
对于做这场演讲,我几乎有点迷信。上次谈论故障期间的调试之后,我遇到了一连串故障;批评固件之后,固件 bug 又仿佛找上了门。不过,性能异常的系统在我的职业生涯中反复出现,我想谈谈我们如何调查它们。
最容易调查的故障往往是那些干脆停下来的故障。致命断言会告诉我们程序在哪里发现了无法容忍的状况。未捕获异常或无效指针可能留下核心转储或崩溃转储,其中包含重现事件所需的状态。
从转储中理解一次罕见的生产环境崩溃,再重现并修复它,令人非常满足。不幸的是,许多故障并不致命。系统可能继续运行,却在重要方面出现问题。
明确的错误或日志消息能提供一个起点。更难处理的是隐性故障:软件继续运行,却返回错误答案、泄漏资源或出现其他异常,而且没有说明原因。数值 bug 尤其难以定位。
最棘手的情况之一是系统看起来没有坏,却慢得可怕。它可能几个月甚至几年都运行良好,然后突然在生产环境中变慢。每个人都想知道原因,系统却没有给出直接的解释。
我职业生涯早期,我们在一台巨大的 Sun E10K 系统上运行基准测试,由其他机器向它提供负载。这是一项令人印象深刻的数据库测试,但机器会周期性地停止有效工作几分钟,随后又像云散开一样恢复。
抽象栈与蝴蝶
层层软件抽象让系统强大,也可能将小问题放大成巨大的性能影响。一次数据库基准测试展示了一个远处的配置原因。
我们一次又一次地调查,专家们夜以继日地工作。这个案例让我认识到,在层层抽象中寻找问题有多难。自 1997 年以来,技术栈变得更深了:软件建立在软件之上,一层又一层。
软件很特别,因为它兼具信息和机器的属性。一层正常工作后,就成为下一层的基础。最终我们建成了令人惊叹的大厦,但它如此之深,以至于我们可能看不清脚下有什么。
我曾采访 Arthur Whitney,他用诗歌比喻软件。这次谈话让我思考软件从哪里开始:算法在机器上运行之前算软件吗?处理器微码算软件吗?这些问题提醒我们,技术栈的边界并不像看起来那么简单。
当技术栈性能不佳时,抽象可能以两种方式成为障碍。高层的一个小错误可能在底层倍增为巨大的工作量;反过来,深层的延迟也可能向上传播,造成延迟激增。
设想一个监控脚本因错误在每台机器上每 10 毫秒运行一次,而不是每 10 秒运行一次。一行代码的错误就可能造成巨大的下游工作量。这就是能引发性能飓风的蝴蝶:微小输入带来不成比例的影响。
回到那次基准测试:我们深入数据库机器的网络栈,修复了几个低效算法,但停滞仍在继续。最终,我们发现实验室的一台路由器固件有缺陷,会周期性重启。
数据库机器还被错误地配置为转发数据包。每当路由器消失,这台极其昂贵的数据库服务器就会尝试充当路由器,而且做得很差,数据库也停止有效工作。路由器回来后,基准测试便恢复运行。
世界各地的专家花了两周才找到这个小配置错误。简单的原因和痛苦的调查过程之间的反差,让我深刻认识到症状和原因可能相距多远。
技术栈各处的故障
内存回收、磁盘固件和页表隔离变更展示了异常行为的不同来源。可见症状可能远离最初的触发条件。
另一个案例涉及自适应替换缓存,也就是 ARC。机器仍然能 ping 通,但进程停止了工作。这表明操作系统仍在运行,而内核中的某个东西阻止了有效工作。
空闲内存跌破阈值,触发了内存回收请求和缓存清理任务。与此同时,一个试图缩小 ARC 的操作被这些工作阻塞。ARC 继续增长,目标大小和实际大小之间的差距大到足以在系统等待内存时阻止其他活动。

最终原因很小,但寻找过程十分痛苦。另一起事件涉及使用机械硬盘的 PostgreSQL。Autovacuum 产生读取请求,整个系统似乎停滞,因此我们最初一直怀疑 PostgreSQL 及其工作量。
当我们终于检查单次读取延迟时,发现即使对于机械硬盘来说也慢得离谱。轨迹显示,写入完成时读取仍未完成。经过约 1,700 毫秒的漫长延迟,读取请求才得到处理。

固件开发人员承认硬盘不应该这样工作。硬盘在纯读取或纯写入基准测试中表现良好,但混合读写暴露了饥饿问题。最终的解决办法是换用另一家厂商的硬盘。
应对 Meltdown 和 Spectre 时出现了另一种调查机会:我们知道系统引入了什么变更,因此可以观察其影响。启用内核页表隔离后,一个 PHP 负载的系统时间出现了令人意外的大幅增长。

该缓解措施通过物理分离映射来应对推测执行问题,但它对该负载性能的影响远超我的预期。如果不知道这项变更,要从可见的变慢追溯到这个原因会极其困难。
调试心态
多个瓶颈、变化的负载和不确定的模型增加了性能调试的难度。Cantrill 反对盲目调优和依赖经验传闻。
总体挑战是:我们看到的是飓风,却必须找到蝴蝶。症状可能远离原因,而且原因可能不止一个。一个问题可能真实存在且值得修复,却不是系统持续缓慢的唯一原因。
在可扩展性工作中,这尤其常见。移除一个瓶颈可能立刻暴露另一个,因此吞吐量未必如预期改善,甚至可能看起来更差。只有消除多个约束后,系统才可能最终发挥全部能力。
生产系统是动态的。调查期间,负载可能变化,或者 autovacuum 等操作可能结束,问题也随之消失。这不代表你修好了它。应当回顾证据,问自己学到了什么;寄希望于问题不再出现不是调试策略。
即使找到了真正的问题,也很难预测修改后整个系统会如何表现。尝试建立模型仍然值得,但改变一个子系统可能使下一个主导约束发生变化。
这是调试,而不是调优。调吉他让人联想到机械地调整旋钮直到声音变好;盲目更换垃圾收集器或修改其他设置,不能代替理解系统为何如此运行。
我见过供应商责怪他们自己不久前推荐过的垃圾收集设置。对熟悉的设置进行猜测,既可能无法解决问题,也可能掩盖实际发生了什么变化。目标应该是先理解系统,再做出有依据的修改。
过去故障的故事也不应该成为处理下一次故障的药方。之前的解释本身可能有误,即使过去的原因是对的,也不能证明今天的原因相同。抵制经验传闻,以当前系统的证据为依据。
提问与可观测性
连续提问可以缩小可能的解释范围。静态和动态埋点让观察系统、检验问题成为可能。
调试并不只是提出假设。要在问题和答案之间反复迭代,缩小可能解释的范围。假设很重要,但只有观察排除了足够多的其他可能,让假设足够具体时,它才真正有用。
我的孩子玩“二十问”时,会立刻猜答案是不是獾。即使碰巧猜对,也跳过了有价值的过程。先提出更宽泛的问题,观察系统,再根据答案决定下一步问什么。
Brendan Gregg 的 USE 方法提供了很好的起点:针对每项资源询问利用率、饱和度和错误。它不是能自动找出故障路由器或解决整个问题的万能配方,而是帮助我们构建最初的问题。
可观测性至关重要,否则我们只能猜测、修改,然后从副作用推断结果。固件等一些底层系统难以直接观察。对于自己开发的软件,我们应该将可观测性列为设计要求。
一项修改看似修复了问题,并不能证明解释正确。也许重启进程只是碰巧消除了症状。相关不等于因果;如果没有调查修改前系统在做什么,即使成功也可能什么都学不到。
静态埋点通过日志、计数器等机制,在源代码中提供有意义的信息。软件表现异常时,这能给我们一个起点。沉默的软件会让第一个诊断问题都更难回答。
光有静态埋点还不够。DTrace 等动态埋点,以及不同层次的其他方法,让我们能够针对事先没想到的问题收集数据。调查不断深入时,我们需要两种形式来提出和回答问题。
运营固件等无法修改的系统时,监控必不可少。如果我们同时开发和运营软件,就能更进一步:可观测性扩大了我们能向自己系统提出的问题范围。这种技术联系是开发与运维结合的一部分。
聚合与可视化
聚合不可或缺,却会丢失时间细节。图表、热力图和火焰图可以揭示模式并启发进一步提问。
埋点可能产生海量数据,因此聚合至关重要。DTrace 将聚合作为一等操作,可以先按 CPU 聚合,再合并结果。这样即使提出深入的问题,也不一定会因过多数据移动而干扰性能。
聚合也有代价:它会丢失时间信息。有些模式只有在未聚合的数据中才能看到。调查磁盘固件问题时,我过度依赖聚合计数器太久,因此错过了单次操作中更鲜明的模式。
可视化利用了我们发现模式的能力,包括那些原本不知道要寻找的模式。图表往往不会给出最终答案,而是带来意外发现,促使我们提出一系列新问题。因此绘图是重要的调查技能。
多年来我一直使用 gnuplot,通常还会搭配 Unix 工具。其他人可能更喜欢 R 或电子表格;重要的是熟练到能够将数据可视化。工具不是魔术师:DTrace 可以帮助回答问题,但不能替你思考。

延迟热力图是一种有用的方法。它绘制延迟随时间的变化,用颜色强度表示各个延迟值出现的频率。异常值和反复出现的形状会显现出来,促使我们进一步追问系统当时在做什么。
Brendan Gregg 开发的火焰图会组织聚合后的调用栈样本,展示 CPU 时间花在哪里。系统正在大量消耗 CPU 时,它尤其有用。可视化与特定数据源分离,因此收集到的性能剖析数据可以渲染为交互式 SVG。
状态图与 Cassandra 基准测试
线程和设备状态的时间线揭示了平均值掩盖的工作与空闲交替现象。Cantrill 介绍了基准测试中涉及的内存压力行为。
对于一次 Cassandra 基准测试,我需要另一种视图。该负载本应充分驱动 I/O 子系统,但平均利用率只有约 60%。我想以亚秒级精度了解这个平均值背后的情况。
我制作了状态图:以时间为横轴,每个线程占一行,用颜色表示其状态。这个方法类似于早期的线程调试视图,但这次负载有数百个线程。SVG 可以让结果具备交互性并支持缩放。

首先引人注目的是线程有大量时间没有做有效工作。有些线程离开 CPU,等待 futex 或其他条件。仅凭 futex 等待不能证明存在锁竞争:它也可能表示线程正在等待某个条件,因此需要更多上下文才能解释。
时间线显示,活动区间之后会出现空闲时段。垃圾收集表现为一个短暂阶段,但它并不是我们正在调查的问题。令人沮丧的问题是,为什么基准测试反复停止向存储设备提供工作。
增加负载并没有消除空闲区间,反而让活跃区间的竞争更加激烈。这个例子中的状态图由 DTrace 数据生成,但渲染器接收的数据也可以通过其他系统上的其他方法收集。
放大到约 200 毫秒后,可以看到单次状态转换。在较大尺度上看似一次垃圾收集暂停,实际上随时间错落分布在各线程中。重点仍然不是责怪垃圾收集,而是利用细节提出更好的问题。
设备时间线显示,表面上的 60% 平均值并不是稳定的 60% 利用率,而是在完全忙碌与空闲之间交替。有些空闲间隔只有约半秒,因此如果检查机器的时机不对就很难捕捉。

团队发现系统的内存略微超额使用。应用仍能分配内存,但内核反复要求各子系统回收内存。网络子系统释放了路由表项,尽管它们的内存占用相对于机器容量微不足道。
这些表项被回收后,系统暂时需要重新确定流量应该去哪里。大约半秒后它又能运行,直到内存回收再次触发同样的循环。团队找到了释放吞吐能力的方法,尽管 Cantrill 说他们当时仍在调查一些细节。
状态图并没有神奇地解决这个案例,但它证实了这种振荡,并带来了有价值的后续问题。即使性能改善后,团队仍在追问为什么活动没有持续达到饱和。
另一张状态图显示 PostgreSQL 进程在等待机械硬盘的随机读取。放大后,不同操作的相对时间清晰可见:短暂的 CPU 工作后接着是长得多的 I/O 等待。Cantrill 分享了这个工具的早期版本,供其他人尝试。
靠坚持,而非直觉
Cantrill 回到调试的方法,要求工程师将猜测转化为可检验的问题。找到原因需要坚持和韧性。
性能异常时,要把问题当作调试来处理。把同事自信的猜测转化为问题:我们能向系统问什么?什么数据可以区分这种解释与其他可能性?让可观测性成为系统本身的一部分。
事后的讲述可能让调查看起来毫不费力。实际上,这些案例需要时间,会走弯路,也需要坚持。直觉甚至可能误导我们;只要愿意不断提问和测量,任何人都可以拿出毅力和韧性。
这些系统都是人造的。每条指令的执行都是因为有人安排它执行,因此原则上我们能够理解正在发生什么。在漫长的调查中,坚持这个信念可能很难,但它能帮助我们继续寻找飓风背后的蝴蝶。
同频道的其他文章
1:34:29EN
1:34:36EN → 简体中文
15:48EN → 简体中文
1:20:57EN → 简体中文
1:12:58EN → 简体中文
58:47EN → 简体中文