这是一份伪转录稿(实际措辞经过修改,比100%忠实转录更易读),内容来自我一两年前在Twitter做的一场简短演讲,主题是关于我们使用延迟指标时容易陷入的误区(根据沟通要求,实际服务名称已匿名化处理)。自那次演讲以来,基础设施方面已取得显著进展,情况比当时展示的要好得多。但我认为这些内容仍然具有参考价值,因为根据我与同行公司人士的交流,许多团队也面临着类似的问题。

在Twitter,我们经常使用尾部延迟指标。最常见的情况是,服务所有者希望获取其服务的集群范围或Twitter全局延迟数据。不幸的是,由于延迟测量设置中的一些历史遗留问题,服务所有者常用的数据与我们实际希望测量的数值存在差异:

  • 不透明、未经过检测的延迟
  • 缺乏集群范围的聚合能力
  • 分钟级分辨率

不透明、未经过检测的延迟

当我们查看大多数服务的仪表盘时,显示并用于告警的延迟指标通常来自服务自身运行的服务器。有些服务由资深SRE设置了仪表盘,他们曾因不可见延迟吃过亏,因此这些仪表盘也会包含服务调用方观察到的客户端延迟。我想讨论这种设置存在的三个问题。

为了便于本次演讲,我们可以将客户端请求视为经过以下管道:从客户端“用户”代码将请求传递给我们的RPC层Finagle(https://twitter.github.io/finagle/),到客户端用户代码收到响应之前(按照Finagle当前处理请求的方式,一旦请求被移交给我们使用的网络库netty,我们就无法获取特定请求的时间戳)。

客户端 netty -> 客户端 Linux -> 网络 -> 服务器 Linux -> 服务器 netty -> 服务器“用户代码” -> 服务器 netty -> 服务器 Linux -> 网络 -> 客户端 Linux -> 客户端 netty

正如我们之前在[一份量化CFS带宽控制节流影响以及过度使用大型线程池导致节流的内部文档]1中所见,netty内部及下层经常出现大量排队现象,这会产生连锁效应,导致服务被内核节流,进而引发大量不透明延迟——尤其是在高负载情况下,而这正是我们最希望仪表盘显示正确延迟数据的时候。

当我们在服务器端采样延迟时,基本上只能获取到:

  • 服务器服务“用户”代码

当我们在客户端采样延迟时,基本上可以获取到:

  • 服务器服务“用户”代码
  • 服务器端netty
  • 服务器端Linux延迟
  • 客户端Linux延迟
  • 客户端netty延迟

关于指标数据有两个问题:我们无法有效判断延迟究竟来自客户端还是服务器堆栈中的不透明部分。作为服务所有者,如果基于客户端延迟设置告警,当客户端延迟因Netty或Linux队列堆积而升高时(即使服务运行平稳),你仍会收到告警。

此外,基于现有暴露的指标,合理的客户端延迟度量反映的是客户端与所有服务器通信的总体延迟,这与服务器端指标展示的视角截然不同——服务器端提供的是单台服务器的延迟数据,且缺乏有效方法跨所有客户端聚合单台服务器的客户端延迟数据。因此,例如很难判断某个特定服务器实例在Netty层是否存在高延迟。

以下是若干集群级客户端与服务器端延迟测量的对比示例。这些数据特意选取了能展示客户端与服务器端延迟差异的典型截面。

客户端与服务器端延迟差异显著的图表

这是一个CDF图,采用标准CDF方向:纵轴为百分位数,横轴为延迟值。曲线向右下方移动表示延迟升高,向左上方移动表示延迟降低;曲线越平缓表示延迟增长越快,曲线越陡峭表示延迟增长越慢。

由于图表采用双对数坐标,尽管两条曲线看似接近,客户端与服务器端延迟的实际差异却很大。例如,观察99%分位延迟:服务器端约16毫秒,客户端约240毫秒,相差15倍。若固定延迟值(如240毫秒)查看百分位数,该值在客户端对应99%分位,但在服务器端已远超99.9%分位。

以下图表具有类似特征,但客户端与服务器端的差异幅度有所不同。

客户端与服务器端延迟在p99.5前差异适中,之后显著扩大
客户端与服务器端延迟在p74前差异较小,之后逐渐扩大
客户端与服务器端延迟在接近客户端超时值前差异适中,超时值附近急剧扩大
客户端与服务器端延迟在p999前差异较小,之后快速扩大

我们可以看到,客户端测量的延迟与服务器端测量的延迟往往存在显著差异,即使在较低百分位数上差异较小的情况下,有时在较高百分位数上差异也会变大——更高的负载会导致更多的排队,从而增加Netty和内核中的延迟。

值得注意的是,对于任意特定的服务器端延迟值,我们观察到客户端延迟值的范围非常广泛。例如,以下是service-5的客户端与服务器端延迟的局部放大散点图。如果缩小范围,我们会发现对于服务器端测量为10ms的请求,客户端测量到的延迟可能高达500ms。更普遍的情况是,许多请求的服务器端延迟与客户端延迟非常接近,但也有一些请求的服务器端延迟完全无法准确反映客户端延迟。在几乎所有此类情况下,客户端延迟更高是由于堆栈中我们无法观测的部分存在排队;而在极少数情况下,客户端延迟更低则是由于我们检测工具的问题。在下图中,由于延迟追踪方式的限制,我们只能以1ms为粒度记录延迟。图中的数据点被随机添加了±0.4ms的抖动,以便更清晰地展示密集区域的分布情况。

按请求绘制的客户端与服务器端延迟散点图,显示任何特定的服务器端延迟值都可能对应非常广泛的客户端延迟值

虽然理论上可以通过Netty和内核的检测工具来追踪Finagle移交后的请求延迟(内核甚至提供了相关钩子使这一过程相对简单),但在短期内这很可能得不偿失。如果你想了解服务受不可观测延迟的影响程度,利用Zipkin并结合Rebecca Isaacs、Jonathan Simms和Rahul Iyer的工作(这也是我生成上述图表的方法),可以轻松获得大致概念。相关代码已提交至[我们单体仓库中的某个路径],如果你只想查看其他服务,直接替换服务名称即可。

缺乏集群级聚合能力

在上述示例中,我们之所以能获取集群级的延迟百分位数,是因为使用了Zipkin的数据——它会尝试对请求进行均匀随机采样。但由于多种原因,服务所有者主要依赖指标数据。虽然这些数据因未被采样而更完整,但无法用于计算集群级聚合,因为我们在每个分片上预先计算了固定的聚合值,且无法从分片聚合结果重建集群级聚合。

从我们服务的仪表盘来看,最常见的延迟指标是每个分片的平均分片级99%分位延迟(部分处于请求树深层的服务,如缓存,会使用更靠尾部的数值)。不幸的是,对分片级尾部延迟取平均值,恰恰违背了监控尾部延迟的初衷。如果我们思考为何要使用尾部延迟,是因为当请求树具有高扇出和高深度时,极小比例的服务器响应变慢就可能导致大量甚至绝大多数顶层请求变慢。此时对尾部延迟取平均值,既无法体现尾部延迟的价值——因为分片级尾部延迟的平均值无法捕捉“少量服务器响应变慢会拖慢大量请求”这一特性,同时也失去了查看集群级平均值的优势——而后者本可以从分片级平均值中重建。

例如,当少数节点返回缓慢时,这对分片级平均尾部延迟的影响很小,但集群级尾部延迟却会显著升高。正如我们在[一份量化整个集群中机器级问题范围及其对数据完整性和性能影响的文档][2](#fn:H)中所见,主机级问题频繁出现,可能导致节点尾部延迟上升一个或多个数量级,有时甚至会使该节点的中位延迟超过其他节点的尾部延迟。由于少数甚至单个此类节点就能决定集群的尾部延迟,因此对所有节点取平均值会产生误导。例如,在一个100节点的集群中,若某个节点的尾部延迟升高了10倍,那么集群级延迟的平均值可能仅增加0.99 + 0.01 * 10 = 1.09倍,而实际尾部延迟的增幅却要大得多。

部分服务所有者试图通过对99%分位值再取百分位数(通常是90%分位或99%分位)来更准确地近似集群级尾部延迟,但这同样行不通。一般而言,不存在任何分片级百分位数或其他聚合方式,能够从分片级尾部延迟重建出集群级尾部延迟。

以下是人们在仪表盘上尝试通过实例级指标数据获取集群范围延迟的各种尝试图,与实际(采样)集群范围延迟的对比——该服务使得百分位数聚合的百分位数比小型服务更准确。我们可以看到相关性非常弱,并且存在我们预期的问题:尾部平均值受异常分片的影响不如“应有”程度大,而各种常用百分位数要么影响不足,要么影响过度(平均而言),且与实际延迟的相关性也很弱。由于我们以分钟粒度追踪指标,下图中每个点代表一分钟,x轴为采样后的集群范围p999延迟,y轴为仪表盘聚合指标值。由于追踪管道的单次延迟测量精度为1毫秒,数据点在水平方向上随机抖动±0.3毫秒以更清晰展示分布(垂直方向未施加此类抖动,因为指标管道无此限制,因此该数据精度更高)。

每分钟散点图:各分片p999平均值 vs 实际p999,显示分片p999平均值是非常差的近似 每分钟散点图:各分片p999的p99 vs 实际p999,显示分片p999的p99是较差的近似 每分钟散点图:各分片p999的p999 vs 实际p999,显示分片p999的p999是非常差的近似

集群范围延迟与分片延迟聚合之间的相关性非常弱,以至于即使你选择了能产生正确平均行为的聚合方式,该值在几乎所有样本(分钟)中仍会严重偏离。鉴于我们的基础设施,真正可行的解决方案只有:扩展追踪管道以用于仪表盘和告警,或在Finagle中添加指标直方图,并将数据通过所有环节最终接入[仪表盘软件],从而获得正确的集群级聚合3

尽管取尾部延迟的平均值很流行(因为简单且人们熟悉,例如某[同行公司名称]的可观测性技术负责人曾表示他们不应费心于平均值以外的指标,因为所有人都只想要平均值),但取分片级尾部延迟的平均值或其他聚合值,既不具备人们期望的特性,也不符合人们的预期。

分钟级分辨率

另一个独立的问题是,我们仅以分钟粒度收集指标,这导致我们观察基础设施运行状况的能力存在缺口。Rezolus能以秒级(某些情况下甚至亚秒级)粒度收集指标,但出于本次演讲范围之外的原因,它通常仅用于系统级指标(少数例外)。

我们都见过这样的情况:某个突发性的、分钟级以下的事件导致了问题。让我们来看一个具体例子。在这次事件中,某个服务的延迟和错误率升高。查看我们导出的标准指标并没有提供有用信息,但观察分钟级以下的指标立刻揭示了线索:

按采样请求绘制的每请求延迟图,显示巨大峰值后请求率急剧下降

对于这个特定缓存分片(以及其他许多未显示的分片),在 时间 0 时延迟大幅增加,随后请求率在 30 秒内极低。这 30 秒是因为 service-6 的分片被配置为:如果 service-6 客户端遇到过多失败请求,则将它们通信的服务器标记为死亡 30 秒。这个决策是分布式的,因此受影响的 cache-1 分片的请求率并未降至零;service-6 的某些分片在延迟升高期间并未向 cache-1 的该特定分片发送请求,因此它们没有将该 cache-1 分片标记为死亡,并继续发出请求。

分钟级以下的请求延迟视图非常清晰地揭示了导致 service-6 错误率和延迟升高的机制。

需要注意的一点是,缺乏分钟级以下的可见性并非这里唯一的问题。延迟升高的很大一部分发生在延迟指标不可见的地方,导致监控 cache-1 延迟不足以检测到问题。下图显示,cache-1 单个实例的报告延迟指标为蓝色点,而客户端观察到的测量(采样)延迟为黑色线4。报告的 p99 延迟为 0.37ms,但实际 p99 延迟约为 580ms,相差超过三个数量级。

报告指标延迟与追踪数据延迟的对比图,显示指标延迟与追踪延迟之间存在极大差异

总结

尽管我们现有的延迟报告和告警设置运行得相当不错——网站通常正常工作,且与同规模同行公司相比,我们的可靠性实际上相当好——但我们的设置确实带来了一些显著成本。

其一,我们经常遇到这样的情况:如果不使用被视为专业工具且大多数人不会使用的工具,就很难看清问题所在,这增加了值班的辛劳。其二,由于我们对集群范围延迟的估计存在较大误差,我们必须预留大量冗余空间,并保持比实际想要实现的延迟严格得多的延迟 SLO,以避免用户可见的事件。这增加了运营成本,正如我们在[一份比较每位用户运营成本与同类流量级别公司的文档]中所见。

如果你喜欢这篇文章,你可能还想阅读 单主机追踪与采样分析器的对比

附录:开环与闭环延迟测量

我们的一些合成基准测试设置(例如 setup-1)采用“闭环”测量方式,即有效发送单个请求,等待其返回后再发送下一个请求。其中一些设置允许一定程度的并行性,即同时最多有 N 个请求在途,但这在真实性方面仍存在类似问题。

举一个简单的例子来说明这个问题:假设有一个服务,在生产环境中恰好每秒接收 1 个请求,且该服务的正常响应时间为 0.5 秒。在正常行为下,如果我们以每秒 1 个请求的频率发出请求,我们会观察到平均、中位数以及所有百分位的请求时间均为 0.5 秒。作为读者的练习,请计算:在服务没有并行性,且一次请求在 1 分钟基准测试运行中途耗时 10 秒的情况下,对于闭环与开环基准测试设置,其平均延迟和 90% 分位延迟。其中,开环情况下基准测试设置以每秒 1 个请求的频率发出请求,闭环情况下同样以每秒 1 个请求的频率,但需等待前一个请求完成后再发送下一个。

更多信息请参见 Nitsan Wakart 关于在 YCSB 基准测试中修复此问题的文章Gil Tene 关于此问题的演讲

附录:未加权平均的使用

在我查看过的仪表板中,一个常见的与尾延迟平均值相关的问题(且独立于取平均值时出现的问题)是,未加权平均常常低估实际延迟。

我经常看到未加权平均出现在两种场景:一是有人通过跨数据中心取未加权平均来获得整体延迟,二是有人通过跨分片取平均来获得集群范围的延迟。这两种情况都存在相同的问题,即负载较低的分片往往延迟也较低。当某个数据中心出现故障时,这一点尤为明显。错误地使用跨数据中心未加权平均的服务,通常会显示延迟降低,而实际上被服务的请求延迟却增加了。

感谢 Ben Kuhn 的评论/更正/讨论。


  1. 这是另一个有些过时的条目,因为这份文档推动了 Flavio Brasil 和 Vladimir Kostyukov 在 Finagle 上的工作,以减轻该问题的影响;随后,我当时的实习生 Xi Yang 在内核调度器上提交了一个补丁,基本消除了该问题——其原理是阻止 cgroups 超出其 CPU 分配(而标准机制允许 cgroups 超额分配,然后实际上让该 cgroup 进入休眠状态,直到其摊销后的 CPU 分配不再超额,这对尾部延迟非常不利)。[返回]

  2. 这是另一个过时的条目,因为内核团队、HWENG 团队以及新成立的集群健康团队已投入大量精力,降低了不健康机器的比例。[返回]

  3. 这一点如今也明显过时了。Finagle 现在确实支持导出分片级别的直方图数据,并且可以通过访问导出的指标端点,以一次性查询的方式获取这些数据。[返回]

  4. 如前所述,不透明延迟可能来自服务器或客户端,但在此案例中,我们有充分证据表明延迟来自 cache-1 服务器而非 service-6 客户端,因为如果 service-6 客户端产生不透明延迟,那么 service-6 的所有请求都应可见该延迟,但我们仅在 service-6cache-1 的请求中观察到较高的不透明延迟,而它“通信”的其他服务器则没有。[返回]