渡渡鸟停泊的那一天。 异步脚本

哈Ha! 我们团队中的每个SRE都曾梦想过在晚上安静地入睡。 梦想成真。 在本文中,我将讨论这一点以及我们如何实现Dodo IS系统的性能和稳定性。


有关Dodo IS *系统崩溃的一系列文章

1. 渡渡鸟停泊的一天。 同步脚本。
2. Dodo IS停止的日期。 异步脚本。

* 资料是根据我在莫斯科DotNext 2018上的表现编写的
在上一篇文章中,我们研究了抢先式多任务范例中的代码阻塞问题。 假定有必要在异步/等待时重写阻塞代码。 我们做到了。 现在让我们讨论一下这样做时出现了什么问题。

我们引入术语并发


在开始异步之前,必须输入术语并发。
在排队论中, 并发是当前系统内部的客户机数。 并发有时会与并行性相混淆,但实际上这是两个不同的事物。
对于那些初次接触并发的人, 我推荐Rob Pike的视频 。 并发是指我们同时处理许多事情,而并行是当我们同时处理许多事情。

在计算机中,并行发生的事情很少。 这样的事情就是在多个处理器上进行计算。 并行度受CPU线程数的限制。

实际上,线程是先占式多任务处理概念的一部分,当我们在并发问题中依赖操作系统时,线程是在程序中为并发建模的一种方法。 只要我们了解我们正在专门处理并发模型而不是并发,该模型就仍然有用。

异步/等待是状态机的语法糖,状态机是另一个可以在单线程环境中运行的有用的并发模型。 本质上,这就是协作多任务处理-模型本身根本不考虑并行性。 与多线程相结合,我们可以使一个模型在另一个模型之上,并且生活非常复杂。

两种型号的比较


它在抢先式多任务处理模型中的工作方式


假设每秒有20个线程和20个请求正在处理。 图片显示了一个高峰-系统中同时有200个请求。 这怎么可能发生:

  • 如果200个客户同时单击一个按钮,则可以对请求进行分组;
  • 垃圾收集器可以将请求停止数十毫秒;
  • 如果代理支持队列,则请求可以在任何队列中延迟。

短时间内的请求累积并成束的原因有很多。 无论如何,没有什么可怕的事情发生,他们站在线程池队列中并缓慢完成。 不再有山峰,一切继续进行,好像什么也没发生。

假设智能线程池算法(那里有机器学习的元素)决定了到目前为止,没有理由增加线程数量。 由于线程数= 20,因此MySql中的连接池也为20。 因此,我们仅需要20个SQL连接。



在这种情况下,从外部系统的角度来看,服务器的并发级别为200。服务器已经收到了这些请求,但尚未完成。 但是,对于在多线程范例中运行的应用程序,并发请求的数量受线程池当前大小= 20的限制。因此,我们处理的并发度= 20。

现在一切如何在异步模型中工作




让我们看看在运行异步/等待且负载和请求分配相同的应用程序中会发生什么。 创建任务之前没有队列,并且请求将立即处理。 当然,来自ThreadPool的Thread的使用时间很短,并且在联系数据库之前,请求的第一部分会立即执行。 由于线程迅速返回线程池,因此我们不需要处理太多线程。 在此图中,我们根本不显示线程池,它是透明的。



这对我们的应用意味着什么? 外部情况是相同的-并发级别=200。同时,内部情况已更改。 以前,请求“拥挤”在ThreadPool队列中,现在应用程序的并发程度也为200,因为TaskScheduler对此没有任何限制。 万岁! 我们已经实现了异步的目标-应用程序“应付”几乎任何程度的并发!

后果:系统的非线性退化


从并发的角度来看,该应用程序已变得透明,因此现在将并发投影到数据库上。 现在,我们需要一个大小等于200的连接池。数据库是CPU,内存,网络,存储。 与其他服务一样,这是具有问题的同一服务。 我们尝试同时执行的请求越多,它们运行的​​速度就越慢。

