我们可以通过对API速率限制来管理系统请求,防止过载。

Service-oriented architecture(SOA,面向服务的架构)具有非常清晰的权责划分,以及使系统之间耦合尽量松散。

公平

bin-packing algorithms

  1. 执行 placement algorithms 以在 fleet 中找到一个spot来运行新的workload。(类似于找到一个有足够容量来放置workload的bin)
  2. 持续监控每个workload和每个server的 utilization 来移动workload。(类似于在bins之间移动workload以确保没有bin过满)
  3. 监控整个fleet utilization,并根据需要增加或者减少capacity。(类似于在所有bins都即将装满之前增加新的bin,或者在他们都即将为空时减少bin)
  4. 只要系统没有得到充分的利用,就可以使工作负载超出硬分配的边界,并在系统得到充分利用时将工作负载保持在边界之内。(类似于允许workloads在每个bin中扩展,只要他们不会挤出其他workloads即可)

先进的算法结合了上面这些技术,比如一个fairness system可以monitor每个workload,评估是否有任何两个workloads可以充分利用某些的资源,之后将它们移动到一个bin中。只要一个workload没有充分利用其调配的资源,同一个bin中的另一个workload就可以借用这些资源。
在借用资源的时候,工作负载无需注意借用。如果工作负载需要使用所有调配的资源,则返回这些借用资源的时间几乎是瞬时的。
另外,workload需要可以在bins之间快速移动。如果一个繁忙的workload习惯于通过从其邻居那里借用超出其已调配的资源,但是其邻居改变了其行为并开始使用其更多的已调配资源,则需要将忙碌的workload移至另一个bin。

通过Load shedding增加公平

通常来说,随着系统负载的增加,系统应该自动进行扩展。最简单的方法是增加capacity并进行水平扩展。而对于像是基于AWS Lambda构建的无服务器架构的服务,由于可按需分配容量来处理工作,因此几乎可以即时进行水平扩展。对于有服务器服务,自动扩展就需要更长时间了。

通常对扩展的时间要求在几分钟之内即可,然而,如果服务器上的负载增加快于Auto Scaling,则我们需要采用快速故障策略,减去多余负载,这样可以为处理到的请求保持一致的性能。这样做的好处在于,系统的一切我们都是可预测的。

通常我们会将服务设计为尽快将不能处理的请求返回给客户端,以最大程度的减少服务器执行的工作量。但是这样会造成单机黑洞,由于相应快,而导致负载均衡器将更多的请求发送过来,因此我们需要故意放慢快速错误的响应速度,以匹配成功响应的延迟。

通常来自多个客户端的负载是不相关的,因此如果服务的总负载突然增加,很有可能是由单个客户端所引起的。出于平衡考虑,我们需要避免由于单个客户端的计划外负载而导致所有客户端中都出现请求失败的现象。对于这种情况,我们使用速率限制来限制计划外的流量增长。可以为每个client设定某个额度的资源和操作的最大值。这样,如果多客户端服务遇到了计划外的负载增加,则该工作负载的计划外部分将被拒绝,其他工作负载将继续以可预测的性能运行。

但是配额的使用会减少服务的可用性,当一个客户端的工作量超过其配额时,它将开始看到其多余的请求失败。但是实际上,该服务可能具有满足这些请求的能力。

通常会以 429 状态码返回 “超出API速率限制”响应。

状态码在500-599范围内意味着服务器由于某种原因而失败
状态码在400-499范围内意味着客户端正在做意外的事情,在本文所述的情况下,是计划外的API调用量

Note: 在实际应用时,会发现某些服务实际上为超出速率返回503状态码,这是因为2012年RFC 6585才正式将429状态码加入到HTTP规范中。因为为了保持 backward compatibility ,很多服务其实会对“超出API速率限制”返回503。

深入配额

服务所有者通常会为每个client配置配额。例如,对于AWS服务,Client通常是一个AWS account。有时,配额会放在比client更细粒度的位置上,例如放在Service拥有的特定资源上,比如DynamoDB表。服务的所有者需要定义规则,为每个调用者提供默认的quota。或者如果client预期即将出现的负载增加,则他们需要要求服务增加其配额。

