对于Cloudflare来说,负载平衡是他们整个业务的支柱,使他们成为零停机专家。了解为什么磁悬浮算法是他们的首选方法:使用 Maglev 的高可用性负载均衡器
背景
我们运行着许多后端服务,为我们的客户仪表板、API 以及边缘计算功能提供支持。我们拥有并运营后端服务的物理基础设施。我们需要一种有效的方法来路由服务之间以及来自这些数据中心外部的任意 TCP 和 UDP 流量。
以前,这些后端服务的所有流量在到达可用实例之前,都要经过多层有状态的 TCP 代理和 NAT。这种解决方案运行了好几年,但随着业务的增长,它给我们的服务和运维团队带来了许多问题。我们的服务团队需要应对可用性下降的问题,而我们的运维团队在维护负载均衡器服务器时也非常辛苦。
目标
考虑到我们有状态 TCP 代理和 NAT 解决方案的经验,我们在保留自己的基础架构的同时,为替代负载平衡服务设定了几个目标:
- 通过路由决策将源 IP 保留到目标服务器。这使我们能够支持需要客户端 IP 地址作为其操作一部分的服务器,而无需使用诸如 X-Forwarded-For 标头或 PROXY TCP 扩展之类的变通方法。
- 支持后端分布在多个机架和子网的架构。这可以避免现有网络设备无法路由的解决方案。
- 允许运营团队在零停机的情况下进行维护。我们应该能够随时移除负载均衡器,而不会导致任何连接重置或服务停机。
- 使用常用且经过充分测试的 Linux 工具和功能。Linux 中有很多非常酷炫的网络功能可供我们尝试,但我们希望优化到对于那些不主要使用这些负载均衡器的操作员来说,能够尽量减少意外行为。
- 负载均衡器之间没有显式的连接同步。我们发现,负载均衡器之间的通信显著增加了系统复杂性,从而增加了出错的可能性。
- 允许从之前的负载均衡器实现分阶段迁移。我们应该能够在两个实现之间迁移特定服务的流量,以发现问题并增强对系统的信心。
快递公司的烦恼:我们公司有超多仓库(后端服务),要给客户发快递(API请求)、送生鲜(边缘计算)。以前的老办法是让包裹经过好几个中转站(TCP代理/NAT),结果经常出现:
- 包裹送错地方(路由错误)
- 双十一就爆仓(扩容困难)
- 修个传送带整个公司都要停工(维护停机)
我们要的完美方案:
- 让收件人能看到真实寄件地址(保留源IP)
- 包裹可以跨城市配送(多机房支持)
- 修传送带时包裹照常送(零停机维护)
- 只用市面上买得到的传送带(标准Linux工具)
- 调度员之间不用对暗号(无状态同步)
实现零停机时间
以前,当流量到达我们的后端数据中心时,我们的路由器会挑选数据包并将其转发到它所知道的其中一个 L4 负载均衡器服务器。这些 L4 负载均衡器会确定流量对应的服务,然后将流量转发到该服务的其中一个 L7 服务器。
这种架构在正常运行期间运行良好。然而,每当负载均衡器组发生变化时,问题就会迅速显现。我们的路由器会将流量转发到新的负载均衡器组,流量很可能会到达与之前不同的负载均衡器。由于每个负载均衡器都维护着自己的连接状态,因此无法为这些新的正在进行的连接转发流量。这些连接随后会被重置,这可能会给我们的客户带来错误。
一致性哈希
在正常运行期间,我们的新架构的行为与之前的设计类似。我们的路由器会选择一个 L4 负载均衡器,然后将流量转发到服务的 L7 服务器。
当负载均衡器集合发生变化时,会发生显著的变化。由于我们的负载均衡器现在是无状态的,因此无论路由器选择将流量转发到哪个负载均衡器都无关紧要,它们最终都会到达同一个后端服务器。
BGP
我们的负载均衡器服务器使用 BGP 向数据中心的路由器通告服务 IP 地址,这与之前的解决方案相同。我们的路由器会根据一种称为等价多路径路由 (ECMP) 的路由策略来选择哪些负载均衡器将接收数据包。
ECMP 会对数据包中的信息进行哈希处理,从而为该数据包选择一条路径。路由器使用的哈希函数通常是在固件中固定的。如果路由器选择了较差的哈希函数或错误的输入,可能会导致网络和服务器负载不平衡,或者破坏协议层的假设。
我们与网络团队合作,确保在我们的路由器上配置 ECMP 以仅根据数据包的 5 元组(协议、源地址和端口以及目标地址和端口)进行散列。
为了进行维护,我们的运维人员可以撤销 BGP 会话,流量将透明地转移到其他负载均衡器。但是,如果某个负载均衡器突然不可用(例如内核崩溃或电源故障),则在 BGP 保活机制失效并导致路由器终止会话之前,会有短暂的延迟。
路由器可以在路由器和负载均衡器之间使用双向转发检测 (BFD) 协议,以更短的延迟终止 BGP 会话。不同的路由器对 BFD 的限制和约束各不相同,这使得 BFD 在大量使用 L2 链路聚合和 VXLAN 的环境中难以使用。
我们将继续与我们的网络团队合作,使用他们最熟悉的工具和配置来寻找减少终止 BGP 会话的时间的解决方案。
使用 Maglev 选择后端
为了确保所有负载均衡器都将流量发送到相同的后端服务器,我们决定使用Maglev 连接调度程序。Maglev 是一个一致性哈希调度程序,它会对每个数据包中的五元组信息(协议、源地址和端口以及目标地址和端口)进行哈希处理,以确定后端服务器。
通过一致性哈希,每个负载均衡器都会为数据包选择相同的后端服务器,而无需持久化任何连接状态。这使得我们可以在负载均衡器之间透明地移动流量,而无需在它们之间进行显式的连接同步。
IPVS 和 Foo-Over-UDP
我们希望尽可能使用常见且可靠的 Linux 功能。自 21 世纪初以来,Linux 就实现了强大的第 4 层负载均衡器——IP 虚拟服务器 (IPVS)。IPVS自 Linux 4.18 起就支持 Maglev 调度程序。
我们的负载均衡器和应用服务器分布在多个机架和子网中。为了路由来自负载均衡器的流量,我们选择使用 Foo-Over-UDP 封装。
在 Foo-Over-UDP 封装中,会在原始数据包周围添加新的 IP 和 UDP 报头。当这些数据包到达目标服务器时,Linux 内核会移除外部的 IP 和 UDP 报头,并将内部的有效负载重新插入到网络堆栈中进行处理,就像数据包最初在该服务器上接收一样。
与其他封装方法(例如 IPIP、GUE 和 GENEVE)相比,我们认为 Foo-Over-UDP 在功能和灵活性之间取得了良好的平衡。直接服务器返回(Direct Server Return)是作为封装的副产品实现的,即应用服务器绕过负载均衡器,直接回复客户端。封装没有相关的状态,每个服务器只需要一个封装接口即可接收来自所有负载均衡器的流量。
MTU 和封装
我们接受的互联网 IPv4 数据包大小上限,即最大传输单元 (MTU),为 1500 字节。为了确保这些数据包在封装过程中不会被碎片化,我们增加了内部 MTU 的默认值,以适应 IP 和 UDP 报头。
团队低估了在所有设备机架上更改 MTU 的复杂性。我们必须调整所有路由器和交换机的 MTU,包括绑定接口和 VXLAN 接口,以及最终的 Foo-Over-UDP 封装。即使我们精心策划了部署,我们仍然在交换机和服务器堆栈中发现了与 MTU 相关的错误,其中许多错误首先表现为网络其他部分的问题。
节点代理
我们编写了一个 Go 代理,运行在每个负载均衡器上,并与跟踪服务位置的控制平面层进行同步。然后,该代理会根据活跃服务和可用的后端服务器来配置系统。
为了配置 IPVS 和路由表,我们使用基于netlink Go 包构建的包。我们开源了今天构建的 IPVS netlink 包,它支持查询、创建和更新 IPVS 虚拟服务器、目标和统计信息。
不幸的是,iptables 没有官方的编程接口,所以我们必须执行 iptables 二进制文件。代理会计算一组理想的 iptables 链和规则,然后将其与实时规则进行协调。
⚡ 黑科技三件套:
- 【BGP协议】就像快递车上的GPS,实时告诉路由器:"我这还能收件!"(ECMP路由)
- 【Maglev磁悬浮算法】超级智能分拣机,同样地址的包裹永远送到同一仓库(一致性哈希)
- 【IPVS+FOU隧道】给包裹套上隐形防护罩(UDP封装),送到仓库自动拆包
️ 实战演示:
加载快递分拣模块 |
踩过的坑:
- 包裹太大卡在传送带(MTU问题):把所有通道从1米扩到1.2米宽
- 新来的分拣员不懂规矩(iptables规则):给每个包裹贴二维码注释
- 突然停电导致包裹滞留(BFD检测):给所有设备装上心跳检测仪
终极效果:
- 双十一流量暴涨300%?稳如老狗!
- 维修工随时拔电源线?包裹照跑不误!
- 新仓库3分钟接入系统?点个外卖的时间!
这套系统现在还能帮我们的K8s集群智能调度,就像给每个集装箱都装了自动驾驶系统!