在数据库上满载的情况下,最好的情况下,响应时间呈线性下降:您发出的查询数量是原来的两倍,它的运行速度却是原来的两倍。 实际上,由于查询竞争,必然会发生开销,并且可能证明系统将非线性降级。

为什么会这样呢?


第二个订单的原因:

  • 现在,数据库需要同时保存在数据结构的内存中,以处理更多请求。
  • 现在,数据库需要服务于更大的集合(这在算法上是不利的)。

一阶原因:


最后,异步与有限的资源作斗争,并...获胜! 数据库失败并开始变慢。 由此,服务器将进一步提高并发性,并且系统将无法继续摆脱这种局面。

服务器突然死亡综合症


有时会发生一个有趣的情况。 我们有一台服务器。 他那样为自己工作,一切都井井有条。 有足够的资源,即使有余地。 然后,我们突然从客户端收到一条消息,表明服务器正在减慢速度。 我们查看图表,发现客户活动有所增加,但是现在一切正常。 考虑DOS攻击或巧合。 现在一切似乎都很好。 直到现在,服务器仍然处于愚蠢状态,直到超时开始大量涌入之前,一切都会变得更加艰难。 一段时间后,使用相同数据库的另一台服务器也开始弯曲。 熟悉的情况?

为什么系统死了?


您可以尝试通过以下事实来解释这一点:服务器在某个时候收到了高峰数量的请求和“中断”。 但是我们确实知道负载已减少,并且此后的服务器在很长一段时间内一直没有变好,直到负载完全消失。

口头上的问题:服务器是否应该由于过多的负载而崩溃? 他们这样做吗?

我们模拟服务器崩溃的情况


在这里,我们将不分析来自实际生产系统的图形。 在服务器崩溃时,我们通常无法获得这样的时间表。 服务器用尽了CPU资源,因此,它无法写入日志,无法提供指标。 在灾难发生时的图表上,经常观察到所有图表都出现中断。

SRE应该能够产生不太容易受到这种影响的监视系统。 在任何情况下至少提供某些信息的系统,同时能够使用碎片信息分析事后分析系统。 出于教育目的,我们在本文中使用略有不同的方法。

让我们尝试创建一个数学上就像在负载下的服务器一样工作的模型。 接下来,我们将研究服务器的特性。 我们丢弃了真实服务器的非线性,并模拟了当负载超过额定值时发生线性减速的情况。 根据需要增加两倍的请求-我们的服务速度是原来的两倍。

这种方法将允许:

  • 考虑充其量会发生什么;
  • 采取准确的指标。

预定的导航:

  • 蓝色-发送给服务器的请求数;
  • 绿色-服务器响应;
  • 黄色-超时;
  • 深灰色-由于客户端未等待超时响应而浪费在服务器资源上的请求。 有时,客户端可以通过断开连接将其报告给服务器,但是通常,这种奢侈在技术上可能不可行,例如,如果服务器在没有与客户端合作的情况下进行CPU限制的工作。




为什么客户的请求图(图中的蓝色)是如此? 通常,我们的比萨店的订单安排在早上会平稳增长,而在晚上会减少。 但是我们在通常的均匀曲线背景下观察到三个峰。 图的这种形式不是偶然为模型选择的,而是偶然的。 该模型是在世界杯期间与俄罗斯比萨店联络中心的服务器一起进行的真实事件调查中诞生的。

案例“世界杯”


我们坐着等待更多的订单。 为锦标赛做准备,现在服务器将能够通过强度测试。

第一个高峰-足球迷们去看冠军,他们饿了,买了披萨。 在上半年,他们很忙,无法订购。 但是对足球漠不关心的人可以,所以在图表上,一切照常进行。

然后上半场结束,第二个高峰到来。 粉丝们变得紧张,饥饿,并下了比第一个高峰期多三倍的订单。 披萨的价格太差了。 然后下半场开始,再次不要披萨。

