[大厂实践] Zuul 连接控制实践
本文介绍了 Zuul 如何通过 HTTP2 连接复用和确定性子集算法优化 Zuul 的连接管理,极大减少了连接数量和失效率,优化了集群通信性能。原文: Curbing Connection Churn in Zuul
痛点
在设计和开发Zuul时,有一个固有假设,即如果不用 mTLS(mutual TLS),那么连接实际上没有额外开销。Zuul 构建在Netty之上,通过事件循环来非阻塞的执行请求,每个 CPU 核一个循环。为了减少事件循环的争用,我们为每个事件循环创建了连接池,使它们完全独立。最终整个请求-响应周期在同一线程上处理,大大减少了上下文切换开销。
这里有个明显的缺点。如果每个事件循环都有一个连接池,该连接池连接到每个源(后端)服务器,那么 Zuul 实例的连接数量将会是事件循环与服务器的乘积。例如,连接到 800 个服务端的 16 核实例将有 12,800 个连接。如果 Zuul 集群有 100 个实例,那就是 1,280,000 个连接。这个数量相当大,相对于大多数集群上的流量来说,肯定超过了必要的数量。
随着流媒体多年来的发展,这一数字随着更大规模的 Zuul 和后端集群而成倍增加。更严重的是,如果出现流量高峰,并且 Zuul 实例扩容,会以指数方式增加连接后端的连接。虽然这是个众所周知的问题,但在我们将大型流媒体应用迁移到 mTLS 和基于 Envoy 的服务网格之前,从来没有成为关键痛点。
固定连接流
改善连接开销的第一步是实现 HTTP/2 (H2)多路复用。多路复用允许为每个连接创建多个流来重用现有连接,每个流都可以发送请求,同时发生的请求可以复用连接,而不用为每个请求创建连接。重用的连接越多,建立 mTLS 会话时的协商、握手等开销就越少。
虽然 Zuul 支持 H2 代理已经有一段时间了,但从未支持多路复用,而是将 H2 连接视为 HTTP/1 (H1)。为了向后兼容现有的 H1 功能,我们修改了 H2 连接建立流程,从而可以创建流并立即将连接释放回池中。然后,后续请求能够重用现有连接,而无需创建新连接。理想情况下,到每个服务器的连接应该收敛到每个事件循环 1 个。这似乎是一个很小的变化,但必须无缝集成到现有指标和连接记录中。
发起 H2 连接(基于 TLS)的标准方法是升级ALPN(应用层协议协商,Application-Layer Protocol Negotiation)。如果服务端不支持 H2, ALPN 允许我们优雅的降级回 H1,因此可以在不影响客户的情况下广泛启用。服务网格在许多服务上可用,并且在默认情况下启用了 ALPN,这使得测试和推出此功能非常容易,而且意味着已经使用服务网格和 mTLS 的服务所有者不需要做任何工作。
不幸的是,当我们推出多路复用时遇到了障碍。虽然这个特性很稳定,在功能上没有任何影响,但并没有减少总体连接。原因是某些原始集群太大了,当所有事件循环都对这些集群发起连接,并没有发生足够的重用来触发多路复用。即使现在有了多路复用能力,也没有办法利用。
分而治之
当对所有现有连接需求很大时,H2 复用可以改善负载下的连接峰值,但在稳态下没有帮助。将整个集群划分为子集可以减少总连接数,同时利用多路复用来保持现有的吞吐量和冗余。
多年来,我们已经多次讨论过子集,但担心算法会破坏负载均衡。流量的均匀分布对于准确的金丝雀分析和防止流量热点至关重要。
子集也是谷歌最近发表的一篇ACM论文中提到的最重要的内容,该论文介绍了谷歌已经使用了多年的确定性子集算法的改进。Ringsteady 算法(下图)创建均匀分布的服务器环(黄色节点),然后遍历该环,将它们分配给每个前端任务(蓝色节点)。
上图来自 Google 的 ACM 论文
该算法基于低差异数字序列的思想来创建自然平衡的分布环,它比基于随机的一致性哈希构建的分布环更一致。所使用的特定序列是Van der Corput序列的二进制变体。只要添加服务器的顺序是单调递增的,每增加一个服务器,分布将在 0-1 之间均匀平衡。下面是二进制 Van der Corput 序列的一个例子。
这种分布的另一大好处是,随着时间推移以及服务器的添加和删除,提供了环的一致性扩展,在子集中均匀分布新节点。从而提高子集的稳定性,避免随着时间推移产生级联混乱。每个添加或删除的节点将只影响一个子集,并且每次都将向不同的子集添加新节点。
下面是上述数列的一个更具体的演示,以十进制形式表示,0-1 之间的每个数字分配给 4 个子集。在本例中,每个子集用自己的颜色表示该范围的 1/4。
可以看到,新节点在子集之间的平衡非常好。如果快速添加 50 个节点,它们的分布将同样均匀。同样,如果移除大量节点,对所有子集的影响是相同的。
但真正的杀手级特性是,如果删除或添加节点,不需要对所有子集进行重排,每次更改通常只会创建或删除一个连接。这也适用于更大的变化,减少了子集中几乎所有变化。
Zuul 的工作
我们在 Zuul 中基于上面讨论的思想进行了实现,并与Eureka服务发现集成,将变化反馈到分发环中。当新的服务器在 Zuul 中注册时,加载实例并创建新的环,从那时起,用增量来管理。在将节点添加到环之前,我们还采取额外的步骤来打乱节点顺序,这有助于防止意外的热点发现或 Zuul 实例之间的重叠。
Google 的负载均衡算法的奇怪之处在于它们通过集中化的方式实现负载均衡。Google 的集中式服务在整个集群中创建子集和负载均衡,具有全局视野。要使用该算法,关键在于将其应用于事件循环而不是实例本身,这使我们能够继续使用分布式的客户端负载均衡,同时还获得确定性子集的好处。尽管 Zuul 继续连接到所有服务器,但每个事件循环连接池只需要知道全局的一小部分。最终我们得到一个可以控制每个实例分布的全局视图,以及每个服务器环单独递增的序列号。
当收到请求时,Netty 将其分配给事件循环,并在整个请求-响应生命周期期间保持不变。在执行入站流量过滤器之后,确定目标并加载到事件循环连接池。目标将从事件循环和节点子集的映射中提取,获得匹配的有限节点集。然后用如前所述修改后的算法进行负载均衡。如果听起来很熟悉,那是因为 Zuul 的工作方式没有根本变化,唯一的区别是向负载均衡器提供了一个节点子集,作为其决策起点。
另一个想法是,我们需要在事件循环中改变子集副本的数量,从而可以为大型和小型服务器集群都保持低连接数。同时,拥有合理的子集大小可以确保能够继续为服务器提供良好的平衡和弹性特性。大多数服务器都需要这一特性,因为集群不够大,无法在每个子集中创建足够实例。
然而,太频繁改变副本因子也不好,因为这会导致整个环重新洗牌,并引入大量混乱。经过多次迭代,最终我们通过"理想"子集大小开始实现,通过计算子集大小以及给定原始节点基数,实现理想的副本因子。此外,当根据流量模式扩展或缩小时,通过增加子集来扩展副本因子,直到达到所需的子集大小。最后根据计算的子集大小将环分成偶数片。
理想的子集大约是 25-50 个节点,所以一个有 400 个节点的集群将有 8 个 50 个节点的子集。在 32 核实例上,副本因子为 4。然而,这也意味着在 200 到 400 个节点之间,我们根本没有对子集进行洗牌。这个子集重新计算的示例如下所示。
一个有趣的挑战是,要满足具有一定基数范围的原始节点的双重约束,以及包含子集的事件循环的数量。我们的目标是在具有较高事件循环的实例上运行时扩展子集,使总体连接呈次线性增长,并提供足够的副本以保证可用性。上面介绍的弹性缩放副本因子帮助我们成功实现了这一点。
成功构造子集
结果非常好。我们在 Zuul 的所有关键指标上都看到了改进,但最重要的是,总连接数和失效率显著降低。
总连接数
这张图表(以及下面的图表)显示了一周的数据量,以及 Netflix 典型的日循环。这三种颜色中每一种都代表我们在 AWS 中的部署区域,蓝色竖线表示打开该特性的时间。
所有 3 个区域的峰值总连接数都显著减少了 10 倍,这是一个巨大改进。例如,一台运行 16 个事件循环的机器可能有 8 个子集,每个子集有 2 个事件循环。这意味着相对原来的连接数除以 8,改进了 8 倍。至于为什么峰值改进达到 10 倍,可能与减少失效率有关(见下图)。
失效
这张图表很好的表示了失效率,显示 Zuul 每秒打开多少 TCP 连接。可以很清楚看到前后对比,从峰值到峰值的改进来看,大约有 8 倍改进。
原始集群随着时间推移而扩大、缩小和重新部署,失效率下降证明了子集的稳定性。
具体来看池中创建的连接,减少的数量更令人印象深刻:
峰值的减少是巨大的,并且清楚表明这种分布是多么稳定。虽然从图表上很难看到,但减少的速度从峰值时的每秒数千下降到每秒 60 左右。实际上,即使在流量高峰时,也不会出现连接中断。
负载均衡
对子集的关键约束是确保后端的负载均衡仍然是一致和均匀分布的。原始节点上的所有 RPS 都按照预期紧密分组,较粗的线条表示子集大小和初始大小。
部署时的均衡
部署 12 小时后的均衡
在第二个图中,注意到我们重新计算了子集大小(蓝线),因为初始数量(紫色线)变得足够大,我们可以减少子集副本。在本例中,我们将 400 台服务器的子集大小从 100 台(4 个分区)改为 50 台(8 个分区)。
系统指标
考虑到连接数显著减少,我们看到 Zuul 上的 CPU 利用率(~4%)、堆使用量(~15%)和延迟(~3%)也降低了。
Zuul 金丝雀指标
推广
当我们把这个特性推广到最大的业务——流播放 API 时,我们看到上面的模式还继续有效,但随着规模扩大,变得更加令人印象深刻。在某些 Zuul 分片上,我们看到在峰值时减少了多达 1300 万个连接,并且几乎没有中断。
如今这一功能得到了广泛推广。我们可以提供同样数量的流量,但连接数减少了数千万。尽管连接减少了,但弹性或负载均衡并没有受到影响。H2 多路复用允许我们基于现有连接扩展请求,子集算法确保了均匀的流量均衡。
虽然很难做到正确,但子集是一项值得的投资。
你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!
版权声明: 本文为 InfoQ 作者【俞凡】的原创文章。
原文链接:【http://xie.infoq.cn/article/3e16fb2fceb20ad5f7019941f】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论