2026 年 7 月 27 日 * 阅读时长9 分钟最近我在日常工作里尝试验证与 lld 相关的一些性能改进。让人沮丧的是基准测试显示有性能提升可生产环境的实时仪表盘却没体现出来。我有从事 Web 服务工作的背景习惯查看单个时间序列仪表盘有时还会查看几个百分位数的数据。我原本期待能看到明显变化可数据太杂乱没法得出任何结论。后来发现一位同事在评估构建速度改进时也碰到了类似问题。有很多变量会影响构建过程像冷缓存、增量构建、本地构建、远程构建等等而且构建时间会根据系统状态和工作负载的不同而有很大差异。她最终利用累积分布函数CDF来可视化数据这让我眼前一亮。这促使我探索除 CDF 之外的其他几种不同的数据可视化方法以及为什么单张图像或单个统计数据往往不足以说明全部情况。本文将通过一个“合成”数据集展示不同的可视化方法如何呈现同一数据的不同面貌。目的是说服你去“观察”数据而不仅仅是用一个数字来概括它。以下所有内容都来自一个使用固定种子生成的合成数据集。完整脚本可以在[这个代码片段](https://gist.github.com/fzakaria/17c72f0eddc0f10469e008e67e1385cc)中找到。这是一个带有 nix - shell 脚本头的单文件只要你使用 [Nix](https://nixos.org/)就可以精确复现每一幅图。 **注意**为了撰写本文我借助 AI 生成了数据和图表。如果这让你感到不适很抱歉。一次“适得其反”的更新情况是这样的我们运营着一个典型的 Web 服务在一周内逐步推出了一个新的缓存层希望能降低请求延迟。更新完全部署后绘制**平均**延迟的仪表盘显示如下平均延迟从 112 毫秒**上升**到了 122 毫秒。☹️于是我们发布了严重事件通知SEV回滚了更改并撰写了事后分析报告。对吧一个数字四种解读对于 Web 服务来说查看各种百分位数的数据是个好习惯尤其是分布尾部的百分位数如 p95 和 p99。统计指标更新前更新后变化平均值112 毫秒122 毫秒9%p50中位数99 毫秒54 毫秒−46%p95224 毫秒454 毫秒103%p99309 毫秒678 毫秒119%现在问题来了而且问题在于**每个人的观点都有道理**。平均值表明这次更新有轻微的性能下降中位数p50则显示这是一次巨大的成功典型请求的速度**几乎提高了一倍**p99 则表明这是一次严重事件SEV。SEV 是用来描述事件或故障严重程度的常用方式通常按影响程度降序进行数字排名例如 SEV0 表示最严重的情况。最差请求的延迟增加了一倍多。由相同数据计算得出的平均值和中位数却指向了**相反的方向**。工程师通常被教导要以数据为导向但有时很容易挑选出支持自己观点的统计数据。观察数据分布形态处理数据分布时接下来可以做的基本操作是绘制其形态。以下是更新前后的两种延迟分布密度图就是这样。☝️ “更新前”的分布是**一个整齐的单峰**而“更新后”的分布是**双峰**。这就解释了之前的矛盾但要正确可视化并不容易。图形的形状取决于我们选择的平滑参数两个填充区域在重叠处相互干扰而且很难从图中读出**百分位数**。我能看出有两个数据群体但不容易看出中位数的变化情况。你未曾使用的最佳图表累积分布函数CDF能同时为每个百分位数回答一个问题**在 _x_ 毫秒或更短时间内完成的请求占比是多少**CDF 是在单张图表中可视化多个百分位数的极其简便的方法。根据曲线我们可以了解请求延迟在整个数据群体中的分布情况。我发现将“更新前”和“更新后”的 CDF 绘制在同一张图表上进行比较非常有用。这样可以直观地看到各个百分位数的变化从而了解更新对整个数据群体的影响。在我们的案例中对于延迟低于 140 毫秒的请求“更新后”的曲线向左偏移这意味着比更新前有更多的请求能更快完成在 140 毫秒右侧“更新后”的曲线高于“更新前”的曲线这意味着比更新前有更多的请求完成得更慢。两条曲线在约 140 毫秒处相交这是一个转折点超过这个点更新就从有益变为有害。 **提示**两条相交的 CDF 曲线是一种明显的信号表明**没有一个百分位数能概括这种变化**因为影响的正负取决于所查看的百分位数。谁是赢家赢了多少CDF 能告诉我们影响的正负即请求是变快还是变慢了。接下来显而易见的问题是在分布的每个点上变化的幅度有多大。我们可以为每个百分位数 _p_ 绘制更新后的延迟减去更新前的延迟这就是所谓的**偏移函数**。在零线下表示请求变快在零线上表示请求变慢。我们可以直观地看到每个百分位数处变化的幅度。性能下降问题一直存在到目前为止我们只查看了更新前后的两个静态快照。但实际上更新通常不是瞬间完成的。在这个案例中我们用了一周时间逐步推出新的缓存层流量从 0% 逐步增加到 100%。那么每天的数据情况是怎样的呢将每天的分布堆叠起来就得到了一个**脊线图**现在我们可以直观地看到性能下降问题是如何随时间出现的。随着更新的推进我们可以看到主峰快速请求向左移动而右侧出现了第二个峰慢速请求。中位数在下降但慢速请求的数量和延迟在悄然增加。这里的 x 轴是对数坐标轴。延迟大致呈对数正态分布在线性坐标轴上快速请求的峰是一个很高的尖峰而慢速请求几乎看不见对数坐标轴能让两个峰都清晰可见。我们也可以将数据压缩到一个网格中绘制**热力图**。每一列代表一天颜色表示每个延迟区间的流量占比你可以隐约看到一个新的数据群体逐渐出现。如果对整周的数据进行任何汇总计算都会将这七个截然不同的日子的数据混为一谈从而完全掩盖了趋势。双峰分布的原因现在我们已经彻底弄清楚了**发生了什么**接下来的问题是**为什么**。这其实和我日常工作中的情况很相似当时我不得不按二进制文件大小例如 50MiB对数据进行分割才能观察到延迟分布中的双峰现象。在我们的案例中新的缓存层要么从缓存中提供请求响应**命中**要么额外跳转一次到后端获取数据**未命中**。我们可以根据这个属性将“更新后”的请求进行划分并分别绘制 CDF根据缓存结果进行划分后每个数据群体又呈现出单峰分布。我们可以很容易地看到缓存**命中**的请求比旧基线更快因为它们的曲线向左偏移。缓存**未命中**的请求由于额外跳转一次延迟明显增加曲线位于右侧很远的地方。哪些请求未命中缓存原因是什么“有些请求未命中缓存”只是一种现象还不是原因。**哪些**请求未命中缓存为什么每个请求还有一个我还未使用的字段**响应大小**。缓存通常存储小而热门的对象大对象会被淘汰或根本无法存入。我们可以将延迟与响应大小绘制成图并根据缓存命中或未命中为每个点着色。我们还可以在每个坐标轴的边缘添加密度图以查看两个数据群体在每个坐标轴上的分布情况这就是**联合图**我们可以看到两个清晰的数据群体小而快的缓存命中和大而慢的缓存未命中。显然延迟分布的双峰现象是由响应大小分布的双峰现象引起的。现在我们有了可采取的措施提高缓存的最大对象大小或者拆分大的响应。一图胜千数通常单张图表最多只能传达部分信息最坏的情况下还可能产生误导。从多个角度观察同一数据有助于全面了解情况。我特别欣赏 CDF 在单张图表中展示整个数据分布的方式尤其是在比较看似多个数据群体时。 “不要相信任何你自己没有伪造过的统计数据。”——温斯顿·丘吉尔