弹性系统设计:从午餐高峰看云服务过载治理

SEO 摘要: 本文介绍如何从餐厅午餐高峰理解弹性系统设计与云服务过载治理,重点讨论负载卸除、公平性与配额管理、自动扩展、队列深度管理、恒定工作量、控制平面与数据平面、冷缓存、断路器、自适应重试和运维可见性等策略,帮助团队构建安全可靠、可预测的分布式系统。

弹性系统设计:从午餐高峰看云服务过载治理

简介:从餐厅高峰理解弹性系统

在加入海外某大型科技公司之前,我曾在餐厅工作过。我做过很多不同岗位,包括服务员、厨师、调酒师和外送员。我注意到,我工作过的大多数餐厅在应对繁忙时段,例如午餐高峰期的客流压力时,处理方式都大同小异。后来进入云计算领域后,我惊讶地发现,餐厅解决客流压力问题的方法,与我们在云端构建弹性系统的方法竟然非常相似。

在餐厅里,通常情况下,所有顾客都能很快入座并得到服务。然而,在某些情况下,即使餐厅整体看起来并不繁忙,一些突发状况也可能导致严重延误。例如,很多顾客同时到达或同时点餐,或者因为失误导致返工,比如不小心打翻一盘菜。不幸的是,我自己也经历过几次这样的情况。

当餐厅客流量超过服务能力时,顾客就会开始排队等候。无论是突发延误,还是持续排队,都会让顾客感到不满。为了应对这些问题,我们采取了一系列策略,努力为顾客提供稳定且始终如一的良好体验。

餐厅工作经历也影响了我后来外出就餐时的行为。我希望尽量减少对餐厅的额外负担。因此,如果门口已经排起长队,我会选择稍后再来,或者去别的地方。如果我向服务员提出某个请求,而服务员太忙忘记了,我会礼貌地再次询问。如果餐厅非常忙,而我的订单只是出现了一些小问题,我可能会选择不吃已经变凉的西兰花,而不是把整份订单都退回去。

我不想给自己和其他人造成额外延误。简单来说,我希望做一个有礼貌的餐厅顾客。不知不觉中,在进入科技行业之前,我已经从分布式系统的“服务端”和“客户端”两个角度体验过过载,并掌握了一些应对策略。

后来,当我把“服务器满负荷”的经验转向云端无服务器技术时,我继续借鉴自己作为餐厅员工和顾客的双重经验。在大型云服务团队中,我重点关注的领域之一,是如何提升系统弹性,尤其是如何控制过载。

我观察到,负载峰值影响云端系统的方式,与餐厅过载影响服务质量的方式非常相似。不过,两者之间也有一个显著差异:云服务具有弹性,可以根据需求增长快速扩展容量;而餐厅的容量,例如面积、座位数和厨房规模,通常是固定的,可能需要数年才能扩展。

因此,虽然餐厅过载很常见,但在云服务中,团队会尽最大努力防止过载发生。

自动化容量预测和自动扩展能力,有助于确保服务容量始终显著高于实际需求。团队通常会倾向于超额配置容量,从而提供容量缓冲。这种方式可以让客户摆脱传统本地部署中的容量规划压力,并帮助他们在云端构建更有弹性的服务。

例如,在某些服务中,团队会针对极小概率事件进行规划,比如单个可用区容量丢失,并在其他可用区中预先配置足够容量,以无缝吸收额外工作负载和预测使用量。

这也意味着,在正常情况下,云服务通常拥有足够容量应对显著的负载波动。真正的云端过载情况非常罕见。但当计划出现偏差,发生这类特殊事件时,系统仍然需要做好准备,避免影响客户体验。

在大型技术系统中,团队会采用多种策略防止过载。这些策略大多围绕如何构建安全可靠的服务展开。其中一部分是运维策略,用于在可观测性系统检测到过载后做出响应;另一部分是架构策略,作为服务设计的一部分,用于主动预防过载。此外,服务通常还会与依赖项交互,因此团队也需要确保自身作为依赖项调用方时行为良好,不会给下游系统造成额外压力。