quota的种类:

  1. the number of things the client can have running at the same time.例如Amazon EC2为特定AWS account可以启动的实例数量实施配额。
  2. rate-based quota. 基于比率的配额通常以 “每秒请求数” 这样的单位进行衡量。本文主要着重于基于比率的配额,但文中的讨论在另一种配额中也大体适当。

下图演示了配额的使用。它显示了具有有限容量的服务(通过百分比显示)。该服务具有三个客户端。该服务已为每个客户端分配了其总容量的1/3.该图显示客户端Blue试图超过其预分配的吞吐量,但是未能成功,对其他客户端对服务的调用也未造成影响。
image.png

为了使可预测性更高,以及方便客户端更加了解调用,服务端可以为客户端提供可查看和使用的指标,以在其使用率接近最大配额时发出警报。例如,DynamoDB发布Amazon CloudWatch指标,该metric显示为表配置的吞吐量,以及该吞吐量和时间的关系。

某些API的服务成本远高于其他API。因此,服务可能会为每个客户端分配较少的昂贵API配额。同样,服务端并非总是预先知道操作成本。例如,返回单个1KB的行查询比返回1MB的行查询便宜。分页可以防止这种开销过于失控,但是最小页面大小和最大页面大小之间仍然可能存在成本差异,这使设置正确的阈值十分具有挑战性。为了解决此类问题,某些服务将较大的请求视为多个请求。此技术的实现方式是,将每个请求视为最便宜的请求,然后在API调用完成后,根据真实的请求成本返回,并记录为客户端的配额。

实施配额需要有一定的灵活性。例如:Client A的配额为每秒1000个事务(1000TPS),但是该服务已经扩展为可以处理10000TPS,并且该服务当前其所有客户端总共情况为5000TPS(并非总配额为5000TPS!)。如果Client A从500TPS飙升至3000TPS,则2000TPS将被拒绝,但是服务实际足够处理这些请求。这时我们可以让服务允许这些请求。如果之后其他client也同时使用更多配额,则该服务可以开始“删除”client A的超出配额的请求。对于这种“计划外配额”,应该及时向客户端发出信号,让client知道它已经超出配额,并且在不可预见的未来会有发生错误的风险。同时,该服务应该知道它可能需要扩展其fleet,并且可以一定程度上自动增加client A的配额。

下图演示了这种情况。图中创建了一个类似于上图用于显示向其client硬分配配额的服务的图表。但是,在下图中,服务为其client的配额增加了灵活性,而不是对其进行硬分配。stack允许客户端使用为利用的服务容量。由于橙色和灰色为使用满其配额,因此允许蓝色超出其预配置的阈值并利用未使用的容量。如果橙色或灰色决定使用其容量,则其流量必须优先于蓝色的突发流量。
image.png

在Amazon,有通过考虑客户的实际用例来研究灵活性和突发性。例如,EC2 instance(及其负载的EBS卷)在启动实例时通常比以后更忙。这是因为启动实例时,需要下载并启动其操作系统和应用程序的代码。当我们考虑到这种流量模式时,我们发现我们可以更慷慨的使用前期突发配额。这样可以减少启动时间,并且仍然提供了我们的长期容量规划工具,以确保工作负载之间的公平。

还可以考虑配额是否可以随时间变化。例如,某些服务会随着客户的增长自动增加其配额。但是,在某些情况下,客户需要并依赖固定配额,例如,用于控制成本的配额。

配额有时候并不一定是保护机制,而是服务的功能。

对准入控制系统的设计

决定流量大小,减少负载,实施基于速率的配额的系统称为 admission control systems。

亚马逊的服务采用多层准入控制设计,可以防止出现大量的需要拒绝的请求。我们经常在服务之前使用API Gateway,并让其处理配额和速率限制的某些方面。API Gateway可以处理庞大的fleet 流量。这意味着我们的fleet不用负担任何额外流量,可以可预测的服务于实际流量。我们还可以配置Application Load Balancer或CloudFront。以及使用Web应用程序防火墙服务AWS WAF进一步减轻admission control的负担。为了提供进一步的保护,AWS Shield提供了DDos保护服务。

在本小节中,将探讨一些技术,包括如何构建服务器端准入控制,如何根据其调用的服务的压力测试结果来做一个优雅响应的客户端,以及如何考虑这些系统的准确性。

Local admission control

