[译] 关于负载均衡

原文地址: https://samwho.dev/load-balancing/

在请求达到一定的量级时, WEB 应用程序会超出单台服务器的承载能力。此时的策略是要么提高可用性, 要么提高可扩展性, 或者两者兼具!

为此, 我们需要在多个服务器上部署应用程序, 并在请求处理之前加入负载均衡器来分发传入的请求。为此可能需要数千台运行web应用程序的服务器来处理负载。

在这篇文章中, 我们将重点关注单个负载均衡器可能的负载方式。我们将从底层开始, 一步步上升到现代负载均衡算法。

可视化问题

让我们从最基本的开始:
一个负载均衡器将请求发送到单个服务器。请求的速率为每秒1个请求(RPS), 服务器在处理每个请求时, 表示请求的球大小会减小.

对于许多网站来说,这个设置就可以很好地工作。现代服务器性能强大, 可以处理大量请求。但是当它们无法跟上请求的发送速度时会发生什么呢?

在这里我们看到, 3 RPS 的速率会导致一些请求被丢弃

如果服务器正在处理一个请求时另一个请求到达, 那么服务器将会丢弃这个请求。我们不想这样, 这将导致用户看到错误。我们可以添加另一台服务器进行负载解决这个问题

所有请求都被处理了 !
我们把依次将请求发送到每台服务器的行为称为 “轮询负载均衡” 。这是最简单的负载均衡形式之一, 在你的服务器性能相当且所有 请求延迟 相近 时表现的不错。

当轮询发送没法解决问题

在实际情况中,服务器的性能很少能保持在完全一致的水平, 每个请求的处理耗时也不会完全相等。这样导致即使使用的是相同的服务器硬件,不同服务器之间的性能也会存在差异。应用程序需要处理不同类型的请求, 这也会对各个请求的响应时间产生影响。

让我们看看当请求的响应时间各不相同时会发生什么。

下面是模拟了不同响应时间的请求情况。你可以通过观察到某些请求缩小的时间慢于其他的请求。

虽然大多数请求都能成功响应, 但我们确实会丢弃一些请求。缓解这个问题的方法之一是使用 “请求队列”

请求队列虽然可以帮助我们缓解一些由于(服务器性能不均、请求处理时间不均、请求类型不同等)导致的请求丢弃, 但这也需要付出一定代价, 那就是一些请求的延迟会更高。

如果你观察这个示例足够长时间,可能会发现请求的颜色会有细微的变化。随着等待响应的时间越长,请求颜色就会越深。同时也能看出,因为请求的响应时间不确定,服务器处理请求的能力开始出现失衡。

队列会在需要处理多个响应时间过长请求的服务器上发生堆积。如果队列满了, 就会发生请求的丢弃

上述所有内容同样适用于性能各异的服务器。在下一个模拟例子中,我们也修改了每个服务器的性能,用更深的灰色表示。

由于服务器的性能是随机的,性能较弱的服务器很快就开始丢弃请求,而性能更强的服务器大部分时间处于空闲状态。
这个场景展示了轮询的关键缺点: 差异(无法根据性能差异合理分发请求)。

尽管存在这些缺陷,但轮询仍然是 nginx 的默认 HTTP 负载均衡算法。

改进轮询

可以通过调整轮询算法来在服务器性能不均的情况下取得更好的效果。

有一种叫做 “加权轮询” 的算法,它让人们为每台服务器添加一个权重值, 以决定发送多少请求到该服务器。

在这个样例中, 我们使用每个服务器已知的性能值作为其权重, 在轮询时给性能更强的服务器分配更多的请求。

虽然与普通的轮询相比,这种方式可以更好适配服务器性能差异问题, 但我们仍需关注请求耗时差异问题。实际上,手动设置权重的方法在运行一段时间后将会失效。

将服务器性能量化为一个数字是很困难的,你需要使用实际请求进行仔细的负载测试。

很少进行这样的测试, 所以加权轮询的另一种变体是通过使用代理的一个指标动态计算权重: 延迟。

按理说,如果一台服务器处理请求的速度比另一台服务器快 3 倍,那么它的速度可能是另一台服务器的 3 倍,并且应该比另一台服务器接收多 3 倍的请求。

这次我在每台服务器下方添加了显示最后3个请求的平均延迟的文本。然后我们根据延迟的相对差值决定向每台服务器发送1、2或3个请求。

其结果与最初的加权轮询模拟非常相似, 但是无需提前指定每个服务器的权重。 该算法还能够随时间适应服务器性能的变化。

这称为 “动态加权轮询”

让我们看看它如何处理一个复杂的情况, 服务器性能和请求成本都存在高方差。

下面的模拟使用了随机值, 所以你可以随意刷新几次页面来查看它如何适应新的变化

移除轮询

动态加权轮询似乎能很好地适应服务器性能和请求耗时差异。不过我们可以用一个更简单的算法做得更好。

这称为 最少连接 负载均衡。

因为 负载均衡设施 位于服务器和用户之间, 它可以准确地跟踪每个服务器当前有多少待处理的请求。

当一个新请求到来并需要确定发送到哪里时, 它知道哪些服务器待处理的请求最少, 并优先将这些请求发往这些服务器。

这个算法的表现极佳, 无论异变 ( 请求耗时的方差 / 服务器性能的方差 ) 有多大。通过准确了解每个服务器在做什么来 尝试 消除不确定性。

实现简单是它的优点。

我们在相似的复杂的模型中观察实际效果, 使用与上述动 态加权轮询 相同的参数。

同样, 这些参数在给定范围内是随机化的, 所以刷新页面可以看到新的变化。