这些技术的具体细节非常复杂,本文无法逐一展开。本文将概述一些常见的负载控制策略,说明每种策略的适用时机和原因,并分析它们的优缺点。希望这些内容能帮助你在自己的系统中选择合适的负载控制策略组合,从而构建安全可靠的服务、行为良好的客户端,或两者兼顾。

对于企业研发团队而言,弹性系统建设并不只是架构师或运维团队的单点工作,而是贯穿目标制定、需求评审、开发、测试、发布上线和故障复盘的长期工程。借助 PingCode 这类智能化研发管理工具,团队可以将弹性建设相关的需求、缺陷、测试结果、发布记录和 Wiki 复盘沉淀串联起来,让系统稳定性改进过程更加自动化、数据化和可追踪。

受保护的服务:管理需求与服务延迟

餐厅需要管理顾客需求,也就是客流量,同时也需要管理服务时间,也就是延迟,才能维持顾客期望的用餐体验。大家都知道,餐厅容量是有限的。桌椅、服务员和厨师数量都有限。因此,当餐厅客满时,只能按照顾客离开的速度安排新顾客入座。用餐服务时间延长,会减慢顾客离开的速度,进而降低新顾客入座的速度。

餐厅用于管理需求和服务时间的方法,有些是运维层面的。这些方法是在负载或延迟开始增加时采取的应对措施。另一些餐厅则会采用架构层面的方法:通过精心设计系统和工作流程,主动防止过载。这些方法可以让许多不同顾客同时用餐,同时确保每位顾客都能获得接近私人包间式的服务和关注。

在云服务中,团队同样希望避免系统过载。就像餐厅一样,系统也会同时采用运维策略和架构策略来应对过载,从而保持良好的客户体验。

运维实践:应对意外负载的策略

运维实践,是指在事件发生后,应对意外负载所采取的策略。通常,团队会组合使用这些策略,每种策略都提供不同层面的保护。下面将介绍一些有助于在过载情况下维持一致客户体验的运维策略。

负载卸除:保护核心服务能力

对我来说,餐饮业最令人头疼的职责之一,就是做各种“杂活”。这些杂活包括补充莎莎酱、补充玉米片、把刀叉卷进餐巾纸、加冰等。它们有助于餐厅运转,但通常没有专门人员处理。

当餐厅非常繁忙时,我们会优先处理最重要的工作,例如接受顾客点餐、续杯饮料和上菜,而把杂活暂时放到次要位置。这样才能满足不断增长的需求,并维持平均服务时间。

如果不调整优先级,就可能把大量时间浪费在对顾客体验并非最关键的工作上,导致餐厅整体效率下降。当然,忽视杂活不能永远持续下去。玉米片和莎莎酱总会用完,最终还是需要补充。

这种降低次要工作的优先级,与云系统中的负载卸除非常相似:系统会有意暂时丢弃或延后部分工作,以保护核心能力。

当系统接收到的负载持续增加时,可能会到达一个临界点。在这个点之后,额外负载会降低系统吞吐量,也就是降低系统在客户端超时时间内成功处理工作的能力。接收新工作、把工作放入处理队列、在不同进程之间切换上下文等操作,都会带来额外处理延迟。

负载卸除有助于防止情况进一步恶化。它可以保护服务免受瞬时负载峰值影响,并为团队争取时间来扩展资源,或解决问题根因。负载卸除的效果,是让系统在负载增加时仍能保持相对稳定的有效吞吐量。

团队可以从不同维度使用负载卸除。它可以按服务、API 或资源维度进行。在实施负载卸除时,构建一个能够识别过载状态的系统非常关键。因此,团队可能会关注请求速率、线程池利用率或队列深度等指标。

过载测试也是关键组成部分。它可以帮助团队识别负载临界点,并确保负载卸除能够让吞吐量保持稳定,而不是在达到临界点后迅速下降。

负载卸除对用户的影响通常比较粗粒度。它是一种相对直接的负载控制工具。虽然它可以延长服务维持高吞吐量的时间,但也会影响用户体验,因此不应成为唯一方案。团队通常会将负载卸除与其他策略结合使用,构建更完整的服务负载控制体系。

