微服务故障的全局解决:Aperture 简介


在处理微服务系统中的故障时,一直使用负载卸载和断路器等本地化缓解机制,但它们可能不如更全球化的方法有效。这些本地化机制在防止单个服务过载方面很有用,但它们在处理涉及服务之间交互的复杂故障时不是很有效,这是微服务的特征失败。

处理这些复杂故障的一种新颖方法采用了系统的全局视图:当出现问题时,将自动激活全局缓解计划,以协调跨服务的缓解措施。

在这篇文章中,我们评估了开源项目Aperture以及它如何为我们的服务启用全局故障缓解计划。我们首先描述我们在 DoorDash 遇到的常见故障类型。然后我们深入研究帮助我们度过难关的现有机制。我们将解释为什么本地化机制可能不是最有效的解决方案,并支持全局意识的故障缓解方法。

此外,我们将分享我们使用Aperture 的初步经验,它提供了应对这些挑战的全局方法。

微服务架构故障的分类
在我们解释我们为处理故障所做的工作之前,让我们探讨一下组织所经历的微服务故障的类型。我们将讨论 DoorDash 和其他企业遇到的四种类型的故障。

在 DoorDash,我们将每次失败视为一次学习机会,有时会在公共博客文章中分享我们的见解和经验教训,以表明我们对可靠性和知识共享的承诺。在本节中,我们将讨论我们遇到的一些常见故障模式。每个部分都伴随着从我们过去的博客文章中提取的真实中断,可以更详细地探索这些中断。

以下是我们将详细说明的故障:

  • 级联故障:不同互连服务失败的连锁反应
  • 重试风暴:当重试对降级的服务施加额外压力时
  • 死亡螺旋:一些节点发生故障,导致更多流量被路由到健康节点,使它们也发生故障
  • 亚稳态故障:一个总体术语,描述由于存在正反馈回路而无法自我恢复的故障

连锁故障
级联故障是指单个服务的故障导致其他服务故障的连锁反应的现象。我们在博客中记录了此类严重中断。在那种情况下,故障链从看似无害的数据库维护开始,这增加了数据库延迟。然后延迟向上游服务冒泡,导致超时和资源耗尽错误。增加的错误率触发了一个错误配置的断路器,它停止了许多不相关服务之间的流量,导致大爆炸半径的中断。
级联故障描述了故障跨服务传播的一般现象,故障可以通过多种方式传递给另一个故障。重试风暴是一种常见的传输模式,我们将在接下来深入探讨。

图 1 :该图说明了一个正在经历级联故障的微服务系统。该问题始于延迟增加的降级数据库。此延迟通过 RPC 调用链传播并触发服务 2 中的错误。此错误导致服务 2 对某些请求做出错误响应,然后影响服务 1。

重试风暴
由于远程过程调用 (RPC)的不可靠特性,RPC 调用站点通常设置超时和重试以使每次调用更有可能成功。当故障是暂时的时,重试请求非常有效。但是,当下游服务不可用或速度慢时,重试会使问题恶化,因为在这种情况下,大多数请求最终都会重试多次,最终仍会失败。这种应用过多和无效重试的场景称为工作放大,它会导致已经降级的服务进一步降级。例如,这种中断发生在我们向微服务过渡的早期阶段:我们支付服务的延迟突然增加导致 Dasher 应用程序及其后端系统的重试行为,这加剧了这种情况。

图 2 :此图说明了正在经历重试风暴的微服务系统。该问题始于延迟增加的降级数据库。这种延迟通过 RPC 调用链传播并触发服务 2、3 和 5 中的重试,这给 DB 带来了更大的压力。

死亡螺旋
故障经常会通过跨服务的 RPC 调用图垂直传播,但它们也可能会在属于同一服务的节点之间水平传播。死亡螺旋是一种以导致节点崩溃或变得非常慢的流量模式开始的故障,因此负载均衡器将新请求路由到其余健康的节点,这使得它们更有可能崩溃或变得过载。这篇博文描述了一次中断,首先是一些 pod 未能通过就绪性探测,因此被从集群中删除,而其余节点由于无法单独处理大量负载而失败。

图 3:此图说明了一个正在经历死亡螺旋的服务集群。节点 1 和节点 2 降级并被新启动的节点取代,这些节点还没有准备好接收流量。负载均衡器将所有传入请求路由到节点 3,使其也更有可能降级。

