我在 Amazon 的“服务框架”团队工作了多年。我们的团队编写了多种工具,帮助 Amazon Route 53、Elastic Load Balancing 等 AWS 服务的拥有者更快速地构建自己的服务,以及更轻松地为调用这些服务的客户端提供服务。其他 Amazon 团队为服务拥有者提供各种功能,例如计量、身份验证、监控、生成客户端库和生成文档。各个服务团队不必手动将这些功能集成到各自的服务中,而是由“服务框架”团队执行一次此集成,然后通过配置向各项服务公开这些功能。

我们面对的一个挑战是确定如何提供合理的默认值,特别是对于与性能或可用性相关的功能。例如,我们无法轻松设置默认的客户端超时值,原因是我们的框架不知道 API 调用可能具有的延迟特性。服务拥有者或客户端自身同样很难清楚知道这些值,因此,我们不断尝试,并在整个过程中收获了一些有用的见解。

我们努力解决的一个常见问题是:确定服务器允许同时向客户端开放的默认连接数。此设置旨在防止服务器因承担过多工作而出现过载。更具体地说,我们想要配置与负载均衡器的最大连接数相称的服务器最大连接数设置。当时 Elastic Load Balancing 尚未出现,因此广泛使用了硬件负载均衡器。

我们着手帮助 Amazon 服务拥有者和服务客户端清楚知道在负载均衡器上设置的最大连接数的理想值,以及在我们提供的框架中设置的相应值。我们断定,如果可以清楚知道如何运用人的判断力来作出选择,我们就能编写软件来模仿这种判断力。

确定理想值最终带来了巨大挑战。将最大连接数设置得过低时,负载均衡器可能会制止请求数增加,即使服务能力仍然充裕也是如此。将最大连接数设置得过高时,服务器会变得速度很慢和无响应。如果将最大连接数设置为恰好适合工作负载,工作负载会变化或者依赖项的性能会改变。此数值又将会出错,并导致不必要的中断或者过载。

最后,我们发现最大连接数概念不够准确,无法提供问题的完整答案。本文介绍了我们觉得很有效的其他方法,例如负载卸除。

过载问题剖析

在 Amazon 内部,我们将系统设计为在遭遇过载情况之前主动扩展,以此来避免过载。但是,保护系统涉及到分层保护。这从自动扩展开始,但还包括有条不紊地卸除超限负载的机制、监控这些机制的能力和持续的测试(最为重要)。
 
我们对服务进行负载测试时,发现服务器在低使用率下的延迟要低于高使用率下的延迟。在负载较重时,线程争用、上下文切换、垃圾收集和 I/O 争用变得更加明显。最终,服务会到达性能开始更急剧下降的转折点。
 
这个观察结果背后的理论称为 通用可伸缩定律,它衍生自 阿姆达尔定律。该理论指出,虽然可以使用并行化来提高系统吞吐量,但系统吞吐量最终受制于串行化点的吞吐量(即受制于无法并行化的任务)。
 
不幸的是,吞吐量不仅受制于系统资源,而且在系统过载时通常也会降低。如果系统承担的工作超过了其资源能够支持的数量,系统会变慢。计算机即使处于过载状态也会承担工作,但会花费更多的时间进行上下文切换,并会变得很慢,从而不可用。
 
在客户端与服务器通信的分布式系统中,客户端在等待一段时间后常常会变得不耐烦,并会停止等待服务器响应。这段时间称为超时时间。如果服务器因过载而导致其延迟超过客户端的超时值,请求会开始失败。下图显示服务器响应时间如何随着提交的吞吐量(以每秒事务数为单位)增加而延长,最终到达情况迅速恶化的转折点。

在前图中,当响应时间超过客户端超时值时,可以清楚看到情况很糟糕,但图中并未显示到底有多糟糕。为了说明这一点,我们可以在延迟旁边画出客户端感知到的可用性。我们不使用常规的响应时间测量值,而是改用中值响应时间。中值响应时间表示 50% 的请求比中值快。如果服务的中值延迟时间等于客户端超时值,则表示一半的请求超时,因此可用性为 50%。在这里,延迟增加将延迟问题转化为可用性问题。下图描绘了所发生的事情:

遗憾的是,此图难以看懂。要描述可用性问题,更简单的方法是区分有效吞吐量吞吐量。吞吐量是指每秒发送到服务器的请求总数。有效吞吐量是吞吐量的子集,它在接受处理的过程中没有产生错误,而且具有足够低的延迟,可让客户端利用响应信息。

正反馈循环

过载情况如何在反馈循环中放大自身影响是它的一个隐患。在客户端超时的时候,客户端出错已经很糟糕了。更糟糕的是,服务器到当时为止在该请求上取得的所有进展都会变得无功而返。系统在过载情况下(此时能力受到限制)最不应该做的是浪费工作。

让事情进一步恶化的是客户端经常会重试请求。这成倍加大了向系统提交的负载。如果面向服务的架构中有足够深的调用关系图(即客户端调用一项服务,后者调用另一些服务,这些服务又调用其他服务),以及如果每一层都执行多次重试,则底层中出现的过载会导致逐级发生重试,从而以指数方式放大提交的负载。

当这些因素结合起来时,过载会建立起自己的反馈循环,导致过载进入稳定状态。

防止浪费工作

卸除负载表面上看起来很简单。在服务器将要出现过载时,它应开始拒绝超限的请求,以便专心处理它决定接受的请求。卸除负载的目标是确保延迟对于服务器决定接受的请求足够低,以便服务在客户端超时之前作出响应。借助此方法,服务器对于它接受的请求能保持高可用性,而且只会影响超限流量的可用性。

通过卸除超限负载来控制延迟能提高系统的可用性。但是,在前图中难以呈现此方法的好处。总体可用性直线仍然向下移动,看起来很糟糕。关键之处是:服务器决定接受的请求仍然可用,原因是它们迅速获得了处理。
卸除负载能让服务器保持其有效吞吐量,并尽可能多地完成请求,即使提交的吞吐量增加也是如此。但是,卸除负载并不是无代价的行为,因此,服务器最终受制于阿姆达尔定律,而且有效吞吐量出现下降。

测试

当我与其他工程师谈论负载卸除时,我想指出,如果他们在对服务进行负载测试时未恰好达到和未远远超出服务中断的程度,则他们应假定服务将会以最不希望出现的方式失败。在 Amazon 内部,我们花费大量时间对服务进行负载测试。通过绘出类似本文前面所示的图形,我们能够确定过载性能的基准,并跟踪我们在改变服务后的一段时间内的具体表现。

负载测试分为多种类型。某些负载测试确保队列会随着负载增加而自动扩展,而另一些测试使用固定队列大小。在过载测试中,如果服务的可用性随着吞吐量增加而快速降至 0,这是一个好的迹象,表明服务需要额外的负载卸除机制。理想的负载测试结果是:有效吞吐量在服务接近充分使用状态时趋于稳定,而且,即使应用更大的吞吐量,有效吞吐量也保持不变。

Chaos Monkey 这样的工具能帮助对服务进行混沌工程测试。例如,它们可以使 CPU 过载,或者引发数据包丢失情况,以模拟在过载期间发生的状况。我们使用的另一种测试方法是:选择一个现有的负载产生测试(所谓的“金丝雀”),持续向测试环境施加负载而不是增加负载,但开始从该测试环境中移除服务器。这增加了每个实例提交的吞吐量,因此可以测试实例吞吐量。这种通过减小队列大小人为地增加负载的方法对于单独测试服务很有用,但它不能完全取代满载测试。端到端的满载测试也会增加依赖该服务的其他服务的负载,从而能发现其他瓶颈。

在测试期间,我们除了测量服务器端的可用性和延迟之外,还确保测量客户端感知到的可用性和延迟。在客户端可用性开始下降时,我们将负载增加到远超该转折点的程度。如果负载卸除起作用,有效吞吐量将保持稳定,即使提交的吞吐量的增加幅度远超服务已扩展的能力也是如此。