公平性和配额管理:避免单个租户占用过多资源

有时候,过高的负载可能是由单个顾客占用过多资源造成的。

在我曾经工作过的一家餐厅,节假日期间我们会限制顾客用餐时间,要求他们在用餐结束后及时离开。这样可以避免同一桌客人整晚占用一张桌子,从而让我们能够及时安排其他客人入座。通过限制每桌客人的用餐时间,我们能够在用餐高峰期接待更多顾客,也能让餐桌使用更加公平。

在云服务中,团队也会采用类似做法,以确保客户能够公平地使用可用资源。当单个客户造成过高负载时,系统不应对所有客户不加区分地进行负载卸除。相反,团队会采用一些技术,在多租户系统中尽量保持单租户体验,确保客户获得一致且可预测的性能。

为此,系统通常会通过请求速率或已配置资源数量衡量资源消耗,并通过服务配额强制执行限制。例如,系统可能会对 API 每秒事务数、可启动的计算实例数量,或账户中函数计算任务的并发数设置服务配额。

为了维护请求速率的公平性,系统可以使用令牌桶、漏桶、指数加权移动平均、固定窗口或滑动窗口等算法,限制超出配额的客户。其中,令牌桶是非常常见的一种方式。

不过,服务并非始终处于满负荷状态。因此,系统有时会允许客户通过突发流量,在一定范围内临时超过配额。令牌桶等算法正适合处理这类场景。

对于服务配额,成熟系统通常会公布默认值和已应用的配额信息,并提供可视化指标、仪表盘和告警能力,帮助客户主动管理配额。当客户接近配额阈值时,系统可以发出告警,提醒客户申请提升配额,或者检查资源是否存在异常使用。

公平性有助于在多租户环境中提供一致的单租户体验。但它的代价是:当系统不得不限制速率或强制执行配额时,客户体验有时会下降。与负载卸除类似,公平性也需要与其他策略结合使用,才能全面管理负载。

自动扩展:让容量领先于需求

当餐厅发现某种菜品在一天、一周或一个月内的使用量呈现上升或下降趋势时,会确保该菜品有足够库存,以满足未来消费需求。这些使用趋势有助于持续预测未来容量需求。

例如,随着季节从春季过渡到夏季,热汤订单可能减少,冷汤订单可能增加。随着季节变化,餐厅会减少热汤食材订购量,增加冷汤食材订购量,以满足预测需求并避免资源浪费。

但这种预测方式无法很好地应对需求突然激增。假设某个本来寒冷的月份突然出现一个全国性的冷汤促销日,如果餐厅没有提前做好准备,当天订单量就可能超过冬季正常库存。通常情况下,如果菜单上的某个菜品售罄,餐厅会暂时把它从菜单上移除,并停止接受相关订单。随后,餐厅会根据食材情况增加供应量,要么准备更多食材,要么等待下一批食材到货。

虽然这通常可以暂时缓解供应紧张,并为餐厅争取时间扩大生产,但如果需求激增只是短暂事件,那么在第二天增加冷汤供应可能就没有意义。云端自动扩展也面临许多类似问题。

在云服务中,确保充足容量的主要策略之一,是自动化容量预测。它可以帮助系统满足未来容量需求,并预留充足缓冲,以应对实际使用量的大幅波动,包括大型促销活动或重要业务节点带来的短期峰值。

团队还会主动监控服务配额,并在服务接近配额上限时自动申请增加配额,确保服务能够相应扩展容量。此外,系统还会预先配置资源,以应对某个可用区容量丢失,这相当于额外容量缓冲。最后,系统也会使用自动扩展能力,根据短期需求变化调整容量。自动扩展可以帮助服务保持弹性,同时优化容量配置。

然而,在极少数情况下,当流量突然激增并超出预测时,仅靠自动扩展可能并不足够,因为它的响应速度可能不够快。某些瞬时负载峰值可能会在自动扩展检测和响应所需时间之前,就超过系统预置容量。