亚稳态失效
最近的一篇论文提出了一种研究分布式系统故障的新框架,称为“亚稳态故障”。我们经历的许多中断都属于这一类。这种类型的故障的特点是系统内的正反馈循环,由于工作放大而提供持续的高负载,即使在初始触发(例如,部署不当;用户激增)消失之后也是如此。亚稳态故障尤其糟糕,因为它不会自我恢复,工程师需要介入以停止正反馈循环,这会增加恢复所需的时间。


图 4:此图说明了亚稳态故障的生命周期。随着负载波动,系统在稳定和脆弱状态下运行。当系统处于脆弱状态时,诸如用户激增之类的触发因素可能会导致其转变为亚稳态,其特征是系统内的正反馈循环会导致持续的高负载。然后需要手动干预以使系统返回到稳定状态。

本地对策
上一节中记录的所有故障都是试图在服务实例内限制本地故障影响的对策类型,但这些解决方案都不允许跨服务协调缓解以确保系统的整体恢复。为了证明这一点,我们将深入研究我们部署的每个现有缓解机制,然后讨论它们的局限性。

我们将讨论的对策是:

  • 减载:防止降级的服务接受更多请求
  • 断路器:在降级时停止传出请求
  • Auto scaling:这有助于处理高峰流量时的高负载,但只有在将其配置为预测性而非反应性时才有用

接下来我们将解释所有这些容错策略是如何工作的,然后讨论它们的缺点和权衡。

减载
Load shedding减载 是一种可靠性机制,当正在运行的或并发的请求数超过限制时,它会在服务入口拒绝传入的请求。通过仅拒绝一些流量,我们最大化了服务的有效吞吐量,而不是让服务完全过载,不再能够做任何有用的工作。在 DoorDash,我们使用 Netflix 库并发限制中的“自适应并发限制”对每台服务器进行检测. 它作为 gRPC 拦截器工作,并根据它观察到的延迟变化自动调整并发请求的最大数量:当延迟上升时,该库降低并发限制以给每个请求更多的计算资源。此外,负载卸载器可以配置为从请求的标头中识别请求的优先级,并且在过载期间只接受高优先级的请求。

图 5:该图显示了工作中的负载卸载器。服务 4 已降级,无法处理所有到达它的请求。在服务入口处,它拒绝来自服务 3 的请求,因为它的优先级较低。

卸载或减载可以有效地防止服务过载。然而,由于减载器安装在本地,它只能处理本地服务中断。正如我们在上一节中看到的,微服务系统中的故障通常是由服务之间的交互引起的。因此,在中断期间进行协调的缓解措施将是有益的。例如,当一个重要的下游服务 A 变慢时,上游服务 B 应该在请求到达 A 之前开始控制请求。这可以防止从 A 增加的延迟在子图中传播,从而可能导致级联故障。

除了缺乏协调的局限性之外,减载也很难配置和测试。正确配置负载卸载器需要精心编排负载测试以了解服务的最佳并发限制,这不是一项容易的任务,因为在生产环境中,一些请求比其他请求更昂贵,而一些请求对系统来说比其他请求更重要。作为负载卸载器配置错误的示例,我们曾经有一个服务,其初始并发限制设置得太高,导致服务启动期间出现临时过载。尽管负载卸载器最终能够调低限制,但最初的不稳定性很糟糕,这表明正确配置负载卸载器是多么重要。然而,工程师通常将这些参数保留为默认值,

断路器
虽然减载是一种拒绝传入流量的机制,但断路器会拒绝传出流量,但就像减载器一样,它只有局部视图。断路器通常作为内部代理来实现,用于处理对下游服务的传出请求。当来自下游服务的错误率超过阈值时,断路器打开,它会迅速拒绝对有问题的服务的所有请求,而不会放大任何工作。一段时间后,断路器逐渐允许更多流量通过,最终恢复正常运行。我们在 DoorDash 的内部 gRPC 客户端中内置了一个断路器。

图 6:该图显示了工作中的断路器。服务 4 已降级并返回错误。在服务 3 的客户端安装的断路器打开和停止来自服务 3 的所有请求。

在下游服务出现故障但能够在流量减少时恢复的情况下,断路器可能很有用。例如,在编队的死亡螺旋期间,不健康的节点被尚未准备好接收流量的新启动节点所取代,因此流量被路由到剩余的健康节点,使它们更有可能过载。在这种情况下,断开断路器会为所有节点提供额外的时间和资源,使其再次恢复健康。