在探索用于避免过载的机制之前,过载测试十分重要。每种机制都会带来复杂性。例如,思考一下我在本文开头提到的服务框架中的所有配置选项,以及确定合适的默认值有多困难。用于避免过载的每种机制都会增加不同的保护,但效果有限。通过测试,团队可以发现其系统的瓶颈,并确定处理过载所需的保护机制组合。

可见性

在 Amazon 内部,无论我们使用何种方法来防止服务出现过载,我们都会仔细思考需要哪些指标和何种可见性来确认这些防过载措施生效。

在断流保护拒绝请求时,这种拒绝行为降低了服务的可用性。如果服务出错,并在自己仍有能力时(例如,在最大连接数设置得过低时)拒绝请求,它将产生误判。我们努力使服务保持零误判率。如果团队发现服务的误判率定期不为零,则或许是将服务调整得过于敏感,又或许是个别主机不断地合理出现过载,并且可能存在扩展或负载均衡问题。在这种情况下,我们可能要对应用程序性能进行一些调整,或者可以转为使用更大的实例类型,以便能更从容地处理负载不均衡问题。

从可见性的角度看,在负载卸除拒绝请求时,我们确保有合适的工具来了解客户端是谁、其调用的是哪个操作以及任何其他将帮助我们调整保护措施的信息。我们还使用警报来检测防范措施是否正在拒绝任何很大的流量。出现断流时,我们首先要做的是增加能力和解决当前瓶颈。

围绕着负载卸除的可见性还有一个细小但重要的注意事项。我们发现,不要让失败请求的延迟拖累服务的延迟指标很重要。毕竟,对请求进行负载卸除时产生的延迟与其他请求相比应极低。例如,如果某服务对其 60% 的流量进行负载卸除,该服务的中值延迟可能会令人惊叹(即使成功请求的延迟很可怕),原因是快速失败的请求导致它被低估了。

负载卸除对自动扩展和可用区故障的影响

配置不当的负载卸除可能会禁止反应式自动扩展。思考以下示例:配置某项服务,以便进行基于 CPU 的反应式扩展,另外还为该服务配置负载卸除,以便在达到类似的 CPU 目标时拒绝请求。在此例中,负载卸除系统将减少请求数,以使 CPU 保持低负载,而反应式扩展将永远不会收到或获得被延迟的指示启动新实例的信号。

在我们设置用于处理可用区故障的自动扩展限制时,我们也仔细考虑了负载卸除逻辑。服务将扩展到此程度:在保持我们的延迟目标的同时,可用区的服务能力可能会变得不可用。Amazon 团队经常会查看 CPU 等系统指标,以估算服务接近其能力极限的程度。但是,在负载卸除机制下,队列在运行时接近请求将被拒绝的程度可能会比系统指标指示的程度更近,而且可能不会预配置超额的能力来处理可用区故障。在负载卸除机制下,我们需要额外确认对服务进行了破坏性测试,以了解任意时刻的队列容量和余量。

实际上,我们可以使用负载卸除来形成非高峰、非关键性的流量,从而节约成本。例如,假设某队列为 amazon.com 处理网站流量,它可能会断定搜索爬网程序的流量不值得扩展能力以实现可用区的完全冗余。但是,我们非常小心地使用此方法。并非所有请求的处理成本都相同,而且,要证明服务应同时为人类流量和卸除超限的爬网程序流量提供可用区冗余,必须仔细进行设计、不断进行测试和获得企业的认可。如果服务的客户端不知道服务以这种方式配置,则它在可用区发生故障时的行为可能看起来像是可用性出现关键性的大幅下降,而不是卸除非关键性负载。因此,在面向服务的架构中,我们力图尽可能早地(例如在接收客户端初始请求的服务中)推进这种形成流量的做法,而不是力图在整个堆栈中作出全局优先级决策。

负载卸除机制

在讨论负载卸除和不可预测的情况时,关注许多会导致断流的可预测情况也很重要。在 Amazon 内部,服务保持充足的超额能力,无需增加更多能力即可处理可用区故障。它们使用限制模式在各客户端之间确保公平性。