这种情况通常需要通过负载卸除或速率限制来管理,以保护服务免受过载影响。

预测、自动扩展和预置容量,是确保云弹性、提供良好客户体验并减少不必要浪费的关键工具。大型技术团队通常会投入大量资源进行容量管理,以应对突发且显著的负载高峰。在极少数需求增长快于自动扩展处理能力的情况下,团队通常会结合负载卸除和速率限制来保护服务,同时扩展容量以满足需求,或等待负载高峰消散。

这些运维措施组合在一起,构成了管理过载的整体方法。

架构实践:从设计阶段预防系统过载

餐厅不仅会被动应对负载问题,也会主动构建工作流程、实践和交互机制,以帮助预防某些类型的过载。

例如,我曾经工作过的一些餐厅要求顾客提前预订,不接受直接到店的顾客。这可以避免周五晚餐高峰出现不可控客流,让餐厅能够以更可预测的客流量运营。

在云服务架构设计中,团队也会致力于减少可能发生的过载情况。下面将介绍一些用于预防过载的架构实践。

管理队列深度:避免亚稳态故障

当餐厅开始实行等位制度时,顾客会被加入等位名单。随着队伍越来越长,准确预测每位顾客的等待时间会变得越来越困难。餐厅需要有效管理排队情况,避免顾客等待过久。

如果等待时间过长,顾客可能会直接离开,转去别的地方。而且,等位名单也不能无限增长。除了提供准确等待时间外,餐厅还需要打烊,并为第二天营业做准备,员工也需要回家休息。

我也亲身经历过排队时间过长对顾客体验的影响。有时,顾客会在餐点上桌前离开,因为他们已经等得太久。如果厨房不知道顾客已经离开,就会继续按照排队顺序,为那些永远不会吃到饭的顾客准备餐点。

随着顾客放弃等待,厨房会继续做一些无法提高有效出餐率的工作。如果更多顾客离开,这种情况可能会持续下去,类似一种亚稳态故障。为了避免这种持续影响,服务员需要及时告知厨房哪些桌的顾客已经离开,这样厨师才能优先处理仍在等待的顾客订单。

在云系统中,过深的先进先出队列也会造成类似问题。工作进程可能会浪费时间和资源,处理客户端已经放弃的消息。

因此,系统设计需要防止队列深度过大,避免因此导致过高延迟和亚稳态故障。常见方法有两类:第一,通过速率限制或负载卸除限制队列大小,拒绝过多工作;第二,采用类似后进先出的策略,优先处理最近接收到、最有可能成功的工作。

这些策略可以提高系统执行有效工作的概率,从而提供更一致的客户体验。它们也可以让团队在最大容量下测试队列,确保系统在负载下仍然具有可预测性能。不过,这些策略也可能导致某些工作被拒绝,或者系统根据成功概率有意忽略某些工作。

恒定工作量:用可预测负载平滑峰值

在我之前工作过的一家餐厅,我们有时会举办私人活动。客人可以自由进出,没有排队等候,至少在符合消防规范的范围内是这样。客人来来往往,我们很难预估他们会停留多久。

这就造成了服务难题。如果我们为每位客人单独点单,并将每份订单都提交给厨房,订单量就会剧烈波动,非常不稳定。这可能导致严重订单积压。此外,客人可能在拿到餐点之前就离开,造成时间和食物浪费。

在这些活动中,我们会通过持续不断地工作来避免服务过载。用餐饮业术语来说,我们会定期派服务员端上开胃小菜和小碟菜,从而减轻厨房压力。员工知道自己需要按时上菜,并且只需要准备固定数量的菜肴。我们会按场地最大容纳人数规划尽可能多的宾客。

这种方式可以为顾客提供良好的用餐体验。每个人都可以自由享用食物,而且新的菜品会很快端上来。厨房和服务人员的工作量不会因为宾客人数的即时变化而剧烈波动。不过,缺点是可能出现食物浪费和额外工作量。

在构建云服务时,如果预期需要处理变化率极高的数据,团队通常会选择“恒定工作量”策略来平滑变化率波动。

