简介

对于“服务框架”,最难的一点是,如何给各个参数提供适当的默认值。特别是如果该参数会影响到性能或者服务的可用性。比如:

  • Client-side Timeout
  • 服务端允许的最大连接数

过载分析(The anatomy of overload)

在设计系统时,我们要将其设计为在遭遇过载情况之前主动扩展,来避免过载的发生。为了达到这一目的,我们需要采取很多措施,包括:

  • 自动扩展
  • 适当的减轻负载
  • 监控机制
  • 持续测试

通过对服务进行负载测试不难发现,低负载的服务器延迟低于高负载的服务器。这是因为在该负载下,线程相互之间争用资源,导致需要进行频繁的上下文切换,垃圾回收和I/O争用也会变得更加明显。最终服务会达到一个拐点,在负载超过拐点后,服务的性能会开始快速的下降。
该观察背后的理论称为“Universal Scalability Law”,它衍生自Amdahl’s law,该理论指出,尽管可以通过使用并行化来提高系统的吞吐量,但系统吞吐量最终会受制于串行化的吞吐量。(即受制于无法并行化的任务)

While a system’s throughput can be increased by using parallelization, it is ultimately limited by the throughput of the points of serialization (that is, by the tasks that can’t be parallelized)

其次,吞吐量不仅受制于系统资源的限制,因为当系统过载时,吞吐量通常会下降。虽然超载时计算机依然可以正常工作,但是其会花费大量的时间用于上下文切换,导致系统速度太慢从而不可用。
在分布式系统中,客户端经常会设置有超时时间。当服务器过载时,latency会超过超时时间,请求会开始失败。下图显示了服务器的响应时间,如何随着吞吐量的变大而变大。
image.png
在上图中,当latency超过timeout时,请求将会大量失败。对与客户端来说,通过下图更能清楚的反应出服务的可用性,在这里使用median response time,其含义为50%的latency低于中值。如果将客户端的超时时间设置为median response time,则一半请求将超时,因此可用性为50%。通过下图,可以将服务端的延迟问题转换为客户端的可用性问题。
image.png
描述可用性的一种更易懂的方式是区分有效吞吐量吞吐量

  • 吞吐量(goodput):是每秒发送到服务器的请求总数。
  • 有效吞吐量(throughput):是吞吐量的子集,它在接受处理的过程中没有产生错误,而且具有足够低的延迟(低于超时时间),可以发送正确的相应给客户端。
    image.png

正反馈循环