然而,尽管采用了这些保护措施和业务做法,但服务在任意时刻的能力数量是一定的,从而可能会因各种原因而过载。这些原因包括流量意外激增、错误的部署或其他情况导致队列容量突然丧失、客户端从发起处理成本低廉的请求(如缓存读取)变为发起处理成本高昂的请求(如缓存未命中或写入)等。在服务过载时,它必须完成已接受的请求,也就是说,服务必须防止自己断流。在本节的其余部分,我们将讨论一些注意事项,以及多年来我们用于管理过载的方法。

了解丢弃请求的代价

我们确保对服务进行的负载测试在程度上远超有效吞吐量趋于稳定的程度。采用此方法的一个关键原因是:确保当我们在负载卸除过程中丢弃请求时,丢弃请求的代价尽可能小。我们发现很容易就会错过意外的日志语句或套接字设置,这可能使丢弃请求的代价远远高于必要的代价。

在极少数情况下,迅速丢弃请求的代价可能高于继续处理请求。在此类情况下,我们会减慢被拒请求的速度,以便至少匹配成功响应的延迟。但重要的是,要在继续处理请求的代价尽可能低时(例如,在请求未捆绑应用程序线程时)这样做。

确定请求的优先级

在服务器过载时,它有机会将传入的请求进行分类,以决定接受和拒绝哪些请求。服务器将接收的最重要请求是来自负载均衡器的 ping 请求。如果服务器未及时响应 ping 请求,负载均衡器将在一段时间内停止向该服务器发送新请求,该服务器将处于空闲状态。在断流情况下,我们最不应该做的是减小队列大小。除 ping 请求之外,请求优先级选项因服务而异。

思考一个提供数据以呈现 amazon.com 的 Web 服务。如果服务调用旨在支持为搜索索引爬网程序呈现网页,则处理该调用可能没有处理源自人类的请求那么重要。处理爬网程序请求很重要,但最好是能将这些请求转移到非高峰时间来处理。但是,在像 amazon.com 这样有大量服务合作的复杂环境中,如果服务使用相冲突的优先级启发式算法,可能会影响整个系统的可用性和浪费已做的工作。

可以同时使用优先级和限制来避免受到严格的上限限制,同时仍然防止服务出现过载。在 Amazon 内部,当我们允许客户端突破已为其配置的限制时,这些客户端发出的超限请求在优先级上可能会低于其他客户端发出的未超限请求。我们投入大量时间重点研究置放算法,以最大限度减小突增的容量变得不可用的可能性,但权衡之下,我们更偏向于可预测的预配置工作负载,而不是不可预测的工作负载。

密切关注时钟

如果服务器在处理请求的中途注意到客户端已超时,它可以忽略剩余的工作,并在当时让请求失败。否则,服务器将继续处理请求,而它迟来的响应就像是森林中的一颗树倒下一样。从服务器的角度看,它返回了成功的响应。但从已超时的客户端的角度看,这是一个错误。

避免这种工作浪费的一种方法是:客户端在每个请求中包含超时提示,这些提示告诉服务器客户端愿意等待多长时间。服务器可以评估这些提示,然后以微小的代价丢弃注定失败的请求。

此超时提示可以用绝对时间或持续时间来表示。不幸的是,众所周知分布式系统中的服务器在商定确切的当前时间方面表现很差。为弥补此不足,Amazon Time Sync Service 会将 Amazon Elastic Compute Cloud (Amazon EC2) 实例的时钟与每个 AWS 区域中一系列由卫星控制的冗余时钟和原子钟进行同步。在 Amazon 内部,正确同步的时钟对于日志记录用途也很重要。比较时钟不同步的服务器上的两个日志文件会使故障排除甚至比开始时更困难。

“观察时钟”的另一种方法是在单台计算机上测量持续时间。服务器擅长在本地测量已经过的持续时间,原因是服务器无需与其他服务器达成一致。遗憾的是,用持续时间来表示超时也存在问题。举个例子:在服务器通过网络时间协议 (NTP) 同步时,您使用的定时器必须是单调定时器,并且不能倒退。还有一个困难得多的问题,即为了测量持续时间,服务器必须知道何时启动秒表。在过载极其严重的某些情况下,大量请求可能在传输控制协议 (TCP) 缓冲区中排队,因此,等到服务器从其缓冲区中读取这些请求时,客户端已经超时。