恒定工作量意味着系统不会根据负载或压力频繁扩展或缩减。在几乎所有情况下,系统都执行基本相同的工作量,从而获得可预测负载,避免出现峰值。例如,服务器集群不会在每次配置更新时都被动接收推送,而是每隔几秒钟轮询一次对象存储桶,获取配置文件并应用它,即使文件实际上没有变化。

当然,这意味着系统必须能够承受这种持续工作量,并且可能会在并不需要的时候执行工作,从而引入额外成本。就像餐厅可能会浪费食物一样,这种模式也会在系统中引入不必要的工作。不过,在许多场景下,这种成本通常可以被它带来的弹性和稳定性收益抵消。

但这种模式并不适用于所有情况。有些系统确实需要事件驱动架构。

让规模较小的服务掌握控制权

在我的餐饮生涯中,我曾做过一段时间调酒师。我记得,在最繁忙的夜晚,当餐厅从提供餐食切换为只提供酒水时,吧台前常常会排起两三排顾客等着点单。通常,在那些繁忙夜晚,顾客数量至少是调酒师人数的十倍。

我们需要控制点单速度,避免所有顾客同时喊出自己的订单,让调酒师应接不暇。为了控制点单速度,我们会在吧台前来回走动,冷静地询问每位顾客:“您想喝点什么?”这样,人数较少的调酒师就能更好地掌控顾客需求。调酒师是主动从顾客那里获取订单,而不是被动等待顾客把订单推给他们。

当我思考云服务中控制平面和数据平面之间的交互时,经常会想起这个策略。

许多云服务都由这两类组件构成。控制平面 API 提供服务的管理和配置能力,例如创建、更新和删除资源。数据平面则提供已配置资源的核心日常功能。通常,控制平面的使用量远低于数据平面。因此,构成控制平面的资源集群,也比构成数据平面的资源集群小得多。

然而,数据平面的运行依赖来自控制平面的信息。因此,团队需要仔细考虑:数据平面应该从控制平面拉取信息,还是由控制平面把信息推送给数据平面?

通常,团队会选择让规模较小的资源集群控制数据流转,也就是由控制平面向数据平面推送信息。这可以防止规模较大的数据平面资源集群对控制平面资源集群造成过载。

这种策略允许服务维护彼此独立的组件,使它们可以根据自身需求独立扩展,同时还能避免潜在过载。但这种策略也存在挑战。

较大的集群需要维护所有从较小集群推送来的数据,并在数据丢失或损坏时实施恢复机制。较小的集群则需要维护较大集群中每个节点的清单,并平均分配推送更新的工作。最后,较小的集群还需要管理更新收敛,确保较大集群中暂时不可用的节点最终也能收到更新。

避免冷缓存:防止下游数据源被压垮

在我工作过的餐厅里,我们通常会提前准备一些最受欢迎的菜品。这有助于加快餐厅开门后的备餐速度。例如,我们会提前炸好一大批薯条,提前准备好沙拉干料,或者预先加热几锅汤。前面提到的许多异步辅助工作,都围绕提前准备好的食材展开。

这套系统运行良好,直到某种预先准备好的食材用完为止。食材用完可能是因为我们减少了辅助工作的投入。例如,当汤用完时,我们就得回到冷藏室,再拿一锅新汤,并从头开始加热,这比逐步补充要慢得多。

为了按时完成午餐的汤和三明治套餐订单,服务员想出了一个临时办法:用微波炉加热汤。于是,你可能会看到厨房里排起长队,服务员们都在等着微波炉加热汤。这个变通方法会给微波炉带来很大压力,而微波炉一次只能加热几杯汤。这种情况与云系统中需要避免的冷缓存问题非常相似。

系统通常会添加缓存,以减少尾延迟、降低成本,或掩盖下游瞬态错误。但随着时间推移,服务往往会围绕这些改进进行优化,缓存也会从辅助组件逐渐变成关键组件。