过载情况存在的问题在于它在反馈环路中会自我放大。如果客户端发来的请求超时,服务器在该请求上做的大量工作都将是无效工作。而系统在过载情况下,最不应该做的就是无效工作。
更糟糕的是,客户端经常会对超时请求进行重试,这使得对系统的请求大量增加,使其远超过系统所能承受的负载。而且,如果在面向服务的体系结构(SOA)中有足够深的调用图(即,A -> B -> C ...),并且如果每个层都执行了多次重试(幸运的是,这一点在前面的文章中已经提出了一些解决方法:https://segmentfault.com/a/11... ),则底层的过载将导致级联重试,从而会以指数形式放大请求的数量。
当这些因素结合在一起时,过载会形成正反馈循环,从而导致系统不可恢复。

防止做无效工作

从表面上来看,load shedding很简单。只要当服务器接近过载时,让它拒绝多余的请求即可。load shedding的目的非常简单,防止客户端超时。通过这种方法,服务器可以为接受的请求保持高可用性,而只会影响多余流量的可用性。
image.png
通过减少多余负载来控制延迟,可以使系统可用性更高。上图中总体可用百分比是在下降,这看起来比较糟糕,但关键是服务对决定接受的请求保持了高可用。从而避免了服务器做无效工作。
通过load shedding,即使总的吞吐量增加,服务器也可以维持一定数量的有效吞吐量的处理。但是,load shedding并不是没有任何代价的,服务器最终还是会受限于Amdahl’s law,使有效吞吐量出现下降。
image.png

测试

一定要对服务进行负载测试,让我们得到类似上面的可视化结果。这可以帮助我们确定性能过载的基线。
负载测试有多种类型。一些负载测试可以确保集群随着负载的增加而自动扩展,而另一些负载测试则是你用固定大小的集群。如果在负载测试中,服务的可用性随着吞吐量的增加而快速降低为0,则表明该服务需要额外的load shedding mechanisms。理想的负载测试结果是有效吞吐量在服务接近充分使用状态时趋于稳定,即,即使吞吐量更大,有效吞吐量也保持不变。
像Chao Monkey这样的测试工具能帮助对服务进行chaos engineering tests。例如,它们可以使CPU过载,或者主动引发数据包丢失的情况,以模拟在服务过载期间可能发生的状况。
还有一种测试技术是选择现有的负载产生测试,也称为canary。将这样的负载量级持续的施加在测试环境上,然后逐渐从测试环境中移除服务器。这样会导致每个instance上的吞吐量增加,因此可以用来测试instance的吞吐量。这种通过人为的减小集群大小来增加负载的技术对于需要隔离测试的服务很有效。但它并不能完全替代负载测试。因为全面的end-to-end负载测试,还会增加该服务依赖项的吞吐量,这些服务依赖项可能会导致实际吞吐量不达预期。
在测试期间,除了要确保服务端的availability和latency之外,我们更需要关注的是客户端感知的可availability和latency。当客户端的可用性开始下降时,我们会将继续增加负载。如果load shedding工作正常,即使吞吐量增加幅度远远超过了服务的负载,有效吞吐量也会保持稳定。
在下文继续探索load shedding mechanism之前,我们需要了解到过载测试非常重要。因为只有通过测试,我们才可以发现我们所设计的系统的瓶颈,并确定处理过载所需要的保护机制组合。

可见性

无论我们使用哪种技术来防止服务出现过载,我们都要仔细思考需要哪些指标来增加我们对过载的感知。即,提高过载的可见性。
在断流保护拒绝新的请求时,这种拒绝会降低服务的可用性。而如果在没有达到负载之前就拒绝请求(比如最大连接数设置过低),就会产生误报。我们需要努力将误报率保持为0。

如果某项服务误报率定期非零,则说明该服务相关参数调整的过于敏感,或者个别主机不断出现过载,并且可能存在扩展或负载均衡相关问题。在这种情况下,我们可能需要对应用程序的性能进行一些调整,或者我们需要使用性能更好的instance,以便从容的处理负载不均衡的问题。

从可见性的角度看,在load shedding拒绝请求时,我们需要确保有合适的工具来了解客户端是谁,他们正在调用哪个API,以及任何其他可以帮助我们调整保护措施的信息。还需要增加警报来检测是否正在拒绝很大的流量。当出现断流时,我们的首要任务是增加服务的capacity,并解决当前的瓶颈。
关于load shedding,还有一个微妙但重要的因素需要考虑。即,尽量不要让失败的请求延迟对服务的延迟指标造成影响。因为与其他请求相比,被拒绝的请求的延迟会非常低。例如,一项服务的过载保护降低了其60%的流量,那么即使有效吞吐量的latency非常大,该服务延迟的中位数看起来也会比较低,这是因为大量请求快速失败,导致中值延迟非常低。

load shedding对自动扩展和可用区故障的影响

如果配置不正确,load shedding有可能会隐式的禁用掉自动扩展。例如,服务配置了基于CPU的响应式扩展,并且还配置了load shedding,在达到自动需要的CPU使用率之前拒绝新的请求。在这种情况下,load shedding将减少请求数,以使CPU保持低负载,因此将永远不会达到自动扩展所需要的CPU使用率。
在设置用于处理可用区故障的自动扩展参数时,我们需要更加详细的考虑load shedding的逻辑。服务可能会扩展到可用区不可用的情况下,latency依然符合我们的预期。我们经常需要查看诸如CPU使用率之类的系统指标,来估算服务达到其容量限度的程度。然而,通过load shedding,发送到集群上的请求数量可能会比系统指标指示的更大,并且可能会导致我们没有了冗余,从而使可用区发生故障时,服务的可用性降低。
通过load shedding,我们可以使用减少非关键流量,或是非高峰期必须流量来节约成本。例如一个集群处理某网站的所有流量,则可以让该集群断定不为爬虫程序的流量扩展可用区冗余。但是使用这种方法时我们需要对系统仔细设计,要证明可以为人类流量提供可用区冗余的同时对爬虫流量进行减载。而且如果服务器的客户端(该爬虫程序)不知道该服务是通过这种方式配置的,则在“可用区故障”期间,服务端的行为看起来就会像是严重的可用性下降问题,而不是非关键的负载减除。因此在SOA中,我们需要尝试尽早考虑到这一点,在设计该服务时就来优化服务,而不是尝试在整个大的系统中来做全局优先级决策。

Load shedding mechanisms

在亚马逊,服务可以保持足够的剩余容量来处理可用区故障,而不必添加更多容量。它们使用限流来确保客户端之间的“公平”。
但是尽管有这些保护措施和操作惯例,服务在任何时间点都具有一定的capacity,所以由于各种原因,还是有可能发生过载。这些原因包括流量意外激增,集群容量突然消失(由于部署不当),客户端发出需要更多时间的请求(由缓存命中的读请求变为了未命中的读请求或写请求)。当服务过载时,我们必须让它完成它已经接受的请求。也就是说,服务必须保护自己免受过载的影响。下面将讨论一些管理过载的注意事项和技术。

了解丢弃请求的成本

我们在进行负载测试时,需要确保测试远远超过有效吞吐量稳定期的范围。采用此方法的关键原因在于,需要确保当我们load shedding丢弃请求的时候,丢弃请求的代价尽可能的小。因为丢弃请求时很容易错过accidental log statement或者socket setting。这可能会使丢弃请求的代价远远高于必要的代价。
在极少数情况下,迅速丢弃请求的代价可能高于继续处理请求。在这些情况下,我们会在保证延迟未超时的情况下减慢拒绝请求的速度。重要的是,要在继续处理请求的代价尽可能低的时候再这样做。例如,当请求未占用应用程序线程时。

确认请求的优先级

比如,从LB发送过来的ping请求优先级一定是最高的。
如上文所述,爬虫请求优先级一定低于人类请求。所以可以将爬虫请求放在非高峰时段处理。但是对于需要大量服务相互合作的复杂场景,如果错误的使用了优先级算法,可能会影响整个系统的可用性。
可以同时使用优先级和节流来避免收到严格的节流上限,同时仍然可以防止服务出现过载。具体来说,如果我们允许客户端发送超过为其配置的请求数量,则这些超限请求在优先级上可能会低于其他客户端发出的非超限请求。通过这种方式我们可以最大程度上降低突发请求使得服务器可用性降低。但是在折中的情况下,我们更希望可预测的预配置负载,而不是处理突发不可预测的负载。

时刻关注clock

如果服务在处理请求的中途发现对于客户端来说已经超时,则它可以跳过其余工作,并使请求直接失败。否则如果服务器继续处理该请求,并在较晚时回复响应,那么从服务器的角度看,它已经返回了成功的响应,但是从客户端超时的角度来看,这是一个错误。
因此可以在每个客户端请求中包含超时时间,以告诉服务器客户端愿意等待多长时间,服务器可以对其评估,并以很少的成本丢弃请求。
此超时时间可以表示为绝对时间或请求持续时间,但是,在分布式系统中,服务器很难约定当前的确切时间。在Amazon,使用了Amazon Time Sync Service通过将EC2 instance的时钟与每个AWS region中的一系列由卫星控制的冗余时钟和原子钟同步。正确的同步时钟对于日志记录也很重要,比较时钟不同步的服务器上的两个日志文件会使故障排除甚至比从新开始更难。
观察时钟的另一种方法是测量一台计算机的持续时间。服务器擅长在本地测量时间,因为它们无需与其他服务器达成共识。但是用本地服务器上的持续时间表达超时也有问题。因为服务器需要知道何时开始计算时间,然而在过载严重的情况下,大量请求会在TCP缓冲区中排队,因此有可能服务器从缓冲区中读取这些请求时,客户端已经超时。
对于客户端的超时请求,需要在每个服务之间进行传播剩余时间。详情见:https://segmentfault.com/a/11... (结合DDL)

完成已经开始的工作

我们不希望浪费任何已经有效的工作,尤其是在过载的情况下,丢弃该请求会创造一个增大负载的正反馈循环,原因是客户端常常会在超时后重试请求。在客户端超时时,它们通常会在第一个连接上停止等待响应,同时在一个单独的连接上发起新的请求。如果服务器完成第一个请求并发送响应,客户端可能不会收到此响应,原因是它此时正在等待重试的请求发回响应。
为了解决这种浪费有效工作的问题,我们尝试将服务设计为执行有界限的工作。在我们会返回大型数据集的API的地方,我们可以将其设计为支持分页的API。这些API返回部分结果,以及客户端可以用来请求更多数据的token。当服务器处理对内存,CPU和网络带宽有上限的请求时,更容易估计服务承受的额外负载。如果服务器不知道需要如何多少资源与时间来处理额外请求,则对服务的控制程度就会降低。
围绕着客户端如何使用服务的API,可以找到与确定请求优先级有关的其他优化策略。例如,假设某个服务有两个API:start(), end()。为了完成工作,客户端需要能够调用这两个API。在这种情况下,应该使该服务的end()请求优先级高于start()请求。因为如果让start()请求优先,客户端将可能无法完成已经开始的工作,从而导致有效工作浪费。分页策略也是如此。如果客户端必须发送多个连续的请求来获取多页,而且在第n-1页看到请求失败并丢弃所有结果,则它浪费了n-2次的服务调用和其在整个过程中执行的任何重试。因此和end()一样,前一页的优先级应该低于后一页的优先级。

监控队列

在管理内部队列时,查看请求的持续时间也很有帮助。除了我们可以限定队列的大小意外,当我们从队列中取出工作时,我们可以查看该工作在队列中停留了多长时间,对于太旧的请求我们可以直接丢弃。这可以降低服务器的压力。使其可以处理成功几率更大的较新的请求。比如我们可以对后进先出队列进行适当的改进。
在收到服务器过载时,我们可以将新传入的请求加入到负载均衡器上的surge queues中。对于队列中的请求的处理,我们可以将其传到某个溢出配置中,使他们直接失败。但是通过对surge queues的监控,我们可以知道负载相关指标。当然,负载测试提供了更加可靠的信息,前提是我们能够想出符合实际的测试用例。


Xinli
1 声望0 粉丝