随着功能和数据的不断发展,面向服务的分布式系统越来越复杂,将总延迟保持在最小不仅是一项具有挑战性的任务,而且是一个持续的问题。由于代码和部署的变化,以及流量模式的变化,系统会不断地变化。无论是跨服务边界还是在单个服务内部,并行执行都是必不可少的,不同的流量切片也具有不同的延迟特性,而一般的延迟分析工具很难在实践中理解系统。
确定分布式系统中高延迟的根本原因具有相当的挑战性,给定一个分布式系统和工作负载,哪些组件可以优化来减少延迟呢?
从一个假设开始
总体目标是分布式系统的延迟分析,我们如何获得关于延迟根本原因的可操作信息呢?
假定一个分布式系统由三个服务组成(a,b,c),每个服务都有两个子组件,目的是接收用户的请求,执行一些处理,并返回响应。请求首先到达服务 a,服务 a 将请求处理交给子组件 A1。反过来,A1依赖于子组件 B1和 A2,它们有自己的依赖关系,例如A2 调用B2, 这些组件可以在请求处理过程中多次调用c中的C1 或者C2。
就这个系统而言,实际的延迟特性很难预测,潜在的问题较多,例如:
- A1是否能够并行调用 A2和 B1?
- 是否存在一个数据依赖关系,使得对 B1的调用必须在对 A2的调用继续之前完成?
- A2在调用 B2之前执行多少内部处理?收到 B2的回复后怎么办?
- 这些要求会重复吗?每个处理步骤的延迟分布是什么?
- 所有这些问题的答案是如何根据传入的请求而变化的?
- ......
如果不能很好地回答这些问题,那么改善整个系统延迟的努力就会缺乏针对性,并可能付诸东流。例如,为了减少 A1及其下游子组件的总延迟,必须知道哪些子组件实际上影响了端到端系统延迟。在决定优化之前,需要知道 A1对A2的调用是否真的重要。
RPC 度量
一般的,我们可以知道 RPC的生成次数及其延迟特性的粗粒度信息,监控服务收集这些信息并创建显示系统性能的仪表板, 这些粗粒度切片(例如,时间范围、来源和目的地信息)用于RPC的分析。
当有一些 RPC 服务总是对延迟很重要时,对这些服务的监视可以快速识别哪些是导致问题的原因,并确定服务提供者,他们可以努力提高性能。然而,RPC的延迟分析在并行计算方面遇到了困难,重复的RPC 也会使监控变得混乱。异构工作负载对 RPC 延迟分析也是一个挑战,来自不同工作负载的数据点通常混合在一起,随着重要切片维数的增加,具有不同工作负载的问题也会增加。
出于效率原因,大多数 RPC 延迟分析依赖于几个维度的聚合数据,难以识别服务中的重要子组件,这使得很难判断哪些请求需要优化。同时,还会遗漏非 RPC引起的延迟问题。
CPU 分析
CPU 分析补充了 RPC的延迟分析,一旦 RPC延迟分析发现了一个有问题的服务,CPU 分析可以帮助找出如何使该服务更快的方法。收集并聚合函数的调用堆栈,可以洞察耗时的代码路径。
CPU 分析在识别服务中特定组件方面表现出色,如果是CPU 时间影响了总延迟,CPU分析就可以帮助确定优化的位置。然而,许多影响 RPC 的问题也会导致 CPU问题。缺乏有关并行性的信息,意味着无法判断CPU约束与RPC相关,还是实际上阻塞了请求的进度。异构的工作负载也会导致问题: 小而重要的流量切片会在噪音中丢失。将 CPU 分析与有关并行性的信息以及来自请求数据连接起来可以解决这些限制,但是这种技术的应用并不普遍。
另外, 这会使我们更可能专注于使用大量 CPU 的代码,而这些代码却不会对整个系统的延迟产生影响。
全链路跟踪
最后一个常用工具是全链路跟踪, 或者叫分布式跟踪。这种方法通过系统跟踪单个请求,在处理这些请求时收集计时点和其他数据。聚合和分析所跟踪的数据以产生应用洞察力。
与 RPC 延迟分析和 CPU 分析不同,全链路跟踪处理并行性和异构的工作负载,收集所有跨服务请求的信息,包括计时点。可视化可以准确显示每个服务的工作开始和结束的时间,以及哪些服务以并行或串行方式运行。
大多数全链路跟踪包括了对 RPC 边界的跟踪,但省略了服务组件的信息,当然,可以根据需要为组件添加跟踪方式。为特别重要的请求查找工作负载切片也是可能的,但需要手动标记。全链路跟踪甚至允许自动分析来确定哪些服务导致了总的延迟。
遗憾的是,使用全链路跟踪进行延迟分析的主要障碍是成本。如果一个大型服务可能有几十个RPC依赖项,但是重要组件的数量很容易达到这个数量的100倍,需要检测组件的数量会导致跟踪系统的大小呈数量级增长。另一个问题是采样率,随着跟踪变得更加详细,全链路跟踪变得更加昂贵。
权衡,无处不在,如何才能经济有效地进行延迟分析呢?
关键路径分析
关键路径,在项目管理中是指完成一个项目所必须完成的许多相互依赖的步骤。在这里,关键路径描述了分布式系统中直接导致请求处理速度最慢步骤的有序列表,将这些跟踪数据聚合起来,就可以识别整个系统中的延迟瓶颈。
很多软件框架可以被用来自动测试服务组件,检测代码自动标识请求执行的关键路径。只有关键路径被保留用于分析,其他跟踪信息被丢弃,这样可以减少跟踪数据的数量级。
关键路径跟踪与其他请求和响应元数据一起记录,这使得标准的日志分析技术可以使用业务标准来查找跟踪的数据,集中的日志记录可以节省额外的成本。全链路跟踪的系统日志为请求中涉及的每台机器,重建一个完整的请求需要加入来自数百台机器的跟踪数据,而将所有关键路径组件记录在一起可以避免数据关联的开销。
定义关键路径
对于延迟分析,当多个步骤并行进行时,最慢的步骤是关键路径上的唯一步骤。
请求的执行可以被建模为命名节点的有向图,图中的每个节点都有自己的计算,图中的每条边都是一个依赖项,在这个依赖项中,节点必须等待其中一个依赖项的完成,然后才能进行计算。关键路径是通过节点的持续时间最长的路径,从请求入口开始,到计算响应的结束,关键路径的长度是处理请求的总延迟。
识别服务组件
基础设施优先的全链路跟踪系统通常默认在 RPC 边界收集数据,并允许开发人员添加打点数据以进行更详细的跟踪。使用关键路径分析,可以利用软件框架提供的标准化,在默认情况下收集更详细的信息。
框架级实现对于可伸缩性至关重要,能够以相对较小的开发团队为成千上万的软件提供详细的关键路径跟踪。由于每个框架工具和关键路径报告都是自动的,所以大多数开发人员并不知道正在进行关键路径跟踪。一些没有使用框架实现的服务也可以实现服务的关键路径跟踪,但这些服务的跟踪可能是粗粒度的。当向不提供任何跟踪的服务发出请求时,系统会优雅地降级。
传播和合并
在这个延迟的有向图中,将子节点路径传播到父节点可以获得更详细的关键路径视图。每个子组件向关键路径添加一个self节点,用于表示内部产生的延迟,这个过程是递归发生的。传播和合并既发生在单个服务中,也发生在 RPC 服务边界之间。参与关键路径协议的服务使用响应数据中的标准字段将其关键路径传播给调用方。然后,框架级代码将来自RPC的关键路径合并到每个服务的关键路径中。
传播需要纠错,在大规模的分布式系统中,测量错误是不可避免的: 网络时间,bug或数据异常等,有时不能确定到底发生了什么,目标是保持测量误差足够小,才能得出正确的结论。过度计数是最明显的需要检测的问题。盲点也是一个常见的问题,看起来像是关键路径上的大块时间,而这些时间都归因于某一个子组件。
触发关键路径
根据框架实现的不同,延迟的关键路径跟踪也可能会导致巨大的开销。在实践中,并不是每个请求都需要跟踪,因此采用抽样方法来摊销成本。采样需要跨服务协调,以避免在跟踪中造成不必要的盲点。
每个服务可以做出一个独立的抽样决策,然后在请求数据中将该决策传递给出站RPC。即使调用者没有选择采样,下游服务也可以自由地跟踪和记录它们自己的关键路径,没有请求采样的调用方将忽略产生的跟踪。当然,也可以选择跟踪特定的请求,而不是依赖于随机抽样。对于调试目的路径跟踪,这种方法很有用。
局限性
关键路径跟踪的操作开销很低,因框架在运行时被重写以合并到关键路径跟踪,这些更改通常不到总 CPU 成本的0.1% ,一般地,这样的CPU 开销被认为是可以接受的。网络开销可能很大,在实践中,网络开销可以通过仅仅0.1% 的请求采样并压缩来减少。
通常,任何延迟优化工作都应该集中在关键路径上的子组件上。然而,非关键路径子组件的资源竞争也会减慢关键路径的执行速度。
流是改善延迟的一种重要技术,但不幸的是,流式API的关键路径跟踪并没有很好的定义。服务可以返回一系列结果,允许在后期结果准备好之前开始处理早期结果。客户端和服务器同时发送多条消息是一种更复杂的编程模型,但在某些情况下对延迟也很有用,这些操作的关键路径必须明确。
聚合和可视化
与大多数数据收集系统一样,关键路径跟踪随着更多数据的收集和汇总,提供了更精确的信息。单个关键路径跟踪很有意思,但可能无法提供整个系统性能的实际视图。单个请求可能是异常值,合并多个文件可以创建统计意义上的系统性能视图。标准的日志分析技术用于选择随机的请求样本,一个有效的技巧是只选择那些极其缓慢的请求,分析这些请求经常会揭示系统性问题,这些问题相当于重要的优化机会。
关键路径分析具有合并机制,在这种机制中,调用方将其后端的关键路径合并到自己的关键路径中。与从后端日志中连接信息的方法相比,这是一种更简单、更快速的方法。
聚合可以看作是整个系统的平均关键路径,由于子组件不是顺序的,平均值可能不反映系统真正的关键路径。平均关键路径上的组件时间既反映了子组件处于关键路径上的频率,也反映了子组件出现时所需的时间。在考虑通常较快但偶尔较慢的系统子组件时,这种差异变得非常重要。与其他统计分析一样,查看样本池中数据的分布可能会有所帮助。需要多少个样本,我们才能确信一个特定大小的关键路径变化不是请求延迟随机变化的结果呢?中心极限定理可以计算不同样本量的置信区间。
一旦数据被聚合,就可以使用各种可视化工具来理解系统延迟,例如,调用图和火焰图都很有帮助。识别优化机会的一个常用策略是查找异常值的请求,把事件放在上下文中是有帮助的。在优化延迟时间时,应首选关键路径的延迟分析,关注受影响的流量是延迟分析的关键。如果可能,最好对流行为提供更好的框架支持。
小结
在真实世界的分布式系统中,现有的RPC度量、 CPU 分析和全链路跟踪,对于理解整个系统及其组件非常有价值。但是,诸如高度并行的执行流、异构的工作负载以及子系统中复杂的执行路径等问题使得延迟分析变得困难,而且,这些系统和工作负载经常发生变化。
实际上,没有一个团队或个人对整个大型系统有着详细的了解。
关键路径跟踪在这样的系统中可能解决这些挑战,提供可操作的、精确的延迟分析。框架级别的集成提供了程序代码的详细视图,而不需要每个开发人员实现自己的跟踪。通用的协议允许一致的触发、聚合和可视化机制,高效的实现允许高采样率,即使对于相对少见的事件也能提供精确的数据。