当缓存因为服务重启、缓存中断或流量模式变化等原因失效时,缓存原本承担的所有工作都会直接落到下游数据源上。如果下游数据源只针对典型负载进行扩展,例如只承担 10% 的读取量,那么这种流量变化就可能压垮下游,导致延迟超过客户端超时时间,进而降低用户感知到的可用性。

为了避免这种情况,系统通常会在缓存预热期间通过速率限制或负载卸除限制并发,防止下游过载。某些服务也可以通过跟踪底层数据存储中最近使用或最常使用的条目,并优先加载这些数据来预热缓存。

在其他情况下,系统会始终从源端读取数据,同时使用缓存,以消除缓存可能引入的双峰行为,并确保数据库始终能够扩展以处理最大负载。这会牺牲一部分性能优势,但也能让缓存继续在源端故障时提供冗余能力。

团队也需要认真思考,缓存是否真的是解决问题的合适方案。

行为良好的客户端:减少对依赖项的额外压力

正如前面提到的,餐厅经历让我想成为一个有礼貌的顾客。我理解服务员和厨房工作人员的压力,尤其是在餐厅繁忙的时候。因此,我不想给任何人增加负担。

在云系统中,团队也遵循类似原则:不要让已经承受压力的依赖项雪上加霜。系统不仅要成为可靠服务,也要成为行为良好的客户端。

下面结合餐厅经验,介绍两种用于避免给依赖项施加过大压力的模式。

断路器:控制依赖项过载扩散

尽管团队会尽最大努力,厨房偶尔还是会不堪重负,无法满足顾客需求。在最糟糕的情况下,餐厅会暂停点餐服务,也就是暂停接受新订单,以便厨师完成已经排队的餐点。等出餐恢复正常后,再重新接受新订单。

在厨房压力巨大时,服务员会尽量表现得体,不继续给厨房增加负担。但这种做法有时会产生意想不到的后果,把小问题放大。

例如,假设餐厅因为烧烤区订单过多而暂停所有点餐。如果油炸区和沙拉区并没有延误,那么停止接受他们仍然可以完成的订单,就是不必要的。这会影响那些只想点沙拉或薯条的顾客。

在分布式系统中,这种防止依赖项持续过载的方法称为断路器模式。它模仿了电气系统中断路器防止单个电路电流过大的原理。

软件中的断路器通常有三种状态。初始状态是关闭状态,此时请求正常流向依赖项。如果依赖项延迟或故障超过阈值,断路器会进入打开状态,此时请求会被阻塞。经过一段时间的退避后,断路器会进入半打开状态,此时系统会向依赖项发送少量请求,以评估其健康状况。如果响应重新达到健康阈值,断路器会关闭;如果响应仍然不健康,断路器会再次进入打开状态。

断路器可以从两个方面防止过载。

首先,它可以阻止服务持续对依赖项造成过载。其次,它可以防止服务在等待依赖项请求失败或超时期间,因为额外并发耗尽自身资源,例如线程、CPU 周期或网络端口。

断路器的潜在弊端,可以从前面的餐厅例子中看出来:一个小事件可能被放大。断路器一旦打开,就会故意拒绝客户端请求,而不给依赖项任何成功机会。事实上,某些对依赖项的请求可能根本不受过载影响,但仍会被断路器拒绝,从而不必要地提高失败率。

例如,某个数据分区因为热点请求而过载,导致断路器打开并停止所有对该服务的请求。但其他数据分区的请求并未受到影响,却仍然被拒绝。

因此,团队需要谨慎选择断路器的使用位置,并且不应对依赖项一视同仁。依赖项故障可能发生在单个分区、故障边界、主机或客户等多个层面。这意味着断路器粒度通常应比“整个依赖项”更细,并与依赖项预期故障域相匹配。

这种更细粒度的断路器实现会增加复杂度,也需要理解依赖项的架构或分区方式。设置合适的断路器阈值同样具有挑战。团队需要确保单个客户端在受到速率限制或触发潜在错误时,不会意外触发影响所有用户的断路器。断路器范围设置不当或调优不当,可能造成更大影响。因此,当团队选择使用断路器控制负载时,需要对这些阈值进行广泛且持续的测试和验证,防止意外影响。