每当 Amazon 的系统表达客户端超时暗示时,我们都尽力通过可传递的方式应用这些暗示。在面向服务的架构包含多个跃点的地方,我们会在每个跃点之间传播“剩余时间”最后期限,以便位于调用链末尾的下游服务能够知道它有多少时间让其响应发挥作用。

在服务器知道了客户端最后期限之后,会出现以下问题:在服务实现中的哪个地方强制实施最后期限?如果服务具有请求队列,我们会利用该机会在每个请求出队列之后估算超时。但这仍然相当复杂,原因是我们不知道请求可能需要多长时间。某些系统保存了 API 请求所需时间长度的估算值,如果客户端报告的最后期限超过延迟估算值,则这些系统会提前丢弃请求。但是,事情很少会这样简单。例如,缓存命中比缓存未命中更快,但估算器预先并不知道是命中还是未命中。或者,服务的后端资源可能被分区,而且只有一些分区可能很慢。有许多机会可以发挥才智,但这些才智也可能会在不可预测的情况下起到反作用。

根据我们的经验,在服务器上强制实施客户端超时虽然很复杂和要作出折衷,但仍然比其他选择更好。我们发现,强制实施“每个请求的生存时间”并丢弃注定失败的请求很有帮助,与之相反的是,请求积压起来,并且服务器可能会处理对任何一方都不再重要的请求。

完成已开始的工作

我们不希望浪费任何有用的工作,特别是在过载情况下。丢弃工作会创造增大过载的正反馈循环,原因是客户端常常会在服务未及时响应时重试请求。发生此情况时,一个消耗资源的请求会变为许多个消耗资源的请求,从而成倍加大服务负载。在客户端超时并重试时,它们通常会在第一个连接上停止等待响应,同时会在一个单独的连接上发起新的请求。如果服务器完成第一个请求并发送响应,客户端可能不会听到此响应,原因是它此时正在等待重试的请求发回响应。

为了解决此浪费工作问题,我们尝试将服务设计为执行有界限的工作。在我们公开可能返回大型数据集(或根本就是任何列表)的 API 的地方,我们将其公开为支持分页的 API。这些 API 返回部分结果和一个令牌,客户端可以使用此令牌请求更多数据。我们发现,当服务器处理具有内存、CPU 和网络带宽数量上限的请求时,更容易估计服务承受的额外负载。如果服务器不知道需要什么才能处理请求,执行准入控制会变得非常困难。

围绕着客户端如何使用服务的 API,可以找到与确定请求优先级有关的更微妙的机会。例如,假设某服务有两个 API:start()end()。为了完成工作,客户端需要能够调用这两个 API。在此情况下,该服务应使 end() 请求优先于 start() 请求。如果它让 start() 优先,客户端将无法完成已开始的工作,从而导致断流。

分页是另一个观察浪费的工作的地方。如果客户端必须发起多个连续的请求才能给服务的结果分页,而且在第 N-1 页后看到请求失败并丢弃结果,则它浪费了 N-2 次服务调用和它在整个过程中执行的任何重试。这表明,像 end() 请求一样,第一页请求的优先级应低于后续页的分页请求。它还突出说明了为何我们将服务设计为执行有界限的工作,并且不要无休止地给这些服务在同步操作中调用的服务分页。

监视队列

在管理内部队列时,查看请求的持续时间也很有帮助。许多现代服务架构均使用内存中队列来连接线程池,以便在工作的不同阶段处理请求。带有执行程序的 Web 服务框架的前面可能配置有队列。对于任何基于 TCP 的服务,操作系统为每个套接字维护一个缓冲区,这些缓冲区可能包含大量积压的请求。

当我们从队列中取出工作时,我们利用该机会查看工作在队列中停留了多长时间。至少,我们尝试在服务指标中记录该持续时间。除了界定队列大小外,我们还发现,对传入的请求停留在队列中的时间长度设置上限极其重要,而且我们会丢弃太旧的请求。这解放了服务器,使其能处理成功机会更大的较新请求。作为此方法的终极版本,我们寻找办法来改用后进先出 (LIFO) 队列(如果协议支持的话)。(给定 TCP 连接上用于发送请求的 HTTP/1.1 管道机制不支持 LIFO 队列,但 HTTP/2 一般支持此类队列。)