断路器具有与负载卸载相同的调整问题:服务作者没有确定跳闸阈值的好方法。许多关于此主题的在线资源使用“50% 的错误率”作为经验法则。但是,对于某些服务,50% 的错误率可能是可以容忍的。当被调用的服务返回错误时,可能是因为服务本身不健康,也可能是因为更下游的服务出现问题。当断路器打开时,其背后的服务将在一段时间内变得实际上无法访问,这可能被认为更不理想。跳闸阈值取决于服务的 SLA 和请求的下游影响,这些都必须仔细考虑。


自动缩放
所有集群协调器都可以配置自动缩放以处理负载的增加。当它打开时,控制器会定期检查每个节点的资源消耗(例如 CPU 或内存),当它检测到高使用率时,它会启动新节点来分配工作负载。虽然此功能看起来很有吸引力,但在 DoorDash,我们建议团队不要使用反应式自动缩放(在负载高峰期间实时扩展集群)。由于这是违反直觉的,我们在下面列出了反应式自动缩放的缺点。

  • 新启动的节点需要时间预热(填充缓存、编译代码等),并且会表现出更高的延迟,从而暂时降低集群容量。此外,新节点将运行代价高昂的启动任务,例如打开数据库连接和触发成员协议。这些行为很少见,因此它们的突然增加可能会导致意想不到的结果。
  • 在涉及高负载的中断期间,为一项服务增加更多容量通常只会将瓶颈转移到其他地方。它通常不能解决问题。
  • 反应性自动缩放使得事后分析变得更加困难,因为指标的时间线会以各种方式针对事件、人类正在采取的缓解措施和自动缩放器进行调整。

因此,我们建议团队避免使用反应式自动缩放,而宁愿使用预测性自动缩放,例如KEDA 的 cron,它会根据全天的预期流量水平调整集群的大小。

所有这些本地化机制都擅长处理不同的故障类型。然而,本地化有其自身的缺点,现在我们将深入探讨为什么本地化解决方案只能让你走这么远,以及为什么全局化观察和干预会更可取。

现有对策的不足
我们采用的所有可靠性技术都具有类似的结构,由三个部分组成:操作条件的测量、通过规则和设置识别问题以及出现问题时采取的行动。例如,在减载的情况下,三个组成部分是:

  • 测量:计算最近的服务延迟或错误历史
  • 识别:使用数学公式和预设参数来确定服务是否存在过载风险
  • 行动:拒绝过多的传入请求

对于断路器,它们是:

  • Measure:评估下游服务的错误率
  • 识别:检查它是否超过阈值
  • 行动:停止该服务的所有传出流量

然而,现有的本地化机制也存在类似的缺点:

  • 他们使用服务本地的指标来衡量运行状况;然而,许多类型的中断都涉及许多组件之间的交互,并且需要一个人具有系统的全局视图,以便就如何减轻过载条件的影响做出正确的决策。
  • 他们采用非常一般的启发式方法来确定系统的健康状况,这通常不够精确。例如,仅延迟不能判断服务是否过载;下游服务缓慢可能导致高延迟。
  • 他们的补救行动是有限的。由于这些机制是在本地进行检测的,因此它们只能采取本地操作。本地操作通常不是将系统恢复到健康状态的最佳选择,因为问题的真正根源可能在别处。

我们将讨论如何克服这些缺点并使缓解措施更加有效。

利用全局化控制:Aperture 进行可靠性管理
一个超越本地对策以实施全局化负载控制的项目由开源可靠性管理系统Aperture实施。它提供了一个可靠性抽象层,使跨分布式微服务架构的可靠性管理更加容易。与只能对本地异常做出反应的现有可靠性机制不同,Aperture 提供了一个集中的负载管理系统,使其能够协调许多服务以响应持续的中断。

Aperture 的设计
与现有的对策一样,Aperture 通过三个关键组件监视和控制系统的可靠性。

  1. 观察:Aperture 从每个节点收集与可靠性相关的指标,并在 Prometheus 中聚合它们。
  2. 分析:独立运行的 Aperture 控制器持续监控指标并跟踪与 SLO 的偏差
  3. 启动:如果有任何异常,Aperture 控制器将启动与观察到的模式相匹配的策略,并在每个节点上应用操作,如负载卸载或分布式速率限制。