自适应重试:避免重试风暴

以前做服务员时,即使我把顾客需求记下来,也经常会忘记一些事情。因为有很多事情会分散注意力,比如迎接新来的客人、送餐,或者完成一些杂活。

现在,当我作为顾客去餐厅吃饭时,如果我的要求没有很快得到满足,我通常会等一会儿,看看服务员是否还记得,然后再问一遍。通过重复询问,即使服务员暂时被其他事情分心,我也更有可能得到自己想要的东西,比如多要一份沙拉酱。

但我也会控制自己询问的频率和次数。如果每次服务员路过时我都拦住他们要沙拉酱,只会让他们更难回到厨房。他们也可能开始不耐烦,甚至忽略我。这样一来,我反而降低了自己成功获得服务的机会。

在分布式系统中,重试机制可以掩盖瞬态错误和速率限制,从而透明地提高依赖项可用性。重试策略有多种实现方式。一种常见方式,是允许客户端使用指数退避,并在每次请求之间加入抖动,对每个请求最多重试 N 次。

这种方法的缺点是,当系统可能已经因过载而性能下降时,重试会继续增加系统工作负载,造成额外负面影响。

回到餐厅例子,假设一位非常不耐烦的顾客想要多加一份沙拉酱。他不会只向服务员提出一次要求,而是会询问所有路过的服务员,直到满足需求为止。如果服务员之间没有明确协调,那么为这位顾客提供一份沙拉酱所需的工作量,就会乘以被询问的服务员数量。而且,由于每个服务员都可能去拿一份沙拉酱,最终还可能造成浪费。

如果请求需要经过调用链中的多个服务,情况会更加糟糕。工作量会随着调用链深度呈指数级增长。最终对最后一个依赖项的调用次数可能达到 k 的 n 次方,其中 k 是每个节点的重试次数,n 是调用链深度。

为了减少工作量放大,系统经常会使用一种称为自适应重试的方法。这种重试模式仍然使用带抖动的指数退避,但会增加两种保护机制。

第一种保护机制,是使用令牌桶实现重试配额。每次重试都会从令牌桶中消耗一定数量的令牌。不同错误对应的重试可能消耗不同数量的令牌。例如,因超时而进行的重试,可能比因内部服务错误而进行的重试消耗更多令牌,因为前者可能表明依赖项已经过载到无法及时响应。

成功响应会允许令牌桶补充一部分重试所需的令牌,而错误响应不会。这样,客户端可以在错误率较低时进行重试,但在错误率较高时受到限制。这限制了依赖项受损时流量增长的最大值,并显著降低放大效应。

第二种保护机制,是客户端速率限制,它也可以通过令牌桶实现。这种方法同时适用于原始请求和重试请求,以便动态适应依赖项容量,尤其适用于已被限流的请求。

在这种情况下,令牌桶的填充率和容量会动态变化。成功响应后,填充率和容量增加;收到限流响应时,填充率和容量减少。这有助于客户端动态响应不断变化的容量状况,并减少因限流而对依赖项造成的额外工作。通常,系统会先应用客户端速率限制逻辑,然后在需要重试时评估可用重试配额。

同样重要的是,无论执行速率限制还是使用断路器,都应尽可能将其放在靠近请求源头的位置,以最大程度减少工作负载放大。

实践表明,重试并不总是安全的。自适应重试通过在两个目标之间取得平衡,帮助系统成为行为良好的客户端:一方面提高瞬态错误期间的成功率,另一方面最大限度减少对处于压力下的依赖项施加额外负载。

运维可见性:监控资源利用率与故障维度

在使用本文提到的任何策略时,团队都必须建立良好的运维可见性,才能有效运行弹性系统。

在餐厅中,通常会有一位前厅经理,负责管理服务员、接待员和调酒师;也会有一位后厨经理,负责管理厨房员工。这些经理会持续检查餐厅各个环节的运营情况,判断是否存在积压和瓶颈,并针对不同情况采取正确应对措施。

在分布式系统中,团队也需要类似洞察力。