一种实现准入控制的常用方法是使用令牌桶(token bucket)算法。令牌桶保存令牌,并且每当请求被接受时,都会从令牌桶中取出令牌。如果没有可用令牌,则请求被拒绝。令牌以配置的速率添加到令牌桶中,直到达到最大容量。该最大容量称为突发容量,因为这些令牌可以立即被消耗,从而支持流量突发。
令牌的这种瞬时突发消耗是一把双刃剑,它允许流量出现某些自然的不均匀性,但是如果突发容量太大,则会有大量请求被拒绝。
可以使用组合的令牌桶来防止无限制的突发流量。让一个令牌桶具有相对较低的速率和较高的突发容量,让另一个令牌桶具有较高的速率和较低的突发容量。通过检查第一个令牌桶,然后检查第二个令牌桶,可以实现高并发,但并发量有限。
对于传统服务(不具有无服务器架构的服务),还可以考虑针对给定客户的请求在服务器上的统一性或不统一性。如果请求不一致,可以使用更宽松的突发值或分布式准入控制技术。
Google Guava的 RateLimiter 就是一种现成的本地速率限制的实现。

Distributed admission control

Local admission control 对于保护本地资源很有用,但是配额的设置或者说公平通常需要在水平扩展的fleet中执行。amazon的团队采用了许多不同的方法来解决distributed admission control的问题,包括:
将配额除以服务器数量,分配到每个服务器上。使用这种方法,服务器根据它们在本地观察到的流量速率执行admission control。这种方法有一个假设:请求在服务器之间的分布相对均匀。当LB以round-robin(轮询)方式在服务器之间请求时,这种方法是可行的。如下图,假设流量在服务器之间相对均匀,且可以使用单个LB进行处理:
image.png
但是,在某些fleet的配置中,LB并不是以round-robin方式向服务器发送请求,而是向具有最少连接数的服务器发送请求。即LB并不是用于请求平衡模式,而是连接平衡模式。当每个服务器的配额足够高时,在实践中也许不会发生问题。然而当一个非常大的fleet具有多个LB时,关于请求在服务器之间的分布相对均匀的假设可能会失效。在这种情况下,client只会将请求发送给部分服务器。
下图说明了上述情况,其中虽然有多个LB,但是由于DNS的caching,导致client的流量并没有均匀的发送给多个LB。虽然当client常常打开和关闭connection时,基本不会出现问题。
image.png
通常我们可以使用一致性哈希来进行distributed admission control。某些服务的所有者运行单独的fleet,例如Amazon ElastiCache for Redis fleet。它们将throttle keys上的一致性哈希应用于特定的速率跟踪器服务器,然后让速率跟踪器服务器根据本地信息执行admission control。该解决方案甚至在服务器数量很多的情况下也可以很好的扩展,因为每个rate tracker server只需要知道fleet的一个子集。但是,当以足够高的速率请求特定的throttle key时,基本实现会在缓存队列中创建“hot spot”,因此需要向服务添加一些intelligence,以逐渐在特定key的throughput增加时,依赖本地准入控制。
下图说明了对数据存储使用一致性哈希的情况,即使在流量不均匀的情况下,使用一致性哈希来计算某种数据存储(例如缓存)中的流量也可以解决distributed admission control问题。然而这种架构引入了扩展挑战。
image.png
下图说明了一种新方法。使用服务器之间的异步信息共享来解决非均匀流量的问题。
image.png

Reactive admission control

配额对于处理常规的意外流量高峰十分重要,但服务应该准备好应对各种意外的工作负载。例如,有问题的客户端可能会发送格式错误的请求,或者客户端可能会发送比预期更昂贵的工作负载,又或者客户端可能有一些意料之外的应用程序逻辑,导致服务端流量暴增。处理这些问题的灵活性非常重要,因此我们可以建立一个准入控制系统,该系统可以对请求的各个方面作出反应,例如user-agent, URI, source IP address等。

Admission control of high cardinality dimensions

简单的准入控制系统只需要跟踪当前观察到的请求量和配额,例如,一个服务被十个不同的应用程序调用,则只需要跟踪十个不同的请求量和配额值即可。
但是,在处理更细粒度维度的准入控制时,系统将变得更加复杂。比如,服务可能会为世界上每个IPv6地址、DynamoDB表中的每一行或是S3存储桶中的每个对象设置基于速率的配额。
当运行这样一个具有高基数为度的系统时,我们需要对流量随时间的变化进行操作可视化。

