编程

负载均衡工作原理

421 2023-05-20 18:21:00

超过某一点后,web应用程序的规模将超过单个服务器的部署。公司要么希望提高可用性、可扩展性,要么两者兼而有之!为了做到这一点,他们在多个服务器上部署应用,并在前面部署一个负载均衡器来分发传入的请求。大公司可能需要数千台运行其web应用的服务器来处理负载。

在这篇文章中,我们将重点讨论单个负载均衡器如何将HTTP请求分发到一组服务器的。我们将从底层开始,逐步发展到现代负载均衡平衡算法。

问题可视化

让我们从头开始:单个负载均衡器向单个服务器发送请求。请求以每秒1个请求(RPS)的速率发送,并且随着服务器的处理,每个请求的大小都会减小。

对于许多网站来说,这种设置效果很好。现代服务器功能强大,可以处理大量请求。但是当他们跟不上的时候会发生什么呢?

在这里我们可以看到,3RPS的速率会导致一些请求被丢弃。如果一个请求在处理另一个请求时到达服务器,服务器会将其丢弃。这将导致向用户显示错误,这是我们希望避免的。我们可以在负载均衡器中添加另一个服务器来解决这个问题。

不再有丢弃的请求!此处负载均衡器依次向每个服务器发送请求,称为“轮询”负载均衡。这是最简单的负载均衡形式之一,当您的服务器都同样强大,请求也同样昂贵时,它可以很好地工作。

› 如果轮询(round robin)不能应付

在现实世界中,很少有服务器同样强大,请求同样昂贵。即使使用完全相同的服务器硬件,性能也可能有所不同。应用程序可能必须为许多不同类型的请求提供服务,并且这些请求可能具有不同的性能特征。

让我们看看当我们改变请求成本时会发生什么。在下面的模拟中,请求并不是同样昂贵。您可以通过某些请求看到这一点,这些请求需要比其他请求更长的时间才能收缩。

 

虽然大多数请求都能成功送达,但我们确实会丢弃一些请求。缓解这种情况的方法之一是建立一个“请求队列”

 

请求队列有助于我们处理不确定性,但这是一种权衡。我们将丢弃更少的请求,但代价是一些请求具有更高的延迟。如果你看上面的模拟足够长的时间,你可能会注意到请求会微妙地改变颜色。它们未被服务的时间越长,颜色就会改变得越多。您还会注意到,由于请求成本的差异,服务器开始表现出不平衡。队列将在那些运气不好的服务器上阻塞,并且必须连续提供多个昂贵的请求。如果队列已满,我们将丢弃该请求。

上述内容同样适用于功率不同的服务器。在接下来的模拟中,我们还改变了每个服务器的功率,这在视觉上用较深的灰色阴影表示。

 

服务器被赋予了一个随机的权重,但有可能有些服务器的性能不如其他服务器,并很快开始丢弃请求。与此同时,性能更强大的服务器大部分时间处于空闲状态。这种情况显示了轮询的主要缺点:方差。

然而,尽管存在缺陷,round-robin仍然是nginx默认的HTTP负载均衡方法。

› 轮询改进

可以对轮询进行调整,以便在有差异的情况下表现得更好。有一种称为“加权轮询”的算法,它包括让人类用一个权重来标记每个服务器,该权重用以标明多少请求发送到对应服务器。

在这个模拟中,我们使用每个服务器的已知权重值作为其权重,并且在轮询过程中向更强大的服务器时发出更多请求。

 

虽然这比普通的轮询更能处理服务器性能的差异,但我们仍然需要处理请求的变化。在实践中,让人类手动设定权重很快就会失效。将服务器性能转换成一个数字是很困难的,而且需要对实际工作负载进行仔细的负载测试。这种情况很少发生,因此加权轮询的另一种变体通过使用代理度量动态计算权重:延迟。

可以肯定的是,如果一台服务器提供请求的速度是另一台服务器的3倍,那么它可能会快3倍,并且接收的请求应该是其他服务器的3倍。

 

这次我向每台服务器添加了文本,显示了最后3个请求的平均延迟。然后,我们根据延迟的相对差异来决定是向每个服务器发送1个、2个还是3个请求。结果与初始的加权循环模拟非常相似,但不需要预先指定每个服务器的权重。该算法还能够适应服务器性能随时间的变化。这被称为“动态加权轮询”

让我们看看它在服务器性能和请求成本都有很大的差异情况下,是如何处理复杂的情况的。下面的模拟使用了随机值,所以可以随意刷新页面几次,以查看它是否适应新的变化。

 

› 离开轮询

动态加权循环似乎很好地解释了服务器性能和请求成本的差异。但如果我告诉你我们可以做得更好,用一个更简单的算法呢?

 

这被称为“最少连接”负载平衡。

因为负载均衡器位于服务器和用户之间,所以它可以准确地跟踪每台服务器有多少未处理的请求。然后,当一个新的请求到来时,此时决定将其发送到哪里了,它知道哪些服务器在处理的工作最少,并优先考虑这些服务器。

无论存在多大的差异,该算法都表现得非常好。它通过保持对每个服务器正在做什么的准确理解来消除不确定性。它还具有实现起来非常简单的优点。由于这些原因,您会发现该算法是AWS负载均衡器的默认HTTP负载均衡方法。它也是nginx中的一个选项,如果你从未更改过默认值,那么它非常值得尝试。