同时,联络中心服务器开始缓慢弯曲并越来越慢地处理请求。 系统组件(在这种情况下为Call Center Web服务器)不稳定。

比赛结束时,第三个高峰将来到。 风扇和系统正在等待处罚。

我们分析服务器崩溃的原因


发生什么事了 服务器可以容纳100个条件请求。 我们了解它是为此功能而设计的,将不再受其影响。 高峰到达了,它本身并没有那么大。 但是并发的灰色区域要高得多。

设计该模型的目的是使并发在数值上等于每秒的订单数,因此从视觉上看,它应该具有相同的比例。 但是,它会更高,因为它会累积。

我们在这里从图中看到一个阴影-这些请求已开始返回到客户端,并已执行(如第一个红色箭头所示)。 时间刻度是看时间偏移的条件。 第二高峰已经淘汰了我们的服务器。 他崩溃了,开始处理的请求比平时少四倍。



在该图的后半部分,很明显,一些请求最初仍在执行,但随后出现黄点-请求完全停止。



再次整个时间表。 可以看出,并发正在疯狂。 一座巨大的山出现。



通常,我们分析的指标完全不同:完成请求的速度有多慢,每秒有多少个请求。 我们甚至没有考虑并发性,甚至没有考虑这个指标。 但是徒劳无功,因为正是这个数量最能说明服务器故障的时刻。

但是,如此巨大的山脉从何而来呢? 最大的负载峰值已经过去了!

小法则


利特尔法则控制并发性。

L(系统内的客户数量)=λ(停留速度)* W(他们在系统内度过的时间)

这是一个平均值。 但是,我们的情况正在急剧发展,平均水平不适合我们。 我们将微分这个方程,然后积分。 为此,请查看发明了此公式的约翰·利特尔(John Little)的书,并在那里查看其组成部分。



我们有系统中的条目数量以及离开系统的人员数量。 一切完成后,请求到达并离开。 以下是与并发线性增长相对应的图形增长区域。



绿色要求很少。 这些是实际上正在实施的。 蓝色的是那些来的。 在不同时间之间,我们有通常的请求数量,情况稳定。 但是并发性仍在增长。 服务器本身将不再应付这种情况。 这意味着他将很快倒下。

但是为什么并发性增加了? 我们看一下常数的积分。 我们的系统没有任何变化,但是积分看起来就像是线性函数,只会逐渐增大。

我们会玩吗?


如果您不记得数学,则积分的解释会很复杂。 在这里,我建议热身并玩游戏。

游戏号码1


先决条件 :服务器接收请求,每个请求在CPU上需要三个处理周期。 CPU资源在所有任务之间平均分配。 这类似于抢先式多任务处理期间消耗CPU资源的方式。 单元格中的数字表示此度量之后剩余的工作量。 对于每个条件步骤,都会收到一个新请求。

想象一下,您收到了一个请求。 在第一个处理周期结束时,仅剩下3个工作单元,剩下2个单元。

在第二阶段,另一个请求被分层,现在两个CPU都处于繁忙状态。 他们为前两个查询做了一个工作单元。 它仍然需要分别完成第一个和第二个请求的1个和2个单元。

现在第三个请求已经到来,乐趣开始了。 似乎第一个请求应该已经完成​​,但是在此期间,三个请求已经共享了CPU资源,因此,在第三个处理周期结束时,这三个请求的完成程度现在是很小的:



更有趣! 添加了第四个请求,现在并发度已经是4,因为所有四个请求在此期间都需要资源。 同时,第四个周期末的第一个请求已经完成,不会转到下一个周期,并且有0个作业要留给CPU。

由于第一个请求已经完成,所以让他来总结一下:它的运行时间比我们预期的长三分之一。 根据工作量,假设每个任务的长度水平理想地为3。 我们用橙色标记它,表示我们对结果不完全满意。



第五个请求到达。 并发度仍然是4,但是我们看到在第五列中剩余的工作总计更多。 这是因为第四列比第三列还有更多的工作要做。