Reacting to rate-exceeded responses

当服务的客户端收到速率超过配额的错误时,它可以重试或返回错误。Amazon的系统可以采用两种方式之一来响应速率超出错误,取决于系统是同步系统还是异步系统。

同步系统对实时性要求非常高,重试请求会有一定机会在下一次尝试中成功。但是,如果客户端依赖的服务频繁返回速率超过限制的响应,重试只会减慢每个响应的速度,并且会在已经负载过重的系统上占用更多资源。因此当服务频繁返回错误时,我们需要有工具自动停止重试。
对异步系统处理更加容易一些。在client收到速率超过配额的响应时,client可以放慢处理速度,直到请求成功。例如,一些异步系统定期会定期运行,而且对他们的期望便是工作需要很长时间才能完成。对于这些系统,它们可以尝试尽可能快的执行,并在某些依赖项称为瓶颈时放慢处理速度。

Evaluating admission control accuracy

无论我们使用哪种准入控制算法来保护服务,我们都需要评估该算法的准确性。
可以采用的一种方法是在每个请求的内容中包含节流键和速率限制。并执行日志分析以测量每个client每秒的实际队列请求。然后我们将其与配置的配额进行比较。由此,对于每个client,我们分析了“true positive rate”(被正确拒绝的请求率),“true negative rate”(被正确允许的请求率),“false positive rate”(被错误决绝的请求率),“false negative rate”(被错误接受的请求率)。
在amazon 我们可以使用cloudwatch或Athena来进行分析。

配额之外

向服务添加准入控制以提高其服务器端可用性,保护client端免受彼此侵害,似乎已经非常完美。但是,配额的使用也会对client带来不便。当client试图完成某件事时,配额会减慢它们的速度。当我们在服务中建立公平机制的同时,我们也需要寻找帮助client快读完成工作的方法,而不是让他们的吞吐量收到配额的限制。
我们帮助client避免超出配额的方法可以因API是控制平面API还是数据平面API分为两类。前者的代表性例子有S3 CreateBucket,DynamoDB DescribeTable和EC2 DescribeInstances等,后者的代表性例子有S3 GetObject,DynamoDB GetItem和SQS ReceiveMessage等。

避免超出配额的容量管理方法

数据平面工作负载具有弹性,因此我们可以将数据平面的服务设计为具有弹性的服务。为了使服务具有弹性,我们可以设计底层基础架构来自动扩展以适应客户工作负载的变化。我们需要帮助客户在管理配额时保持这种弹性。Amazon的service team使用各种技术来帮助其客户管理配额并满足客户对于弹性的需求:

  1. 如果fleet配备了一些未充分利用的‘slack’ capacity,我们会提醒我们的client。
  2. Amazon实施了 Auto Scaling 并随着每个client在正常业务过程中的增长而增加其配额。
  3. 我们让client很容易看到它们距离配额的距离,并让它们配置alarm,以在到达极限的时候立即提醒
  4. 我们会注意client何时接近并达到配额的限制。即,当服务以较高的整体速率流量或同时有太多client达到配额限制时,我们会发出警报。

避免超出配额的API设计方法