我们使用 Aperture 的经验
Aperture在检测和处理系统异常的方式方面具有高度可配置性。它采用以 YAML 文件编写的策略来指导其在中断期间的操作。例如,下面的代码取自 Aperture文档并经过简化,可计算指数移动平均 (EMA) 延迟。它从 Prometheus 获取延迟指标,并在计算值超过阈值时触发警报。

触发警报时,Aperture 会根据为其配置的策略自动执行操作。它目前提供的一些操作包括分布式速率限制和并发限制(又名卸载)。Aperture 具有对整个系统的集中视图和控制这一事实为缓解中断开辟了无数可能性。例如,可以配置一种策略,在下游服务过载时减轻上游服务的负载,允许过多的请求在到达有问题的子图之前失败,从而提高系统的响应速度并节省成本。

为了测试 Aperture 的功能,我们运行了 Aperture 的部署并将其集成到我们的一项主要服务中,所有这些都在测试环境中进行,并发现它是一个有效的负载卸载器。当我们增加RPS在发送到服务的人工请求中,我们观察到错误率增加了,但吞吐量保持稳定。在第二次运行中,我们降低了服务的计算能力,这一次我们观察到吞吐量降低了,但延迟仅略有增加。在两次运行的幕后,Aperture 控制器注意到延迟增加并决定降低并发限制。因此,我们应用程序代码中的 API 集成拒绝了一些传入请求,这反映在错误率增加上。降低的并发限制可确保每个接受的请求都获得足够的计算资源,因此延迟只会受到轻微影响。

通过这个简单的设置,Aperture基本上充当负载卸载器,但它比我们现有的解决方案更易于配置和用户友好。我们能够使用复杂的并发限制算法配置[url=https://github.com/fluxninja/aperture]Aperture[/url],从而最大限度地减少意外负载或延迟的影响。Aperture 还提供了一个使用 Prometheus 指标的一体式 Grafana 仪表板,可让您快速了解我们服务的健康状况。

我们尚未尝试 Aperture 的更高级功能,包括跨服务协调缓解措施的能力,以及具有在持续负载后触发自动缩放的升级策略的可能性。评估这些功能需要更精细的设置。话虽如此,可靠性解决方案最好在真正发生中断的生产环境中进行测试,而中断总是不可预测的。

Aperture 整合细节
值得深入探讨 Aperture 如何集成到现有系统中。Aperture 的部署包括以下组件:

  • Aperture 控制器:这个模块是光圈系统的大脑。它不断监控可靠性指标并决定何时执行缓解蓝图。当蓝图被触发时,它会向 Aperture 代理发送适当的操作(例如卸载)。
  • Aperture agent:每个 Kubernetes 集群运行一个 Aperture agent 实例,负责跟踪和确保同一集群中运行的节点的健康。当请求进入服务时,它会被集成点截获,然后将相关元数据转发给 Aperture 代理。Aperture 代理记录元数据并回复是否接受请求的决定。这样的决定是基于光圈控制器提供的信息。
  • 集成点:想要受益于集中式可靠性管理的服务可以通过三种方式与 Aperture 集成。如果服务构建在服务网格上(目前仅支持 Envoy),Aperture 可以直接部署在服务网格上,无需更改应用程序代码。还有一些Aperture SDK,可用于将应用程序代码与 Aperture 端点集成。对于 Java 应用程序,还可以使用Java Agent自动将 Aperture 集成注入 Netty。为了说明此集成的作用,下面是一个代码片段,演示了如何在Java中使用 Aperture SDK 。
  • Prometheus 和 etcd:这些是存储可靠性指标的数据库,由 Aperture 控制器查询以获取当前操作条件的度量。

结论
现有的可靠性机制在单个服务的本地级别进行检测,我们已经证明全局化机制在处理中断方面效果更好。在这篇博客中,我们展示了为什么保持微服务系统可靠运行是一个具有挑战性的问题。我们还概述了我们当前的对策。这些现有的解决方案有效地防止了许多中断,但工程师通常对它们的内部工作原理知之甚少,并且没有对它们进行最佳配置。此外,它们只能在每个服务内部进行观察和操作,这限制了它们在缓解分布式系统中断方面的有效性。

为了测试使用全局化机制来减少中断的想法,我们调查了开源可靠性管理项目 Aperture。该项目通过集中监视和控制职责,而不是让它们由单独的服务来处理,将可靠性管理提升为系统的主要组成部分。通过这样做,Aperture 实现了自动化、高效且具有成本效益的方法来解决中断问题。我们在初始试用期间获得了积极的体验,我们对其潜力感到兴奋。