我们将继续三个时期。 等待答案。
-服务器,您好!
-...



“您的电话对我们非常重要...”



好吧,终于有了第二个请求的答案。 响应时间是预期时间的两倍。



并发程度已经增加了三倍,没有任何迹象表明情况会有所好转。 我没有进一步说明,因为对第三个请求的响应时间将不再适合图片。

我们的服务器已进入不良状态,永远不会自行退出。 游戏结束

服务器的GameOver状态的特征是什么?


请求会无限期地累积在内存中。 记忆迟早会结束。 另外,随着规模的增加,用于服务各种数据结构的CPU开销增加。 例如,连接池现在应该跟踪超时以获取更多连接,垃圾收集器现在应该再次检查堆上的更多对象,依此类推。

探索活动对象累积的所有可能后果不是本文的目的,但是即使是RAM中简单的数据累积也已经足以填满服务器。 此外,我们已经看到客户端服务器将其并发性问题投影到数据库服务器以及用作客户端的其他服务器上。

最有趣的是:现在,即使您向服务器提交了较低的负载,它仍将无法恢复。 所有请求都将以超时结束,并且服务器将消耗所有可用资源。

我们到底期望什么? 毕竟,我们有意识地为服务器提供了它无法处理的大量工作。

在处理分布式系统架构时,考虑一下普通人如何解决此类问题很有用。 以一家夜总会为例。 如果有太多人输入,它将停止运行。 保镖简单地解决了这个问题:它看起来里面有多少人。 一个向左-启动另一个。 一个新的客人将来欣赏队列的大小。 如果线路很长,他会回家。 如果将此算法应用于服务器怎么办?



让我们再玩一次。

游戏号码2


先决条件 :同样,我们有两个CPU,每个周期有3个单元的相同任务,但现在我们将设置弹跳器,这些任务将变得很聪明-如果他们看到队列长度为2,则立即返回。





第三个请求来了。 在此期间,他排队。 期末他的数字为3。 残差中没有小数,因为两个CPU执行两项任务,一项执行一段时间。

尽管我们分层了三个请求,但系统内部的并发度=2。第三个请求在队列中,不计算在内。



第四次出现-相同的画面,尽管已经积累了更多的工作。


...
...

在第六阶段中,第三次请求以第三次延迟完成,并且并发度已经= 4。



并发度提高了一倍。 她再也无法成长了,因为我们已明确禁止这样做。 以最快的速度,只有前两个请求得以完成-那些首先进入俱乐部的请求,而每个人都有足够的空间。

黄色请求在系统中的时间更长,但是它们排成一行,并且不会延迟CPU资源。 因此,那些在里面的人很开心。 这可能会一直持续下去,直到一个男人来,说他不会排队,而是他会回家。 这是一个失败的请求:



这种情况可以无休止地重复,而查询执行时间保持在同一水平上-恰好是我们希望的两倍。



我们看到对并发级别的简单限制消除了服务器的生存能力问题。

如何通过并发级别限制提高服务器的生存能力


您可以自己编写最简单的“弹跳器”。 下面是使用信号量的代码。 外部行的长度没有限制。 , .

const int MaxConcurrency = 100; SemaphoreSlim bulkhead = new SemaphoreSlim(MaxConcurrency, MaxConcurrency); public async Task ProcessRequest() { if (!await bulkhead.WaitAsync()) { throw new OperationCanceledException(); } try { await ProcessRequestInternal(); return; } finally { bulkhead.Release(); } } 

要创建受限队列,您需要两个信号灯。 为此,Microsoft建议使用Polly库 。 注意隔板模式。 直译为“ bulkhead”(布尔头)-一种允许船舶不沉没的结构性元素。 老实说,我认为保镖一词更适合。 重要的是,这种模式允许服务器在无望的情况下生存。

首先,我们从服务器上挤出负载台上所有可能的东西,直到确定它可以容纳多少个请求。 例如,我们确定为100。我们放入了舱壁。