对于控制平面,上述讨论的一些技术可能并不适用。因为控制平面被设计为相对不频繁的被调用,而数据平面被设计为会被大量调用。但是,当控制平面的client最终创建了许多资源时,他们仍然需要能够对这些资源进行管理、审计和执行其他操作。客户在大规模管理许多资源时可能会用完他们的配额并遇到API速率限制,因此我们需要寻找替代方法来通过不同类型的API操作满足他们的需求。以下是 AWS 在设计API时采用的一些方法,可以帮助客户避免可能导致用完基于rate的配额的调用模式:

  1. Supporting a change stream. 例如,我们发现一些客户订起轮询EC2 DescribeInstances API操作,以列出他们的所有EC2实例。通过这种方式,他们可以找到最近启动或终止的实例。然而随着客户的EC2 instances的增长,这种API调用会变得越来越昂贵,导致超出基于rate的配额的可能性增加。对于某些user cases,我们能够通过AWS CloudTrail提供相同的信息,来避免API被真正调用。CloudTrail公开操作的更改日志,因此客户可以对流中的更改做出反应,而不是定期轮询EC2 API。
  2. Exporting data to another place that supports higher call volumes. S3 Inventory API就是一个这样的例子。客户如果在S3中有大量对象,而其需要从中筛选以找到特定对象时,他们会使用ListObjects API。为了帮助客户实现高吞吐量,Amazon S3 team根据这种情况,提供了一个Inventory API操作,该操作将存储桶中的对象列表异步导出到一个称为Inventory Manifest file的S3对象中,该文件包含存储桶中所有对象的JSON序列化列表。这样客户就可以以数据平面的吞吐量访问其存储桶的Manifest了。
  3. Adding a bulk API to support high volumes of writes. AWS的客户会有调用一些API写入操作来创建或更新控制平面中的大量内容。一些客户愿意容忍API施加的速率限制。但是,他们不想编写程序,也不想处理部分失败和速率限制产生的异常,以避免其他写入用例也失败。AWS IoT team通过API设计解决了这个问题。它们添加了asynchronous Bulk Provisioning APIs。使用这些API操作,客户上传一个包含他们想要进行更改的所有文件,当服务完成这些更改时,它们会向调用者提供一个包含结果的文件。这些结果将会显示哪些操作成功了,哪些操作失败了。这使得客户可以方便的处理大批量操作,但他们不需要处理重试、部分失败等这样的细节。
  4. Projecting control plane data into places where it needs to be commonly referenced. EC2 DescribeInstances 控制平面API操作从每个实例的网络接口返回有关实例的所有所有元数据到块设备映射。但是,其中一些元数据与在实例本身上运行的代码非常相关。当有很多实例时调用该方法时,每个实例调用DescribeInstances的流量都会很大。如果调用失败,这对于在实例上运行的客户应用程序来说将是一个很大的问题。为了完全避免这些调用,Amazon EC2在每个实例上公开一个本地服务,提供了实例元数据。通过将控制平面数据投影到实例本身,客户的应用程序将会通过避免同时远程调用,从而不会有API调用超出配额的情况。

准入控制是一个feature

在很多情况下,客户会发现准入控制比无约束的弹性更可取,因为它有助于他们控制成本。通常,服务不会对被拒绝的请求想客户收费,因为他们往往很少发生并且处理起来相对便宜。例如, AWS Lambda的客户要求能够通过限制潜在的并发调用次数来控制成本。当客户想要这种控制时,重要的我们需要提供他们这种可以通过API调用轻松自行调节的能力。它们还需要足够的visibility和alarming capabilities。通过这种方式,他们可以看到系统中的问题,并在他们认为有必要时提高配额。

结语

被多个客户端调用的服务具有资源共享的属性,使他们能够以更低的基础架构成本和更高的运行效率运行。因此我们在多客户端的服务中建立公平性,为我们客户提供可预测的性能和可用性十分重要。
服务配额时实现公平性的重要工具。基于速率的配额通过防止一种工作负载的不可预测的增加影响其他工作负载,使多客户端的服务更加可靠。但是,实施基于速率的配额并不总是足以给客户提供优质的体验。Customer visibility, controls, burst sharing, and different flavors of APIs 都可以帮助客户避免超出配额。

分布式系统中的admission control的实现时复杂的。在AWS中,API Gateway提供了多种节流功能。AWS WAF 提供了另一层服务保护,它可以集成到应用程序负载均衡器和API Gateway中。DynamoDB在单个索引级别提供与配置的吞吐量控制,让客户能够隔离不同工作负载的吞吐量需求。同样,AWS Lambda公开了每个函数的并发隔离以将工作负载彼此隔离。

使用配额的准入控制是构建具有可预测性能的高弹性服务的重要方法。然而只有准入控制是不够的。我们也要确保在准入控制之外解决问题,例如使用Auto Scaling,这样如果出现意外的load shedding,我们的系统就会通过Auto Scaling 自动响应增加机器的需求,来处理load shedding的问题。

从表面上看,在将服务作为单client服务与公开作为多client服务之间的成本和工作负载隔离之间似乎存在固有的权衡。但是,我们发现通过在多clients系统中实现公平性,从而使customer鱼和熊掌兼得。


Xinli
1 声望0 粉丝

« 上一篇
拒绝服务过载
下一篇 »
Debug