[译] 使用 IPVS 进行负载均衡

原文地址: https://medium.com/google-cloud/load-balancing-with-ipvs-1c0a48476c4d

IP 虚拟服务器 (IPVS) 直接在 Linux 内核内部实现 L4 负载平衡. 我需要让它在 GCP 中运行, 认为这不会那么困难, 然后就让我头疼了, 不过这让我获得了许多乐趣 :)

当情况简单时

像 GCP 这样的云环境非常棒, 最初它因为些 网络规则 有点令人迷惑, 但后来你会开始喜欢它提供的所有抽象. 例如, 实现 L4 负载平衡是通过您可以配置的服务完成, 然后就可以遗忘了, 它总是有效的™.

好吧, 的确. 可能并不总是那么简单. 但是很少有场合需要通过查看配置之外的调试来深入. 有时我需要使用 “tcpdump” (我真的喜欢这个), 故事就是这样开始的. 但我没想到它会以什么方式结束.

当前的目标是运行一个4层负载均衡器. 在使用云服务的时候, 有些情况下, 现有的云服务并不能完全满足你的需求. 我(我的客户)决定使用 IPVS. 这是 Linux 虚拟服务 的一部分, 它运行在服务器主机中, 提供单个 VIP(虚拟IP) 用于客户端访问后端真实服务器的服务, 并在传输层上进行负载均衡. 提一嘴, IPVS 被用在 Kubernetes 的 kube-proxy 组件中.

这应该不会太难…

试一逝

所以我立马动手. 我部署了一个非常简单的配置, 包含三个虚拟机, 我测试时喜欢从简单的开始. 客户端计算机、IPVS 的虚拟服务器和真实服务器(LVS 所声称的). 是的, 只有一台真实服务, 负载均衡应该不会太难…

虚拟服务器的 IP 为 10.1.0.6, 这是我目标服务的 VIP. 真实服务器运行一个基于 python 的简单 HTTP 服务器, 监听端口 8085 并响应 Hello! 信息. 我通常选择 8085 这样的端口, 因为进行数据包捕获时, 它不会与 80 或 443 等典型端口中的其他流量混合. 测试一下, 客户端应该能够访问它.

之后我在虚拟服务器上配置 IPVS 规则. 首先进行安装 ipvsadm 这是在内核配置 IPVS 的工具, 然后使用这个工具配置一个 IPVS 服务指向真实服务.

1
2
3
sudo apt-get install ipvsadm
sudo ipvsadm -A -t 10.1.0.6:8085
sudo ipvsadm -a -t 10.1.0.6:8085 -r 10.1.0.50 -m

IPVS 支持 TCP 和 UDP / 三种数据包转发的方法 (DSR/IPIP/masquerading) / 多种负载平衡或调度算法以及许多其他选项. 除了对使用 TCP (-t) 进行测试外, 我只对进行 masquerading/NAT(-m) 感兴趣. 因为其他转发方法(特别是 DSR)不太适合在 GCP 中运行和满足我的目的. 对于其余选项, 我保留默认值. 您可以在 IPVS 手册页中找到更多信息.

简单的配置结束了. 来测测我是否可以通过虚拟服务器的 VIP 从客户端访问真实服务器了……

失败!

两秒钟后没有看到响应, 那么应该是出问题了. 好吧, 不用担心, 是时候使用 tcpdump 了.

什么是 masquerading 呢

我想采集的数据包应该能告诉我发生了什么. 将 tcpdump 运行在三台主机上, 应该能向你呈现完整数据包访问过程, 不过我只向你展示虚拟服务器捕获的网络帧, 因为它处在通信流量的中间.

虚拟服务器收到来自客户端 10.1.0.3 的数据包, 发往 VIP 10.1.0.6;端口也是一样. 然后 IPVS 启动并转换数据包以到达真实服务器 10.1.0.50 并通过网络发送出去. 但是, 源IP地址, 仍然是10.1.0.3!

我以为我已经配置了 masquerading. 对于熟悉 iptables 的人来说, Masquerade 类似于 SNAT, 其中源 IP 被转换为转发数据包的主机(在本例中为虚拟服务器)的 IP.

好吧, 事实证明 IPVS 并不是这样工作的. IPVS 仅执行 DNAT, 这在称为双臂负载平衡的模型中很有用.

负载均衡器充当路由器, 每个网络上都有一个臂, 这些网络之间的所有流量都经过负载均衡器.

当你需要它时将会是特别有用的模型. 避免 SNAT 可以保留客户端 IP 并避免可能出现的转发服务器端口分配和耗尽问题. 但不是我所需要的, 也不是我所期待的! IPVS 说它使用伪装进行转发, 对我来说这是误导.

那么现在怎么办?

回到原始问题上