在服务过载时,负载均衡器也可能会使用名为波动队列的功能将传入的请求或连接加入队列。这些队列可能会导致断流,原因是当服务器最终收到请求时,它并不知道请求在队列中停留了多长时间。通常很安全的默认做法是使用溢出配置,它会让超限的请求快速失败而不是加入队列。在 Amazon 内部,这项知识已融入到下一代 Elastic Load Balancing (ELB) 服务中。Classic Load Balancer 使用了波动队列,但 Application Load Balancer 会拒绝超限的流量。无论配置如何,Amazon 的团队都会监视其服务的相关负载均衡器指标,如波动队列深度或溢出计数。

根据我们的经验,监视队列的重要性怎么强调也不为过。在我依赖的系统和库中,我经常惊讶地发现我在直觉上认为不会查看的内存中队列。当我深入研究系统时,我发现假定在我尚未知道的某个地方存在队列是很有帮助的。当然,过载测试提供了比深入研究代码更有用的信息,前提是我能够想出符合实际的合适测试用例。

在下层防止过载

服务由多层组成(从负载均衡器到具有 netfilteriptables 功能的操作系统,再到服务框架,然后到代码),而且每一层都提供了保护服务的功能。

像 NGINX 这样的 HTTP 代理通常支持最大连接数功能 (max_conns),以便限制它将传递给后端服务器的活动请求或连接的数量。这可能是很有帮助的机制,但我们学会了将它用作最后的手段而不是默认的保护选项。使用代理时,很难确定重要流量的优先级,而且原始处理中请求的计数跟踪有时提供的关于服务是否实际过载的信息并不准确。

在本文开头,我说明了当我在“服务框架”团队工作时遇到的挑战。当时,我们尝试向 Amazon 团队提供在其负载均衡器上配置的最大连接数的建议默认值。最终,我们建议这些团队将其负载均衡器和代理的最大连接数设置为较高值,并允许服务器利用本地信息实现更准确的负载卸除算法。但同样重要的是,最大连接数的值不能超过服务器上的侦听器线程、侦听器进程或文件描述符的数量,以便服务器有充足的资源处理来自负载均衡器的关键运行状况检查请求。

用于限制服务器资源使用量的操作系统功能很强大,而且在紧急情况下很有用。由于我们知道可能会发生过载,因此,我们将合适的 Runbook 与准备好的特定命令一起使用,确保为过载做好准备。iptables 实用程序能为服务器将接受的连接数设置上限,并能拒绝超限的连接,而且成本比任何服务器的处理成本低得多。还可以为其配置更复制的控制措施,例如允许以限定的速率建立新连接,甚至允许按照源 IP 地址限制连接速率或数量。源 IP 筛选器功能强大,但不适用于传统的负载均衡器。不过,ELB 网络负载均衡器会通过网络虚拟化保留调用方(即使位于操作系统层)的源 IP,从而使源 IP 筛选器之类的 iptables 规则发挥预期作用。

层内保护

在某些情况下,服务器会耗尽资源,以便在不减慢速度的情况下拒绝请求。考虑到这一实际情况,我们查看服务器及其客户端之间的所有跃点,以了解它们如何合作和帮助卸除超限的负载。例如,多个 AWS 服务默认情况下包含了负载卸除选项。在我们通过 Amazon API Gateway 将服务前置时,我们可以配置任何 API 都将接受的最大请求速率。当服务被 API Gateway、Application Load Balancer 或 Amazon CloudFront 前置时,我们可以配置 AWS WAF,使其在多个维度上卸除超限的流量。

可见性造成了难以解决的矛盾。提前拒绝很重要,因为这是成本最低的丢弃超限流量的地方,但这会降低可见性。因此,我们实施层内保护:允许服务器接受超过其处理能力的流量,然后丢弃超限的流量,并记录足够多的信息以了解它丢弃的流量。由于服务器可以丢弃的流量是有限的,因此,我们依靠位于服务器前面的层来防止服务器接收海量的流量。