让我们在类似的复杂模拟中看看这一点,与我们在上面给出的动态加权循环算法中给出的参数相同。同样,这些参数在给定范围内是随机的,因此刷新页面以查看新的变体。

 

虽然这种算法在简单性和性能之间取得了很好的平衡,但它也不能避免丢弃请求。然而,您会注意到,该算法唯一一次丢弃请求是发生在实际上没有更多可用的队列空间时。它将确保所有可用资源都在使用中,这使它成为大多数工作负载的一个很好的默认选择。

› 延迟优化

到目前为止,我一直在回避讨论的一个关键部分:我们正在优化什么。言外之意,我一直认为丢弃的请求非常糟糕,并试图避免它们。这是一个不错的目标,但它不是我们最想在HTTP负载均衡器中优化的指标。

我们通常更关心的是延迟。这是以毫秒为单位测量的,从创建请求的那一刻起到请求得到服务的那一时刻。当我们在这种情况下讨论延迟时,通常会谈论不同的“百分位数”。例如,第50百分位数(也称为“中位数”)被定义为毫秒值,其中50%的请求低于该值,50%的请求高于该值。

我在60秒内用相同的参数进行了3次模拟,并每秒进行各种测量。每个模拟只是所使用的负载平衡均衡算法不同。让我们比较3种模拟中每种模拟的中值:

您可能没有预料到,但轮询具有最佳的中位延迟。如果我们不看任何其他数据点,我们就会错过完整的故事。让我们看一下第95个百分位数和第99个百分位位数

注意:每个负载均衡算法的不同百分位数之间没有颜色差异。较高的百分位数在图表上总是较高的。

我们看到轮询在较高的百分位数中表现不佳。轮询怎么可能有一个很好的中位数,但糟糕的第95和第99百分位数?

在轮询中,每个服务器的状态没有被考虑进去,因此会有相当多的请求流向空闲的服务器。这就是为什么会出现低50百分位。另一方面,我们也依然向过载的服务器发送请求,因此出现了糟糕的第95和第99百分位数。

我们可以以直方图形式查看完整数据:

我为这些模拟选择了参数,以避免丢弃任何请求。这保证了我们对所有3种算法进行相同数量的数据点比较。让我们再次运行模拟,但增加RPS值,这样做的目的在于让所有算法都超过它们所能处理的范围。以下是一段时间内丢弃的累积请求的图表

最少连接可以更好地处理过载,但这样做的成本略高于95%和99%的延迟。取决于您的实际情况,这可能是一个有价值的权衡。

最后一个算法

如果我们真的想优化延迟,我们需要一种将延迟考虑在内的算法。如果我们能将动态加权轮询算法与最小连接算法相结合,那不是很好吗?加权轮询的延迟和最小连接的弹性。

事实证明,我们不是第一个有这种想法的人。以下是使用一种名为“峰值指数加权移动平均”(或PEWMA)的算法进行的模拟。这是一个冗长而复杂的名字,但请别着急,我稍后会详细介绍它的工作原理

 

我已经为这个模拟设置了特定的参数,这些参数可以保证表现出预期的行为。如果你仔细观察,你会注意到算法会在一段时间后停止向最左边的服务器发送请求。之所以会这样是因为它发现所有其他服务器都更快,而且不需要向最慢的服务器发送请求。这只会导致请求具有更高的延迟。

那么它是如何做到这一点的呢?它将动态加权轮询的技术与最少连接的技术相结合,并在其上面加入了一点自己的魔法。

对于每台服务器,算法都会跟踪最后N个请求的延迟。它不是用它来计算平均值,而是用指数递减的比例因子对值进行求和。这会产生一个值,其中延迟越老,它对总和的贡献就越小。最近的请求比以前的请求对计算的影响更大。

然后取该值,乘以与服务器打开的连接数,结果就是我们用来选择向哪个服务器发送下一个请求的值。越低越好。

那么,两者相比如何呢?首先,让我们来看看第50个、第95个和第99个百分位数与之前的最少连接数据进行比较。

I

我们看到了全面的显著进步!它在较高的百分位数上更为明显,但在中位数上也一直存在。在这里,我们可以在直方图中看到相同数据。

那么丢弃的请求呢?

它一开始性能更好,但随着时间的推移,性能比最少连接差。这是有道理的。PEWMA是机会主义的,因为它试图获得最佳的延迟,这意味着它有时可能会使服务器负载不足。

我想在这里补充一点,PEWMA有很多参数可以调整。我为这篇文章编写的实现使用的配置似乎适用于我测试过的情况,但进一步的调整可以获得相对于最少连接更好的结果。这是PEWMA相对于最少连接的缺点之一:额外的复杂性。

› 结论

我花了很长时间在这篇文章上。在现实主义和通俗易懂之间取得平衡并不容易,但我对自己的切入点感觉很好。我希望能够通过这些复杂系统在实践中,在理想和不太理想的场景中的表现,有助于您直观地了解它们何时最适合您的工作负载。

免责声明:你必须始终将实际情况为基准,而不是将互联网上的建议视为福音。我在这里的模拟忽略了一些实际限制(服务器启动缓慢、网络延迟),并设置为显示每个算法的特定属性。从表面上看,它们并不是现实的基准。