此外,服务器将仅跳过所需数量的请求,其余的将排队。 明智地选择一个较小的数字,以便有库存。 对于这个问题,我没有现成的建议,因为这取决于上下文和具体情况。

  1. 如果服务器行为稳定地取决于资源方面的负载,则此数目可能接近限制。
  2. 如果介质易受负载波动的影响,则应考虑这些波动的大小,选择一个更保守的数字。 这种波动可能由于各种原因而发生,例如,GC的性能环境的特征是CPU上的负载峰值很小。
  3. 如果服务器按计划执行定期任务,则也应考虑到这一点。 您甚至可以开发一个自适应隔板,该隔板将计算可在不降低服务器性能的情况下同时发送多少个查询(但这已经超出了本研究的范围)。

查询实验


最后看看此验尸,我们将不再看到它。

所有这些灰色堆无疑与服务器崩溃相关。 灰色是服务器的死因。 让我们切断它,看看会发生什么。 似乎一定数量的请求将返回,根本无法满足。 但是多少钱?

100个内部,100个外部



事实证明,我们的服务器开始过得很有趣。 他不断地以最大的力量耕作。 当然,当出现高峰时,会将他赶出场,但不会持续很长时间。

受到成功的启发,我们将尝试确保他完全不被弹跳。 让我们尝试增加队列的长度。

内部100个,外部500个




情况有所好转,但尾巴增大了。 这些是很长时间以后执行的请求。

内100个,外1000个


由于事情已经变得更好,所以让我们尝试使其荒谬。 让我们解决队列长度比同时投放多十倍的问题:



如果我们谈论俱乐部和保镖的隐喻,这种情况几乎是不可能的-没有人比在俱乐部花费更多的时间在入口处等待更长的时间了。 我们也不会假装这是我们系统的正常情况。

最好不要完全为客户提供服务,而要在网站或移动应用程序中折磨客户,方法是将每个屏幕加载30秒并破坏公司的声誉。 最好立即诚实地告诉一小部分客户,我们现在无法为他们提供服务。 否则,我们将为所有客户提供慢几倍的服务,因为该图表明情况持续了相当长的一段时间。

还有另一种风险-可能没有为这种服务器行为设计其他系统组件,并且,正如我们已经知道的,并发被投影到客户端上。

因此,我们回到第一个选项“每100个100”,并考虑如何扩展我们的产能。

优胜者-100个内部,100个外部




¯\ _(ツ)_ /¯

使用这些参数,运行时间的最大降级恰好是“标称值”的2倍。 同时,查询执行时间降低了100%。

如果您的客户端对运行时很敏感(人工客户端和服务器客户端通常都是这样),那么您可以考虑进一步减少队列长度。 在这种情况下,我们可以使用一定比例的内部并发性,并且可以肯定地知道服务的响应时间降级不会平均超过此百分比。

实际上,我们并不是在创建队列,而是在保护自己免受负载波动的影响。 在此,就像确定舱壁的第一参数(内部数量)的情况一样,确定客户端可能造成的负载波动是很有用的。 因此,我们将大致了解在哪种情况下,我们会错过潜在服务带来的利润。

确定哪些延迟波动可以承受与服务器交互的其他系统组件甚至更为重要。 因此,我们将知道,我们确实在最大限度地利用现有系统,而没有完全失去服务的危险。

诊断与治疗


我们正在使用隔板隔离来处理不受控制的并发。
与本系列文章中讨论的其他方法一样,此方法也可以通过Polly库方便地实现。

该方法的优点在于,将很难破坏系统本身的稳定性。 系统会在成功请求的时间方面获得非常可预测的行为,而成功完成请求的机会则更高。

但是,我们并不能解决所有问题。 例如,服务器电源不足的问题。 在这种情况下,您显然必须在负载突然增加的情况下决定“放下镇流器”,我们认为这种情况过大。

我们的研究未解决的其他措施可能包括例如动态缩放。

Source: https://habr.com/ru/post/zh-CN461081/


All Articles