换个角度思考过载

在本文中,我们讨论了以下现实如何催生对卸除负载的需求:由于向系统分配了更多的并行工作,以及由于资源限制和争用等力量的加入,导致系统变得更慢。过载反馈循环由延迟驱动,最终导致浪费工作、请求速率增大甚至产生更多过载。重要的是,在遇到过载时卸除超限的负载并保持可预测、稳定的性能,以此来避开这股由通用可伸缩定律和阿姆达尔定律驱动的力量。关注可预测、稳定的性能是一个关键的设计原则,Amazon 的服务均建立在此原则上。

例如,Amazon DynamoDB 是一种数据库服务,用于大规模提供可预测的性能和可用性。即使工作负载激增并超过了预配置的资源,DynamoDB 也能为该工作负载保持可预测的有效吞吐量延迟。DynamoDB Auto Scaling自适应容量按需等因素会快速作出反应,以增大有效吞吐量速率,从而适应增加的工作负载。在该段时间内,有效吞吐量保持稳定,并使高于 DynamoDB 的层内的服务也能保持可预测的性能,同时还改善了整个系统的稳定性。

在关注可预测的性能方面,AWS Lambda 提供了范围更广的示例。在我们使用 Lambda 实现服务时,每个 API 调用均获分配数量一致的计算机资源,并在自己的执行环境中运行,而且该执行环境一次仅处理一个请求。这不同于基于服务器的模式(在该模式中,指定的一个服务器处理多个 API)。

隔离每个 API 调用自己的独立资源(计算、内存、磁盘、网络)将在某种程度上绕过阿姆达尔定律,原因是一个 API 调用的资源将不会与另一个 API 调用的资源竞争。因此,如果吞吐量超过有效吞吐量,有效吞吐量将保持不变,而不是像在较为传统的基于服务器的环境中那样下降。这并非万能之计,原因是依赖项可能会变慢,并导致并发性升高。但是,在此情况下,至少我们在本文中讨论的主机上资源争用类型将不适用。

此资源隔离是现代无服务器计算环境(如 AWS FargateAmazon Elastic Container Service (Amazon ECS) 和 AWS Lambda)的一个优点,虽然不明显,但是很重要。在 Amazon 内部,我们发现,要实现负载卸除,必须做大量工作(从调整线程池,到选择最适合负载均衡器最大连接数的配置)。很难或无法为这些配置类型找到合理的默认值,原因是它们依赖于各个系统的独特操作特性。这些较新的无服务器计算环境提供下层资源隔离,并公开上层的旋钮式功能(如限制和并发控制),以防止过载。在某种程度上,我们并不追寻理想的默认配置值,而是能够完全避开该配置,并且根本无需任何配置就能防止各类过载。

了解更多内容

Universal Scalability Law(通用可伸缩定律)
阿姆达尔定律
Staged event-driven architecture (SEDA)(分段式事件驱动型架构)
Little's law(利特尔法则,描述系统中的并发性和如何确定分布式系统的容量)
Telling Stories About Little’s Law(讲述有关利特尔法则的故事),Marc’s Blog
Elastic Load Balancing Deep Dive and Best Practices(Elastic Load Balancing 深入探索和最佳实践),re:Invent 2016 上的演讲(介绍 Elastic Load Balancing 在停止对超限请求排队方面的演变)
Thinking in Promises: Designing Systems for Cooperation,作者:Burgess,出版社:O’Reilly Media,2015 年出版



关于作者

David Yanacek 是 AWS Lambda 的高级首席工程师。自 2006 年以来,David 一直在 Amazon 从事软件开发工作,之前致力于 Amazon DynamoDB 和 AWS IoT 以及内部 Web 服务框架和队列运营自动化系统的开发。在工作中,David 最喜欢做的就是执行日志分析并筛选操作指标,进而找到逐步提升系统运行流畅性的方法。

超时、重试和抖动回退 实施运行状况检查 检测分布式系统以获得运营可见性