首先,系统需要从不同视角进行衡量,就像餐厅需要前厅和后厨两个视角一样。如果一家餐厅只从厨房角度衡量等待时间,就可能忽略餐厅外排队的顾客。对云服务来说,全方位衡量非常重要,它可以帮助团队全面了解服务,并知道负载影响了哪些方面。

其次,团队需要衡量资源利用率。资源可能会耗尽,因此在资源即将耗尽之前提供预警指标非常关键。这些资源包括 CPU、内存、磁盘空间、线程池、队列或服务配额等。所有这些资源都可能因过度使用而耗尽。这种过度使用通常表明系统负载过高,团队希望在资源真正耗尽之前就发现问题,从而采取策略避免影响客户。

第三,考虑指标维度非常重要,因为这能确保观测结果足够精确。系统级错误率可能掩盖某个单独 API 中发生的错误。因此,指标需要包含每个 API 的维度,以便针对每个 API 的错误率发出告警。

此外,团队还需要分别衡量不同故障域。例如,可用区代表一种故障域,因此也需要为每个可用区生成指标,以便针对单个可用区中的资源故障做出响应。

在某些情况下,这些维度可能具有很高基数,也就是存在大量唯一键,例如集群中的主机或客户。这使得为每个唯一键创建告警和仪表盘变得不现实。因此,团队通常会将指标和日志结合起来,并使用聚合查询找到某个指标的前 N 个贡献者。随后,可以根据贡献者数据创建仪表盘和告警。这有助于回答一些关键问题,例如“哪些客户给服务带来了过大负载?”或“是否有某台主机产生的延迟明显高于其他主机?”

最后,建立以弹性和卓越运营为核心的工程文化也非常关键。成熟团队通常会定期审查仪表盘和指标,确保运维人员在出现问题时能够迅速行动。他们也会检查告警阈值,并确保指标足够完善。持续检查有助于团队确保自己拥有足够的运维可见性,能够在发生负载事件时及时发现并做出响应。

这些运维实践不仅依赖监控系统,也依赖清晰的任务分派、文档沉淀、沟通同步和复盘跟进。团队可以通过 Worktile 这类通用项目协作系统统一管理故障响应任务、运维文档、IM 沟通、日历排期、甘特图、工时记录和审批流程,让弹性系统建设中的跨团队协作更加清晰可控。

实践中,可见性越高越好。通过持续迭代地监控服务,团队可以获得更深入的洞察,从而更快、更准确地做出反应。

结论:组合使用负载控制策略提升系统弹性

管理负载的主要策略,是主动预测和扩展,让容量领先于客户需求,并提供足够缓冲来应对显著的负载波动。

在极少数情况下,负载会超过预置容量。此时,本文概述的策略与适当的运维可见性结合起来,可以帮助团队更好地控制如何应对过载。

每种策略都有特定应用场景和优缺点,但组合使用这些方法,可以缓解多种类型的过载影响。本文讨论的大多数策略都适用于请求—响应式服务。但团队也需要思考,这种架构是否是最合适的选择,或者发布—订阅、批处理、推送式 WebSocket 架构等天然更不易过载的架构,是否更符合实际需求。

通过运营策略和架构策略,让客户端行为良好,并让服务安全可靠,是构建弹性系统的关键。这些策略能够帮助系统提供一致、可预测的客户体验。

成熟团队通常会采用经过充分测试的策略,并将它们集成到通用库或基础服务中,从而简化工程实践,方便不同团队采用。无论是由于服务突然爆红、某个行为异常的客户发送过多请求,还是其他因素组合导致负载升高,这些策略都能帮助服务持续提供顶级餐厅般稳定而卓越的体验。

文章包含AI辅助创作:弹性系统设计:从午餐高峰看云服务过载治理,发布者:shang,转载请注明出处:https://worktile.com/kb/p/3975300

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
shang的头像shang

发表回复

登录后才能评论
注册PingCode 在线客服
站长微信
站长微信
电话联系

400-800-1024

工作日9:30-21:00在线

分享本页
返回顶部