虽然这个算法在 实现简单性能 之间达到了很好的平衡, 但它也无法完全避免请求的丢失。

不过, 这个算法仅在所有的队列已经饱和时才会丢弃请求。

它会确保所有可用资源都被利用, 这使得它成为大多数工作负载的默认选择。

延迟的优化

到目前为止, 文章其实一直在避免讨论的部分: 我们要优化的是什么。

文章中一直默认将 保证请求被处理 视为主要, 并试图避免发生。这是一个不错的目标, 但它不是我们在 HTTP 负载均衡器中最需要优化的指标。

我们更关心的通常是 延迟。指的是从请求 创建开始请求完成花费 的毫秒数。

当我们进行讨论延迟时, 通常会谈到不同的 “百分位数”

例如, 第 50 百分位数 (也称为 中位数) 被定义为 50% 的请求 低于 这个毫秒值, 50% 高于这个值。

我运行了 3 次模拟, 每个模拟参数相同, 持续 60 秒, 每秒进行各种测量。

每个模拟仅通过使用的负载均衡算法有所不同。让我们比较这 3 次模拟的中位数:

令人意外的是轮询算法具有最低的中位数延迟。让我们来看看 95 和 99 百分位数的情况

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

可以看到轮询算法在更高的百分位数上表现不佳。轮询算法中位数很好,但 95 和 99 百分位数差,这怎么可能?

在轮询中, 不考虑每个服务器的状态, 所以会有相当多的请求被发送到空闲的服务器。这就是我们得到低中位数的原因。

另一方面, 我们也会继续向过载的服务器发送请求, 这就导致了糟糕的 95 和 99 百分位数。

我们可以以直方图的形式再次观察数据:

目前选择的参数 不会 丢弃任何请求。这保证了我们对所有 3 种算法比较了相同数量的数据点。

让我们再次执行, 但提高 RPS 值, 为了显现算法无法处理的极限。

下图显示了随时间累积丢弃的请求数量。

“最少连接” 在过载时的情况要好得多, 但这样做的成本是 95 和 99 百分位数延迟略有增加。

依据你的实际情况, 这可能对你是一个不错的方案.

最后一个算法

如果延迟是对你的应用特别敏感的, 那我们需要一个将延迟考虑在内的算法。

如果我们能够将动态加权轮询算法与最小连接算法结合起来,同时获得加 权轮询的低延迟最小连接的弹性 , 那不是很棒吗?

事实证明,我们并不是第一个有此想法的人。下面展示是使用一种称为 “峰值指数加权移动平均” (PEWMA)的算法。
这个名字很长很复杂, 但请耐心听我说完

这个模拟设置了一些特定的参数, 以保证预期的行为发生。如果你仔细观察, 你会发现这个算法在一段时间后就停止向最左的服务器发送请求了.

这是因为它发现让其他的服务器处理请求可以更快, 向最慢的服务器发送请求没有必要。那只会导致请求延迟增长。

这个算法是如何工作的呢? 它结合了 动态加权轮询最少连接 两种技术, 并在顶部添加了额外的优化。

对每一个服务器,算法会跟踪最后 N 个请求的延迟时间 Latency1, Latency2, … LatencyN 然后它计算这些延迟的 加权和:

这里 w1 > w2 >…> wN, 是按指数衰减的权重。也就是说, 最早 发送的请求 被处理耗时延迟 贡献的值更大, 晚发送的请求被处理延迟贡献越小。

最后,这个 Sum 与服务器当前打开的连接数 ConnNum 相乘,得到一个最终的评分:

我们就根据这个 Score 值最小的原则, 来选择处理下一个请求的服务器。

这样利用了动态加权轮询的延迟状况, 同时也考虑了最小连接数的负载状况, 实现了更智能的负载均衡。

与其他算法相比, 它的表现如何呢? 首先, 让我们看看与之前最小连接数据相比的 50、95 和 99 百分位数。

我们可以看到各个指标上都有显著的提高!

在更高的百分位数上改进尤为明显, 但中位数也一直保持较低。下面我们以直方图的形式看到相同的数据。

那么请求的丢失情况呢?

它起初的表现更好, 但随着时间的推移, 表现比最小连接更差。

这是有原因的。 PEWMA 具有随机性, 它试图获得最低延迟, 这意味着它有时可能会使各服务器负载不充分。

我想在这里补充的是, PEWMA 有很多可调参数。我在这篇文章中编写的实现使用了在我测试的情况下似乎表现良好的配置, 但进一步调整参数可能会相对最小连接获得更好的结果。

这是 PEWMA 相对于最小连接的缺点之一: 额外的复杂性

结论

这篇文章上花了我很长时间。很难在现实性和易于理解性之间找到平衡, 但我对最终的结果很满意。

我希望通过观察这些复杂系统在理想和不太理想的场景下的实际运行, 可以帮助你对适合自己工作负载的最佳选择产生直观的理解。

⚠ 必要的免责声明 ⚠: 你必须要基于自己的工作负载进行基准测试, 而不是将互联网上的建议奉为圭臬。

我在这里的模拟忽略了一些现实中的约束(服务器延迟启动、网络延迟等), 并设置了显示每个算法特定属性的场景。

它们不能作为面值接受的真实基准测试。

为了完善这个主题,我留给你一个可以在实时调整大多数参数的模拟版本。玩得开心点!

动手试试 !!!

本文中的动画是使用 PixiJS 进行绘制的
原文地址: https://samwho.dev/load-balancing/

Author: Sean
Link: https://blog.whileaway.io/posts/9d67a061/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.