IPVS - Linux 负载均衡器
May 26, 2021 22:00 · 2261 words · 5 minute read
IPVS(IP Virtual Server)是 Linux 内核网络包过滤框架 netfilter 中的一部分,是一种很强大的负载均衡器,例如 Kubernetes 中就在使用。
IPVS 三种工作模式:
- NAT
- Direct Routing
- IPIP
NAT/Masquerade 模式
这种最简单,负载均衡器监听某个虚拟 IP(VIP),每当有数据包过来,目的 IP 会被改成后端服务器中的某个,然后包被递交至那台服务器。这种模式开销很大,因为所有的回包都得从负载均衡器经过,所以会把包的源 IP 改成虚拟 IP。
Direct Routing 模式
也叫网关模式,负载均衡器收到发向 VIP 的包后不动目的 IP,而是将目的 MAC 地址换成后端服务器中的某个。后端服务器也有相同的 VIP,所以它能收到从负载均衡器递交来的包。但是回包会直接发往本地路由器,绕过负载均衡器。
注意,负载均衡器和后端都配有 VIP,通常会导致 IP 地址冲突。但是我们将后端配置成不在网络上 advertise 那个 VIP。这样,网络中的其他人认为,持有 VIP 的就只有负载均衡器。所有包都会通过负载均衡器。
DR 模式最主要的优势在于降低了负载均衡器的负载,因为只需要处理来包了。缺点呢就是有点复杂,并且只在本地网络上有效。
IPIP 模式
IPVS 支持的最后一种叫 IPIP 模式。和 DR 模式类似,不修改目的 MAC 地址了,使用 IP 隧道将过来的 IP 数据包发送给后端。这种方法最大的优点是扩展性,但要是来包太大,MTU 就不够了,可能导致 IP 包碎片化。最简单的办法就是放大 MTU,但也不是想改就改的。
实验
首先需要启动 3 台 Linux 虚机,使用 CentOS 7.8 作为操作系统,第二和第三个节点上安装 Docker 并以 host 网络模式运行 nginx 容器:
$ docker run --name nginx --network host --restart always -d nginx:latest
需要关闭 firewalld
systemctl stop firewalld && systemctl disable firewalld
,否则会导致无法通过主机 IP 访问 nginx 服务。
NAT/Masquerade 模式
NAT 是负载均衡最经典的办法:负载均衡会改变每个来包的目的 IP 地址。
必须先开启 IP 转发:
$ echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
$ sysctl -p
在作为负载均衡器的节点添加 IPVS 规则(VIP 为 10.0.1.1):
$ ipvsadm -A -t 10.0.1.1:80 -s rr
以 Masquerade 的方式(-m
选项)添加两个后端:
$ ipvsadm -a -t 10.0.1.1:80 -r 10.211.55.69:80 -m
$ ipvsadm -a -t 10.0.1.1:80 -r 10.211.55.70:80 -m
$ ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.0.1.1:80 rr
-> 10.211.55.69:80 Masq 1 0 0
-> 10.211.55.70:80 Masq 1 0 0
在负载均衡节点上通过 VIP 来访问:
$ curl http://10.0.1.1
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
通过两个后端节点的 nginx 容器日志就能够看出请求被转发至哪个后端节点。
但是,NAT 在性能方面有明显的短板,但对于小型系统来说问题不大。还要注意,后端看不到原来的目标 IP 因为被负载均衡器改掉了,但仍能拿到源 IP 和端口,这对许多应用程序来说至关重要。
Direct Routing 模式
网关模式也就是 Direct Routing 在操作上有一点棘手。
删除上面创建的负载均衡器:
$ ipvsadm --clear
首先还是搭建负载均衡器(注意要使用 -g
选项):
$ ipvsadm -A -t 10.211.55.8:80 -s rr
$ ipvsadm -a -t 10.211.55.8:80 -r 10.211.55.69:80 -g
$ ipvsadm -a -t 10.211.55.8:80 -r 10.211.55.70:80 -g
$ ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.211.55.8:80 rr
-> 10.211.55.69:80 Route 1 0 0
-> 10.211.55.70:80 Route 1 0 0
现在后端节点也同样需要 VIP,将 VIP 10.211.55.8 添加至回环设备:
$ ip addr add 10.211.55.8/32 dev lo
$ ip addr show lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet 10.211.55.8/32 scope global lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
我们还要将 VIP 10.211.55.8 分配给负载均衡节点的默认网卡(eth0):
$ ip addr add 10.211.55.8/32 dev eth0
在测试节点(可能需要于同一网络另起一台虚机)上通过 VIP 来访问:
$ curl http://10.211.55.8:80
Emmm,多尝试几次我们会发现所有的应答都来自后端节点 1,后端节点 2 却从未应答,这里发生了什么?
这就是麻烦的地方了,后端节点 1 的网络设备(lo)上也分配了 VIP,当测试节点询问与 VIP 绑定的 MAC 时,后端节点 1 与 2 也会应答,取决于查询先到达哪个节点,网关会将应答缓存在它的 ARP 表中。所以我们要采取措施来避免这种情况,VIP 被添加在后端节点的回环设备上。我们要让这两个节点不要公布自己的 VIP:
$ cat <<EOF > /etc/sysctl.d/ipvs.conf
net.ipv4.conf.eth0.arp_ignore=1
net.ipv4.conf.eth0.arp_announce=2
EOF
$ sysctl -p /etc/sysctl.d/ipvs.conf
然后在测试节点上清除 ARP 缓存:
$ ip -s -s neigh flush all
10.211.55.2 dev eth0 lladdr a6:83:e7:ba:3d:64 ref 1 used 0/0/0 probes 4 DELAY
10.211.55.8 dev eth0 lladdr 00:1c:42:69:93:e6 used 75/70/52 probes 4 STALE
*** Round 1, deleting 2 entries ***
*** Flush is complete after 1 round ***
再次 curl 轮询就生效了。
IPIP 模式
IPIP 模式和 Direct Routing 模式有点像因为原 IP 包没变,而是被封装进了另一个 IP 包发送到目标服务器。
第一步还是搭建负载均衡器(注意要使用 -i
选项):
$ ipvsadm -A -t 10.211.55.8:80 -s rr
$ ipvsadm -a -t 10.211.55.8:80 -r 10.211.55.69:80 -i
$ ipvsadm -a -t 10.211.55.8:80 -r 10.211.55.70:80 -i
$ ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.211.55.8:80 rr
-> 10.211.55.69:80 Tunnel 1 0 0
-> 10.211.55.79:80 Tunnel 1 0 0
第二步在后端节点上启用 IPIP 隧道:
# Load the IPIP module
$ modprobe ipip
$ ip link set tunl0 up
接着将 VIP 10.211.55.8 分配给 TUN 设备:
$ ip addr add 10.211.55.8/32 dev tunl0
$ ip addr show tunl0
4: tunl0@NONE: <NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN group default qlen 1000
link/ipip 0.0.0.0 brd 0.0.0.0
inet 10.211.55.8/32 scope global tunl0
valid_lft forever preferred_lft forever
将 VIP 10.211.55.8 分配给负载均衡节点的默认网卡(eth0):
$ ip addr add 10.211.55.8/32 dev eth0
这里同样要像 Direct Routing 模式那样在后端节点上调整 ARP 设置:
$ cat <<EOF > /etc/sysctl.d/ipvs.conf
net.ipv4.conf.eth0.arp_ignore=1
net.ipv4.conf.eth0.arp_announce=2
EOF
$ sysctl -p /etc/sysctl.d/ipvs.conf
如果现在就 curl,啥都没有,因为过来的数据包被 Linux 反向路径过滤了,这个过滤器会检查数据包是否来自出包的方向,在这个案例中不是的。所以要关掉 tunl0 网卡的 rp_filter:
$ sysctl net.ipv4.conf.all.rp_filter=0
$ sysctl net.ipv4.conf.tunl0.rp_filter=0
最后来讨论下 IPIP 模式的一些小陷阱。首先回包不走隧道,而是直接返回客户端。IPIP 模式的重点在于后端和负载均衡器能够处于不同网络。但是要留心回包被其他东西挡掉。
另一件要说的事乃是 IP 分片。通常连接的最大传输单元(MTU)为 1500 字节,如果来包不小于 1500 字节,再次的 IP 封包会使其大小超过 MTU,这就要分片了,会导致性能下降。
健康检查
IPVS 原生不支持监控检查。可以通过 ldirectord 这样的项目来实现。
高可用
幸运的是,IPVS 有一个在多个负载均衡实例间同步会话的机制,会尽快将连接信息同步至备机。
# On the primary:
ipvsadm --start-daemon=master --mcast-interface=eth0
# On the secondary:
ipvsadm --start-daemon=backup --mcast-interface=eth0
这只是尽力局,在故障切换时还是可能丢失部分连接。同步进程不会将规则也一起传输到备机,这得你自己实现。还有一个问题,如何自动分配 VIP?当然绝大多数案例使用 keepalived 就够了。或者,也可以将负载均衡与路由器集成在一起,比如 FRRouting 这个项目。