互联网服务提供商在面临快速增长的挑战的同时,还要管理不断增加的系统分布。虽然服务的可靠运行对于谷歌、亚马逊等大公司来说很重要,但它们的系统一次又一次地发生故障,导致大规模中断和糟糕的客户体验。仅举几例,深受影响的Gmail(2012)、AWSDynamoDB(2015)和最近的Facebook(2021)。在这种情况下,人们经常会遇到所谓的级联故障,导致超出普通系统故障的不良并发症。但是,考虑到他们的预算和技术诀窍,为什么即使是在线业务的大公司也不能完全避免这些故障呢?您可以对自己的系统使用哪些实用的风险缓解方法?这篇文章的目的是向您介绍如何通过防止故障传播来提高大型分布式系统的弹性。1.级联故障级联故障是由于正反馈回路而随时间增加的故障。典型行为最初是由单个节点或子系统故障触发的。然后它将负载分散到其他系统的节点上,这反过来又进一步增加了系统故障的可能性,导致恶性循环或滚雪球效应。级联故障在三个方面很重要:首先,它们可以在短时间内关闭整个服务。其次,受影响的系统不会像往常一样恢复,而是会逐渐恶化。最终还是要靠人为干预。最后,在最坏的情况下,由于负载分布和故障发生得很快,级联故障可能会突然发生并且没有任何警告。本文主要关注分布式计算环境中的级联故障,但它们也可能发生在其他各个领域,例如电力传输、金融、生物学和生态系统。因此,它们是一种相当普遍的现象,有点类似于自然界中发现的模式。为了更好地理解级联故障在计算机科学中的表现,让我们看一个具体案例。2.案例研究:2015年AWSDynamoDB中断AWSDynamoDB是一种高度可扩展的非关系型数据库服务,分布在多个数据中心,提供高度一致的读取操作和ACID事务。Netflix、Airbnb、IMDb等多家知名互联网公司都在使用它。我们要研究的级联故障示例的事件发生在2015年9月20日,当时美国东部地区DynamoDB不可用超过四个小时。涉及两个子系统:存储服务器和元数据服务,这两个子系统都被复制到多个数据中心。存储服务器从元数据服务中为其数据分区分配请求所谓的成员资格。如图1所示。 图1:存储服务器和元数据服务有相应的成员请求超时(也用于数据分区的分配)。如果超时,相应的存储服务器会重试并将自己从服务中排除。不幸的是,此事件的先决条件是DynamoDB引入的称为全局二级索引(GSI)的新功能。这使客户可以更好地访问他们的数据,但缺点是会显着增加元数据表的大小。因此,处理时间变长。同时遗憾的是,元数据服务的容量和会员请求的超时时间都没有相应调整。真正的问题是由于短暂的网络问题导致对某些存储服务器(处理非常大的元数据表)的成员请求超时,这些服务器变得不可用并不断重试它们的请求。这使元数据服务超载,响应速度变慢,并导致更多服务器在达到超时时重新提交其成员资格请求。结果,元数据服务的状况进一步恶化。尽管多次尝试增加资源,系统仍陷入故障循环数小时。最终,问题只能通过中断对元数据服务的请求来解决,即服务基本上处于离线状态。结果是美国东部地区的DynamoDB大范围中断,这是级联故障的典型示例。但是,陷入这个错误循环的系统的基本概念和模式是什么?3.连锁故障的成因首先,应该说连锁故障的触发点是多种多样的。例如,它可能是新功能、维护、流量减少、cron作业、分布式拒绝服务(DDoS)、节流等。它们的共同点是它们在一组有限资源的上下文中工作,这意味着可能会出现服务器过载、资源耗尽、服务不可用等影响。让我们详细了解一下:服务器过载最常见的原因是服务器过载。发生这种情况时,系统性能下降通常会影响系统的其他区域。如图2所示,在初始场景(左)中,来自两个反向代理的负载分布在集群A和B之间,因此集群A以每秒1000个请求的假设最大容量运行。在第二种情况(右)中,集群B发生故障,整个负载都转到集群A,这使集群A过载。集群A现在必须每秒处理1200个请求,并且开始表现异常,导致性能远低于预期的1000个请求每秒。 图2:集群A和B根据容量接收负载(左),如果集群B发生故障,集群A将接收超载(右)资源耗尽服务器资源有限。如果负载增加到超过某个阈值,服务器的性能指标(例如,延迟或错误率)就会恶化,这意味着崩溃的风险更高。后续影响取决于造成瓶颈的资源类型,例如:如果没有足够的CPU,可能会出现各种问题,包括请求缓慢、过度排队效应或线程不足。如果内存/RAM被过度使用,任务可能会崩溃,或者缓存命中率会下降。此外,线程饥饿会直接导致错误或导致健康检查失败。在这种情况下对主要原因进行故障排除通常很痛苦。这是因为所涉及的组件是相互依赖的,根本原因可能隐藏在复杂的事件链之后。例如,假设可用于缓存的内存较少,导致缓存命中率较低,从而导致后端负载较高,以及这些情况的组合。服务不可用当资源耗尽导致服务器崩溃时,流量会转移到其他服务器,从而增加这些服务器崩溃的可能性。建立了这样一个服务器崩溃循环。更糟糕的是,这些问题可能会在系统中持续存在,因为某些机器仍在关闭或正在重新启动,并且流量的持续增加使它们无法完全恢复。一般来说,当我们将流量从不健康的节点重新分配到健康的节点时,总是存在级联故障的风险。这可能是编排系统、负载平衡器或任务调度系统的情况。为了解决级联故障,我们需要仔细研究所涉及的组件之间的关系。4.跳出循环——如何修复级联故障从DynamoDB的案例中可以看出,修复级联故障非常棘手。特别是从大型科技公司的角度来看,分布式系统增加了很多复杂性,这使得跟踪各种互连变得更加困难。我们在这里使用称为因果循环图(CLD)的方法来描述这些(级联)关系。CLD是一种建模方法,有助于可视化复杂系统中的反馈回路。图3可视化了AWSDynamoDB损坏的CLD。解释如下:箭头表示初始变量和后续变量之间的动态。例如,如果元数据服务的延迟增加,则超时次数增加,并且需要的重试次数增加。如果系统中的影响高度不平衡,即正面和负面的数量在很大程度上不相等,则存在强化循环。这意味着系统可能对级联故障敏感。 图3:2015年AWSDynamoDB中断的因果循环图现在,对于级联故障场景,我们可以做很多事情。第一个也是最直观的选择是增加资源。在上图中,您可以看到元数据服务容量在循环中引入了一个负号。如果增加,它会削弱循环的强化,但是,这可能没有用,正如我们在AWS中看到的那样。除了增加资源外,还可以采取其他策略:尽量避免健康检查失败,防止系统因为过多的健康检查而死掉。在线程阻塞请求或死锁的情况下,重新启动服务器。大幅减少流量,然后慢慢增加负载,让服务器逐渐恢复。通过丢弃某些类型的流量来切换到降级模式。消除批处理/不良流量,通过减少非关键或错误工作来降低系统负载。这可能会导致系统的某些服务不可用,客户端可以感知到,所以最好在第一时间避免级联故障。5.避免级联故障有很多方法可以使分布式系统对级联故障具有鲁棒性。一方面,大型互联网公司已经在思考如何防止系统陷入级联错误,比如通过错误隔离,为此市场上已经开发了很多工具和框架。例如,Hystrix(来自Netflix),一个延迟和容错库,或Sentinel。对于前者,Netflix做了进一步的开发,AdaptiveConcurrencyLimiting(你可以在这里阅读更多[4])。但一般来说,这些工具都是将外部调用包装成某种数据结构,试图抽象出关键点。另一方面,目前有一些技术正在开发中,有一些复杂的解决方案,例如,实现所谓的sidecar代理、Istio等服务网格。其他一些示例是Envoy或Haproxy。除了这些解决方案之外,还需要牢记某些系统设计概念。例如,尽量减少系统中同步调用的次数。通过应用发布-订阅设计(例如使用Kafka)从编排转变为编排。面对不断增加的流量,此解决方案通常更加稳健。执行容量规划(取决于用例)等其他方法也可能有帮助。这通常意味着实施自动化供应和部署、自动缩放和自动修复的解决方案。在这种情况下,密切监视SLA和SLO很重要。现在,为了更好地理解底层解决方案的方法,我们可以看看分布式系统中的典型反模式,在发生级联故障时应该避免这些模式。LauraNolan提出了其中的六个,我们将讨论风险缓解策略。反模式1:接受无限数量请求的队列/线程池中的任务数量应该是有限的。这控制了在请求过多的情况下服务器何时以及如何减慢速度。此设置应在服务器可以处理峰值负载的范围内,但不要太多以至于阻塞。在这种情况下,对于系统和用户来说,快速失败比长时间挂起要好。在代理或负载均衡器端,这通常通过速率限制策略来实现,例如,避免DDoS和其他形式的服务器过载。但是还有许多其他方面需要考虑,例如,在队列管理的上下文中,因为大多数服务器在线程池前面都有一个队列来处理请求。如果数量增加超过队列的容量,请求将被拒绝。队列中等待的大量请求会消耗更多内存并增加延迟。如果请求数接近常量,那么小队列或无队列都可以。这意味着如果流量增加,请求会立即被拒绝。如果预期偏差较大,则应使用更长的队列。此外,为了保护服务器免受过载的影响,卸载和优雅降级的概念是可行的选择。负载卸载用于在过载条件下尽可能保持服务器的性能。这是通过简单地返回HTTP503(服务不可用)状态代码来确定请求的优先级来丢弃流量来实现的。一个更复杂的变体是优雅降级,它逐渐切换到较低质量的查询响应。这些可能运行得更快或更有效率。然而,这必须是一个深思熟虑的解决方案,因为它给系统增加了很多复杂性。反模式2:危险的(客户端)重试行为要减少系统的工作负载,确保避免过多的重试行为很重要。指数退避是一种合适的方法,它通过连续增加重试之间的时间间隔来工作。也可以使用所谓的抖动机制,它会向重试间隔添加随机噪声。这可以防止系统受到累积“负载波”的影响,也称为重试放大(见图4)。 图4:重试放大的典型模式还有一种设计模式叫做熔断。保险丝可以被认为是一种开关。在初始状态下,允许来自上游服务的命令传递给下游服务。如果错误增加,保险丝就会打开,系统很快就会失效。这意味着上游服务出了问题,让下游服务恢复。一段时间后,请求再次逐渐增加。例如,在Hystrix(上面提到的)中,实现了某种断路器模式。另一种减轻危险重试行为的方法是设置服务器端重试预算,设置每分钟可以重试的请求数。任何超出预算的都将被丢弃。但是,我们必须从大局着眼。避免在软件架构的多个级别上执行重试很重要,因为这会呈指数级增长。最后,重要的是要注意重试应该是幂等的并且没有副作用。无状态调用在系统复杂性方面也是有益的。反模式3:输入错误导致崩溃系统应确保服务器不会因输入错误而崩溃。此类崩溃与重试行为相结合,可能会导致灾难性的后果,例如一个服务器一个接一个地崩溃。在这方面,尤其要仔细检查来自外部的输入。使用模糊测试是检测这些类型问题的好方法。反模式4:基于邻近的故障转移确保不要将所有流量重定向到最近的数据中心,因为它也可能过载。相同的逻辑适用于集群中的单个服务器故障,其中一台机器接一台发生故障。因此,为了提高系统的弹性,必须在故障转移期间以可控的方式重定向负载,这意味着必须考虑每个数据中心的最大容量。基于IP任播的DNS方法最终会将流量转发到最近的数据中心,这可能会产生问题。反模式5:故障导致的工作失败通常会给系统引入额外的工作。特别是,在少数节点上发生的故障最终可能会导致剩余节点的大量额外工作(例如,副本)。这会产生有害的反馈循环。一种常见的缓解策略是延迟或限制副本的数量。反模式6:启动时间长一般来说,开始时处理通常很慢。这是因为实例需要初始化和运行时优化。故障转移后,服务和系统经常因负载过重而崩溃。为防止这种情况,我们希望系统启动速度更快。此外,缓存在系统启动时通常为空。这使得查询更加昂贵,因为它们必须去原始位置获取数据。因此,崩溃的风险比系统在稳定模式下运行时更高,因此请务必保持缓存可用。除了这六个反模式外,还有其他系统组件或参数需要检查。例如,您可以查看请求或RPC调用的截止日期。一般来说,很难设定好的截止日期。但是在级联故障的情况下,经常遇到的一个普遍问题就是客户端超过了很多设定的deadline,这意味着大量的资源浪费。AWSDynamoDB示例从一开始就是这种情况。通常服务器应该在截止日期前检查请求是否还有时间,以避免浪费工作。一种常见的策略是所谓的术语传播。也就是说,在请求树的顶部有一个绝对的截止日期。再往下的服务器只获得前一个服务器完成计算后剩余的时间值。例如,服务器A的截止时间为20秒,计算需要5秒,然后服务器B的截止时间为15秒,依此类推。6.结论级联故障是分布式系统中一种可怕而奇特的现象。这是因为有时必须采取违反直觉的路径来避免它们,例如实际上旨在减少错误的定制工作,例如看似智能的负载平衡,这可能会增加完全失败的风险。有时最好的策略是向客户端显示一条错误消息,而不是实施复杂的重试逻辑并冒DDoS攻击系统的风险。但是,有时必须做出妥协。测试、容量规划和在系统设计中应用某些模式有助于提高系统弹性。毕竟,吸取的教训和大型科技公司的后果为采取进一步行动以避免未来发生连锁反应提供了极好的指导。然而,最新的技术和趋势也值得关注。