从这里开始我走得越来越深, 首先退回到iptables, 并认为这应该足够了. 我已经很久没有研究这些主题了……

iptables 规则

IPVS 不进行 SNAT, 因此我考虑通过 iptables 手动进行此操作. 我可以使用 SNAT 或 MASQUERADE (MAS-QUE-RA-DE, 你知道吗?)作为目标.

1
sudo iptables -t nat -A POSTROUTING -s 10.1.0.3 -j MASQUERADE

真实场景不仅需要考虑客户端 IP, 还需要以某种方式考虑整个客户端范围或目的地, 但对于我的测试来说已经足够了. 然后通过curl从客户端发送请求并且……

Whaaat? 什么事情都没有发生, 就像这条规则不存在一样! 这条规则没有被触发吗?

Nope :(

这下我真的很困惑了. 这是一个全新的环境, 系统中没有其他 iptables 规则可以绕过这个规则, 我尝试在其他表和链中设置规则来跟踪数据包的进出, 但没有观察到什么特别的. 除了这个规则没有被触发.

我谷歌了一下, 发现有一个 iptables 与 IPVS 匹配使用. 我不喜欢在不理解事情的情况下就留下它们, 但我认为测试一下它是个好主意.

1
sudo iptables -t nat -A POSTROUTING -m ipvs --vaddr 10.1.0.6 -j MASQUERADE

您猜怎么了? 同样的结果, 或者同样的没有结果. 规则也没有被触发. 然而, 我阅读更多内容后, 发现应该启用 "CONFIG_IP_VS_NFCT" 并将 sysctl var "conntrack" 设置为 1, 即使我也不知道会发生什么.

内核代码和模块

如果你曾经编译过 Linux 内核, 就会认识 CONFIG 格式. 阅读源码发现了这一点:

内核源码文件[net/netfilter/ipvs/Kconfig](https://elixir.bootlin.com/linux/v5.10/source/net/netfilter/ipvs/Kconfig#L325)

就是这个! Netfilter 是 iptables、连接跟踪、IPVS 和其他组件所依赖的数据包过滤的内核框架. 看起来, 如果没有明确启用它, IPVS 就不允许从用户空间访问此过滤. 所以我查了一下:

奇怪的是, 它已启用并且显然内置于内核中. 我又挖掘了点:

内核文件[net/netfilter/ipvs/Makefile](https://elixir.bootlin.com/linux/v5.10/source/net/netfilter/ipvs/Makefile#L14)

我懂了. 这意味着 IP_VS_NFCT 的代码是内核对象 IP_VS 的一部分……

…作为模块加载, 而不是内置功能…

…但这也被加载到正在运行的内核中. 如果你仔细观察, 你还可以看到 xt_ipvs 模块, 它允许匹配从用户空间通过 iptables 配置的数据包的 IPVS 连接属性, 无需使用 modprobe. 所以看起来一切都已就位.

但是等等, 还有一个元素, sysctl var "conntrack":

在 net.ipv4.vs 条目下, 但还未被设置! 我很幸运, 因为仅当 ipvsadm 服务启动时该设置才存在, 我在内核源代码中看到了它, 也给你看看:

[Linux networking documentation for ipvs-sysctl](https://www.kernel.org/doc/html/v5.10/networking/ipvs-sysctl.html)

正如它所述, 如果不设置 conntrack, iptables 不会处理IPVS连接! 所以我设置了它, 握拳祈祷, 然后再次启动 curl:

太棒了, 成功了! 嘿, 远没有. SNAT完成了它的任务, 你可以看到一个 TCP SYN 包,源地址从 10.1.0.3 传输到虚拟服务器IP 10.1.0.6. 真实服务器也发送对应的 SYN-ACK 包到虚拟服务器. 然而, 通信在这一阶段就卡住了, 没有来自客户端的 ACK 回复. 事实上, 看起来 SYN-ACK 包没有被 NAT 传输回去, 这是为什么呢?

NAT 实现是有状态的, 如果一个被 NAT 过的数据包出去了又回来了, 它会被处理以撤销 NAT 操作. 我们可以通过连接跟踪系统检查这一点:

好的, 所以连接跟踪系统确实处理了从 10.1.0.50 到 10.1.0.6 的 SYN-ACK 包(它将连接改变为 SYN_RECV 状态). 那么, 数据包发生了什么事?它在某个点被丢弃了, 但是我怎样才能找出原因呢?

在内核中进行 Debug

我知道, 对内核进行调试听起来像是掉进兔子洞, 但在这一点上我决定弄清这件事的底细. 我考虑了几种内核跟踪工具: KgdbSystemTapperf(我甚至略微试过)、eBPF. 但是我想保持简单, 专注于问题本身.

由于我想调试网络堆栈, 监控内核中的数据包丢弃情况, 又不想太麻烦, 所以我使用了 Dropwatch. 这是一个简单的交互式实用程序, 正是用于此目的. 使用它需要安装一些软件包:

1
sudo apt-get install git libpcap-dev libnl-3-dev libnl-genl-3-dev binutils-dev libreadline6-dev autoconf libtool pkg-config build-essential

你需要克隆项目并且编译它:

1
2
3
4
5
6
git clone https://github.com/nhorman/dropwatch
cd dropwatch
./autogen.sh
./configure
make
sudo make install

当 Dropwatch 运行并检测到数据包丢弃时, 它会告诉你发生丢弃的原始指令指针, 但这几乎没有用. 我需要的是将该指令指针映射到内核对应的函数名, 而 Dropwatch 可以使用内核的符号文件 kallsyms 来完成这件事:

1
sudo dropwatch -l kas

我用它报告丢失的数据包并且在客户端启动了 curl:

啊, 多美好的记忆! 函数符号和十六进制地址:) 这显示数据包丢弃发生在 ip_error 函数,偏移量为 0x7d. 这个函数非常短, 很容易发现调用了 kfree_skb , 也就是内核中用于释放套接字缓冲区的函数.

更有趣的是, ip_error 函数在输入路径中被调用, 以及丢弃数据包的原因. 特别是, "主机不可达" 这个案例引起了我的注意, 它会增加 SNMP 计数器 IpInAddrErrors. 这个案例处理 "目标IP地址不是本地地址, 而且IP转发没有启用" 的情况. 让我们思考一下:

  • 来自真实服务器 10.1.0.50 ➔ 10.1.0.6 的回复包 “被SNAT传输” 回 10.1.0.50 ➔ 10.1.0.3
  • 我们希望那个回复包由 IPVS 处理, 将其转换回对应的 10.1.0.6 ➔ 10.1.0.3, 但是在输入路径 IPVS是在SNAT之前. 所以当包为 10.1.0.50 ➔ 10.1.0.6 不符合 IPVS 的规则, 所以不处理, 而到了 SNAT 转成了 10.1.0.50 ➔ 10.1.0.3.
  • 现在输入路径需要处理一个目标地址为 10.1.0.3 的数据包,而不是这台机器的地址 10.1.0.6. 它无法处理, 所以就丢弃掉了.

为了验证我的理论, 我在启动 curl 时使用 nstat(netstat被弃用了)观察计数器的增长情况, 与每个 ip_error 丢弃相符:

你可能会疑惑, 我怎么知道 IPVS 是在 SNAT 之前启动的? 通过查看源代码. 在那里我也找到了我需要的答案:

IPVS 通过几个地方挂载(Netfilter钩子), 包括 FORWARD 链. 所以我需要做的是在那里访问 IPVS 来处理回复包, 而不是丢弃数据包. 你可能已经猜到怎么做了: 启用 IP 转发.

最后一步

启用 IP转发通常意味着主机将作为路由器运行. 我之前没有启用它, 因为这不是我们的情况, 但不管怎样还是需要它. 注意, GCP 中有一个 VM 设置叫 canIpForward ,如果你真的想让 VM 作为路由器运行, 但重点不是这个, 所以不需要设置

1
sudo sysctl -w net.ipv4.ip_forward=1

最终:

\o/

最后几句

哇, 这真是艰难的一关. 好吧, 在结束之前我有几点想说:

  • LVS 项目不仅由 IPVS 组成, 还有更多组件可以创建高度可扩展和可用的服务器集群, 具有传输层和应用层负载均衡等功能, 以及集群管理. 我只是触及皮毛.
  • 那些组件之一是 Keepalived, 它提供健康检查. 如果你通过 keepalived 而不是 ipvsadm 来配置虚拟服务器和真实服务器, 我在这里解释的 Full NAT 解决方案将无法工作. 我还没有研究为什么.
  • 尽管我只测试了 TCP,但 IPVS 支持 TCP 和 UDP. 而 UDP 支持正是出于这个原因. 我的客户需要对来自 GCP 中的客户端到内部 TCP 和 UDP 流量进行负载均衡. 我们的内部 TCP/UDP 负载均衡不支持内部后端, 而内部 TCP 代理负载均衡支持内部后端但不支持 UDP.
  • 这里解释的测试设置类似于单臂模型, 其中负载均衡器、客户端和服务器共享同一个网络, 需要 Full NAT. 但是, 如果你想避免 SNAT 或者需要保留客户端 IP, 可以通过多网卡负载均衡器创建两臂设置.
  • LVS 官方页面上, 我发现有关 IPVS 从 Linux 内核 2.6.32 版本开始支持 Full NAT 模式的(旧)消息. 然而, 我没有在最近的内核中找到任何代码来证明那种支持, 也没有找到配置的方法.

总的来说, 这是一个有趣的经历!

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