版本比较

标识

  • 该行被添加。
  • 该行被删除。
  • 格式已经改变。

IPVS Service 模式

iptables 模式下 iptables 的规则后期会随着 pod 增加成正比增加,导致 iptables 匹配复杂度上升,K8S 在 v1.11 添加了 IPVS 模式。

环境准备

管理 IPVS 规则需要安装 ipvsadm ,这里用两台干净的 CentOS 7.8 来做环境。

IP
192.168.2.111
192.168.2.222

先配置基础组件

代码块
languagebash
yum install -y ipvsadm curl wget tcpdump ipset conntrack-tools

# 开启转发

sysctl -w net.ipv4.ip_forward=1

# 确认 iptables 规则清空
$ iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
$ iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT

思考

因为 SVC 端口和 POD 的端口不一样,所以 kube-proxy 使用的 nat 模式。暂且打算添加一个下面类似的 SVC 

代码块
languagebash
IP:                169.254.11.2
Port:              https  80/TCP
TargetPort:        8080/TCP
Endpoints:         192.168.2.111:8080,192.168.2.222:8080
Session Affinity:  None

web 的话使用的 golang 的一个简单 web 二进制起的 ran

代码块
languagebash
wget https://github.com/m3ng9i/ran/releases/download/v0.1.6/ran_linux_amd64.zip
unzip -x ran_linux_amd64.zip
mkdir www

# 两个机器创建不同的 index 文件
echo 192.168.2.111 > www/test
echo 192.168.2.222 > www/test

./ran_linux_amd64 -port 8080 -listdir www

两个机器的这个 web 都起来后开个窗口去 192.168.2.111 上继续后面的操作。

lvs nat

kube-proxy 并没有像 lvs nat 那样有单独的机器做 NAT GW,或者认为每个 node 都是自己的 NAT GW。现在来添加 169.254.11.2:80 这个 SVC ,使用 ipvsadm 添加:

代码块
languagebash
ipvsadm --add-service --tcp-service 169.254.11.2:80 --scheduler rr

先添加本地的 web 作为 real server ,下面含义是添加为一个 nat 类型的 real server :

代码块
languagebash
ipvsadm --add-server --tcp-service 169.254.11.2:80 \
  --real-server 192.168.2.111:8080 --masquerading --weight 1

查看下当前列表:

代码块
languagebash
$ ipvsadm -ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  169.254.11.2:80 rr
  -> 192.168.2.111:8080           Masq    1      0          0 

因为是自己的 NAT GW,所以 VIP 配置在自己身上:

代码块
languagebash
ip addr add 169.254.11.2/32 dev eth0

测试下访问看看:

代码块
languagebash
$ curl 169.254.11.2/www/test
192.168.2.111

添加上另一个节点的 8080:


代码块
languagebash
$ ipvsadm --add-server --tcp-service 169.254.11.2:80 \
  --real-server 192.168.2.222:8080 --masquerading --weight 1

$ ipvsadm -ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  169.254.11.2:80 rr
  -> 192.168.2.111:8080           Masq    1      0          0         
  -> 192.168.2.222:8080           Masq    1      0          0

测试下访问看看:

代码块
languagebash
$ curl 169.254.11.2/www/test

发现 curl 在卡住和能访问返回 192.168.2.111 之间切换,没有返回 192.168.2.222 的。查看下 IPVS 的 connection ,发现调度到非本机才会卡住:

代码块
languagebash
$ ipvsadm -lnc
IPVS connection entries
pro expire state       source             virtual            destination
TCP 00:48  SYN_RECV    169.254.11.2:50698 169.254.11.2:80    192.168.2.222:8080

在 192.168.2.222 上抓包看看:

代码块
languagebash
$ tcpdump -nn -i eth0 port 8080
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
07:38:26.360716 IP 169.254.11.2.50710 > 192.168.2.222.8080: Flags [S], seq 768065283, win 43690, options [mss 65495,sackOK,TS val 12276183 ecr 0,nop,wscale 7], length 0
07:38:26.360762 IP 192.168.2.222.8080 > 169.254.11.2.50710: Flags [S.], seq 2142784980, ack 768065284, win 28960, options [mss 1460,sackOK,TS val 676518144 ecr 12276183,nop,wscale 7], length 0
07:38:27.362848 IP 169.254.11.2.50710 > 192.168.2.222.8080: Flags [S], seq 768065283, win 43690, options [mss 65495,sackOK,TS val 12277186 ecr 0,nop,wscale 7], length 0
07:38:27.362884 IP 192.168.2.222.8080 > 169.254.11.2.50710: Flags [S.], seq 2142784980, ack 768065284, win 28960, options [mss 1460,sackOK,TS val 676519146 ecr 12276183,nop,wscale 7], length 0
07:38:28.562629 IP 192.168.2.222.8080 > 169.254.11.2.50710: Flags [S.], seq 2142784980, ack 768065284, win 28960, options [mss 1460,sackOK,TS val 676520346 ecr 12276183,nop,wscale 7], length 0
07:38:29.368811 IP 169.254.11.2.50710 > 192.168.2.222.8080: Flags [S], seq 768065283, win 43690, options [mss 65495,sackOK,TS val 12279192 ecr 0,nop,wscale 7], length 0
07:38:29.368853 IP 192.168.2.222.8080 > 169.254.11.2.50710: Flags [S.], seq 2142784980, ack 768065284, win 28960, options [mss 1460,sackOK,TS val 676521152 ecr 12276183,nop,wscale 7], length 0
07:38:31.562633 IP 192.168.2.222.8080 > 169.254.11.2.50710: Flags [S.], seq 2142784980, ack 768065284, win 28960, options [mss 1460,sackOK,TS val 676523346 ecr 12276183,nop,wscale 7], length 0
07:38:33.376829 IP 169.254.11.2.50710 > 192.168.2.222.8080: Flags [S], seq 768065283, win 43690, options [mss 65495,sackOK,TS val 12283200 ecr 0,nop,wscale 7], length 0
07:38:33.376869 IP 192.168.2.222.8080 > 169.254.11.2.50710: Flags [S.], seq 2142784980, ack 768065284, win 28960, options [mss 1460,sackOK,TS val 676525160 ecr 12276183,nop,wscale 7], length 0
07:38:37.562632 IP 192.168.2.222.8080 > 169.254.11.2.50710: Flags [S.], seq 2142784980, ack 768065284, win 28960, options [mss 1460,sackOK,TS val 676529346 ecr 12276183,nop,wscale 7], length 0

从 Flags 看,就是 tcp 重传,并且 SRC IP 是 VIP 。节点 192.168.2.222.8080 给 169.254.11.2.50710 回包会走到网关上去。网关上抓包也看到确实如此:

代码块
languagebash
$ tcpdump -nn -i eth0 host 169.254.11.2
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
19:39:47.487362 IP 192.168.2.222.8080 > 169.254.11.2.50714: Flags [S.], seq 4149799699, ack 251479303, win 28960, options [mss 1460,sackOK,TS val 676599263 ecr 12357303,nop,wscale 7], length 0
19:39:47.487405 IP 192.168.2.222.8080 > 169.254.11.2.50714: Flags [S.], seq 4149799699, ack 251479303, win 28960, options [mss 1460,sackOK,TS val 676599263 ecr 12357303,nop,wscale 7], length 0
19:39:48.487838 IP 192.168.2.222.8080 > 169.254.11.2.50714: Flags [S.], seq 4149799699, ack 251479303, win 28960, options [mss 1460,sackOK,TS val 676600264 ecr 12357303,nop,wscale 7], length 0
19:39:48.487868 IP 192.168.2.222.8080 > 169.254.11.2.50714: Flags [S.], seq 4149799699, ack 251479303, win 28960, options [mss 1460,sackOK,TS val 676600264 ecr 12357303,nop,wscale 7], length 0
19:39:49.569667 IP 192.168.2.222.8080 > 169.254.11.2.50714: Flags [S.], seq 4149799699, ack 251479303, win 28960, options [mss 1460,sackOK,TS val 676601346 ecr 12357303,nop,wscale 7], length 0
19:39:49.569699 IP 192.168.2.222.8080 > 169.254.11.2.50714: Flags [S.], seq 4149799699, ack 251479303, win 28960, options [mss 1460,sackOK,TS val 676601346 ecr 12357303,nop,wscale 7], length 0

也就是如图

为了避免这种问题,需要让 DNAT 到非本机的包的来源 IP 是 192.168.2.111,也就是做 SNAT。

lvs 和 netfilter

在介绍 lvs 的实现之前,需要了解 netfilter ,Linux 的所有数据包都会经过它,而我们使用的 iptables 是用户态提供的操作工具之一。Linux 内核处理进出的数据包分为了 5 个阶段。netfilter 在这 5 个阶段提供了 hook 点,来让注册的 hook 函数来实现对包的过滤和修改。下图的 local process 就是上层的协议栈。

下面是 IPVS 在 netfilter 里的模型图,IPVS 也是基于 netfilter 框架的,但只工作在 INPUT 链上,通过注册 ip_vs_in 钩子函数来处理请求。因为 VIP 我们配置在机器上(常规的 lvs nat 的 VIP 是在 NAT GW 上,我们这里是自己),我们 curl 的时候就会进到 INPUT 链,ip_vs_in 会匹配然后直接跳转触发 POSTROUTING 链,跳过 iptables 规则。

所以我们希望的请求流程是:


代码块
languageplain
# CIP: client IP    # RIP: real server IP

    CLIENT
       | CIP:CPORT -> VIP:VPORT
       |        ||
       |        \/
           | CIP:CPORT -> VIP:VPORT
       LVS DNAT
          | CIP:CPORT -> RIP:RPORT # DNAT 后,也要做 SNAT
          |        ||
       |        \/
       | CIP:CPORT -> RIP:RPORT
       +
    REAL SERVER

lvs 做了 DNAT 并没有做 SNAT ,所以我们利用 iptables 做 SNAT 

代码块
languagebash
$ iptables -t nat -A POSTROUTING -m ipvs --vaddr 169.254.11.2 --vport 80 -j MASQUERADE

访问看看还是不通,抓包看还是没生效,nat 是依赖 conntrack 的,而 IPVS 默认不会记录 conntrack,我们需要开启 IPVS 的 conntrack 才可以让 MASQUERADE 生效。

代码块
languagebash
# 让 Netfilter 的 conntrack 状态管理功能也能应用于 IPVS 模块
$ echo 1 >  /proc/sys/net/ipv4/vs/conntrack
$  curl 169.254.11.2/www/test
192.168.2.111
$ curl 169.254.11.2/www/test
192.168.2.222
$ curl 169.254.11.2/www/test
192.168.2.111
$ curl 169.254.11.2/www/test
192.168.2.222
$ curl 169.254.11.2/www/test
192.168.2.111
$ curl 169.254.11.2/www/test
192.168.2.222

现在实现了单个 SVC 的,但是仔细思考下还是有问题,如果后续增加另一个 SVC 又得增加一个 iptables snat MASQUERADE 规则了,那就又回到 iptables 的匹配复杂度耗时长上去了。所以我们可以利用 iptables 的 mark 和 ipset 配合减少 iptables 规则。

iptables 的五链四表如上图所示,我们先删掉原有的规则:

代码块
languagebash
$ iptables -t nat -D POSTROUTING -m ipvs --vaddr 169.254.11.2 --vport 80 -j MASQUERADE

平时自己家里使用了软路由 ,之前看了下上面的 iptables 规则设计挺好的,特别是预留了很多链专门给用户在合适的位置插入规则,比如下面的 INPUT 规则:

代码块
languagebash
-A INPUT -i eth0 -m comment --comment "!fw3" -j zone_lan_input
...
-A zone_lan_input -m comment --comment "!fw3: Custom lan input rule chain" -j input_lan_rule
-A zone_lan_input -m conntrack --ctstate DNAT -m comment --comment "!fw3: Accept port redirections" -j ACCEPT
-A zone_lan_input -m comment --comment "!fw3" -j zone_lan_src_ACCEPT

zone_lan_src_ACCEPT 是最后面,zone_lan_input 是最开始,那用户向 input_lan_rule 链里插入规则即可,利用多个链来设计也方便别人。 规则设计我们先逆着来思考下,最后肯定是 MASQUERADE 的,得在 nat 表的 POSTROUTING 链创建 MASQUERADE 的规则。

但是添加之前先思考下,lvs 做了 DNAT 后,最后包走向了 POSTROUTING 链,而且后面我们是有多个 SVC 的。此刻包的 SRC IP 会是每个对应的 VIP,见上面抓包的结果:


代码块
languageplain
# 假设没做 masq 的时候(刚好调度到非本地的 real server 上
#也就是上面之前不通在目标机器上抓包)包的阶段

SRC:169.254.11.2:xxxx
DST:169.254.11.2:80
      ||
      || 没经过 POSTROUTING 做 masq snat 的源 IP
      \/
SRC:169.254.11.2:xxxx
DST:192.168.2.222:8080

# 第二个 svc
SRC:169.254.11.33:xxxx
DST:169.254.11.33:80
      ||
      || 没经过 POSTROUTING 做 masq snat 的源 IP
      \/
SRC:169.254.11.33:xxxx
DST:192.168.2.222:8090

因为会被 DNAT,而来源 IP 除去 VIP 以外,后续可能是在 docker 环境上部署,可能默认桥接网络下的容器也会去访问 SVC,此刻的 SRC IP 就不会是网卡上的 VIP 了,所以我们在 PREROUTING 阶段 dest IP,dest Port 是 svc 信息则做 masq snat。

可以在此刻利用一个 ipset 存储所有的 SVC_IP:SVC_PORT 匹配,然后打上 mark,然后在 POSTROUTING 链去根据 mark 去做 MASQUERADE 。


代码块
languagebash
# PREROUTING 阶段处理

# 提供一个入口链,而不是直接添加在 PREROUTING 链上
iptables -t nat -N ZGZ-SERVICES
iptables -t nat -A PREROUTING -m comment --comment "zgz service portals" -j ZGZ-SERVICES

# 在 PREROUTING 子链里去 ipset 匹配,跳转到我们打 mark 的链
iptables -t nat -N ZGZ-MARK-MASQ
# 创建存储所有 `SVC_IP:SVC_PORT` 的 ipset 
ipset create ZGZ-CLUSTER-IP hash:ip,port -exist

# 专门 mark 的链
iptables -t nat -A ZGZ-MARK-MASQ -j MARK --set-xmark 0x2000/0x2000

# 匹配 svc ip:svc port 的才跳转到打 mark 的链里
iptables -t nat -A ZGZ-SERVICES -m comment --comment "zgz service cluster ip + port for masquerade purpose" -m set --match-set ZGZ-CLUSTER-IP dst,dst -j ZGZ-MARK-MASQ


# POSTROUTING 阶段处理

# 提供一个入口链,而不是直接添加在 POSTROUTING 链上
iptables -t nat -N ZGZ-SERVICES-POSTROUTING
iptables -t nat -A POSTROUTING -m comment --comment "zgz postrouting rules" -j ZGZ-SERVICES-POSTROUTING
# 在 POSTROUTING 阶段,有 mark 标记的就做 snat
iptables -t nat -A ZGZ-SERVICES-POSTROUTING -m comment --comment "zgz service traffic requiring SNAT" -m mark --mark 0x2000/0x2000 -j MASQUERADE

然后添加下 SVC_IP:SVC_PORT 到我们的 ipset 里:

代码块
languagebash
ipset add ZGZ-CLUSTER-IP 169.254.11.2,tcp:80 -exist

上面我们创建的 ipset 里 ip,port 和 iptables 里 --match-set 后面的 dst,dst 组合在一起就是 DEST IP 和 DEST PORT 同时匹配,下面是一些举例:

ipset typeiptables match-setPacket fields
hash:net,port,netsrc,dst,dstsrc IP CIDR address, dst port, dst IP CIDR address
hash:net,port,netdst,src,srcdst IP CIDR address, src port, src IP CIDR address
hash:ip,port,ipsrc,dst,dstsrc IP address, dst port, dst IP address
hash:ip,port,ipdst,src,srcdst IP address, src port, src ip address
hash:macsrcsrc mac address
hash:macdstdst mac address
hash:ip,macsrc,srcsrc IP address, src mac address
hash:ip,macdst,dstdst IP address, dst mac address
hash:ip,macdst,srcdst IP address, src mac address

然后访问下还是不通,通过两台机器轮询负载均衡的 curl html 返回内容看到了访问不通的时候都是调度到非本机,也就是此刻的 curl 只经过 OUTPUT 链,过 POSTROUTING 的时候并没有 mark 也就不会做 masq , 调试了下发现确实会走 OUTPUT 链:

代码块
languagebash
$ echo 'kern.warning /var/log/iptables.log' >> /etc/rsyslog.conf
$ systemctl restart rsyslog
$ iptables -t nat -I OUTPUT -m set --match-set ZGZ-CLUSTER-IP dst,dst  -j LOG --log-prefix '**log-test**'
$ curl 169.254.11.2/www/test
^C
$ cat /var/log/iptables.log
Sep 27 23:17:51 centos7 kernel: **log-test**IN= OUT=lo SRC=169.254.11.2 DST=169.254.11.2 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=44864 DF PROTO=TCP SPT=50794 DPT=80 WINDOW=43690 RES=0x00 SYN URGP=0 
Sep 27 23:17:52 centos7 kernel: **log-test**IN= OUT=lo SRC=169.254.11.2 DST=169.254.11.2 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=2010 DF PROTO=TCP SPT=50796 DPT=80 WINDOW=43690 RES=0x00 SYN URGP=0 

需要添加下面规则,让它也进下 svc 判断和打 mark:

代码块
languagebash
iptables -t nat -A OUTPUT -m comment --comment "zgz service portals" -j ZGZ-SERVICES
# OUTPUT 链上或许 ipvs 同样具有 dnat 能力
# 在 2010 年 ipvs 已经移除了 NF_INET_POSTROUTING HOOK 点,并且在 NF_INET_LOCAL_OUT 添加了新 HOOK 用于支持 IPVS 的 LocalNode 功能
# https://github.com/torvalds/linux/commit/cf356d69db0afef692cd640917bc70f708c27f14
# https://github.com/torvalds/linux/commit/cb59155f21d4c0507d2034c2953f6a3f7806913d

keepalived 的自动化实现

到目前为止都是手动挡,而且没健康检查,其实我们可以利用 keepalived 做个自动挡的。

安装 keepalived 2

CentOS7 自带的源里 keepalived 版本很低,我们安装下比自带新的版本:

代码块
languagebash
yum install -y http://www.nosuchhost.net/~cheese/fedora/packages/epel-7/x86_64/cheese-release-7-1.noarch.rpm
yum install -y keepalived
# 备份下自带的配置文件
cp /etc/keepalived/keepalived.conf{,.bak}

配置 keepalived

我们需要配置下 keepalived ,修改之前先看下默认相关的:


代码块
languagebash
$ systemctl cat keepalived
# /usr/lib/systemd/system/keepalived.service
[Unit]
Description=LVS and VRRP High Availability Monitor
After=syslog.target network-online.target

[Service]
Type=forking
KillMode=process
EnvironmentFile=-/etc/sysconfig/keepalived
ExecStart=/usr/sbin/keepalived $KEEPALIVED_OPTIONS
ExecReload=/bin/kill -HUP $MAINPID

[Install]
WantedBy=multi-user.target
$ cat /etc/sysconfig/keepalived
# Options for keepalived. See `keepalived --help' output and keepalived(8) and
# keepalived.conf(5) man pages for a list of all options. Here are the most
# common ones :
#
# --vrrp               -P    Only run with VRRP subsystem.
# --check              -C    Only run with Health-checker subsystem.
# --dont-release-vrrp  -V    Dont remove VRRP VIPs & VROUTEs on daemon stop.
# --dont-release-ipvs  -I    Dont remove IPVS topology on daemon stop.
# --dump-conf          -d    Dump the configuration data.
# --log-detail         -D    Detailed log messages.
# --log-facility       -S    0-7 Set local syslog facility (default=LOG_DAEMON)
#

KEEPALIVED_OPTIONS="-D"

/etc/sysconfig/keepalived 里修改为下面:

代码块
languagebash
KEEPALIVED_OPTIONS="-D --log-console --log-detail --use-file=/etc/keepalived/keepalived.conf"

我们选择在主配置文件里去 include 子配置文件,keepalivd 接收 kill -HUP 信号触发 reload ,后续自动化添加 SVC 的时候添加子配置文件后发送信号即可。


代码块
languagebash
cat > /etc/keepalived/keepalived.conf << EOF
! Configuration File for keepalived

global_defs {

}
# 记住 keepalived 的任何配置文件不能有 x 权限
include /etc/keepalived/conf.d/*.conf
EOF
mkdir -p /etc/keepalived/conf.d/

写一个脚本,一个是用来添加一个子配置文件里的相关信息到 ipset 里,另一方面也让它在重启或者启动 keepalived 的时候每次能初始化,先添加 systemd 的部分:

代码块
languagebash
mkdir -p /usr/lib/systemd/system/keepalived.service.d
cat > /usr/lib/systemd/system/keepalived.service.d/10.keepalived.conf << EOF
[Service]
ExecStartPre=/etc/keepalived/ipvs.sh
EOF

然后编写脚本 /etc/keepalived/ipvs.sh :


代码块
languagebash
#!/bin/bash

set -e

dummy_if=svc
CONF_DIR=/etc/keepalived/conf.d/


function ipset_init(){
    ipset create ZGZ-CLUSTER-IP hash:ip,port -exist
    ipset flush ZGZ-CLUSTER-IP
    local f ip port protocol
    for f in $(find  ${CONF_DIR} -maxdepth 1 -type f -name '*.conf');do
        awk '{if($1=="virtual_server"){printf $2" "$3" ";flag=1;};if(flag==1 && $1=="protocol"){print $2;flag=0}}' "$f" | while read ip port protocol;do
            # SVC IP port 插入 ipset 里
            ipset add ZGZ-CLUSTER-IP ${ip},${protocol,,}:${port} -exist
            # 添加 SVC IP 到 dummy 接口上
            if ! ip r g ${ip} | grep -qw lo;then
                ip addr add ${ip}/32 dev ${dummy_if}
            fi
        done
    done
}

function create_Chain_in_nat(){
    # delete use -X
    local Chain option
    option="-t nat --wait"

    for Chain in $@;do
    if ! iptables $option -S | grep -Eq -- "-N\s+${Chain}$";then
        iptables $option -N ${Chain}
    fi
    done
}

function create_Rule_in_nat(){
    local cmd='iptables -t nat --wait '
    if ! ${cmd}  --check "$@" 2>/dev/null;then
        ${cmd} -A "$@"
    fi
}

function iptables_init(){
    create_Chain_in_nat ZGZ-SERVICES  ZGZ-SERVICES-POSTROUTING ZGZ-SERVICES-MARK-MASQ

    create_Rule_in_nat ZGZ-SERVICES-MARK-MASQ -j MARK --set-xmark 0x2000/0x2000

    create_Rule_in_nat ZGZ-SERVICES -m comment --comment "zgz service cluster ip + port for masquerade purpose" -m set --match-set ZGZ-CLUSTER-IP dst,dst -j ZGZ-SERVICES-MARK-MASQ

    create_Rule_in_nat PREROUTING -m comment --comment "zgz service portals" -j ZGZ-SERVICES
    create_Rule_in_nat OUTPUT -m comment --comment "zgz service portals" -j ZGZ-SERVICES

    create_Rule_in_nat ZGZ-SERVICES-POSTROUTING -m comment --comment "zgz service traffic requiring SNAT" -m mark --mark 0x2000/0x2000 -j MASQUERADE
    create_Rule_in_nat POSTROUTING -m comment --comment "zgz postrouting rules" -j ZGZ-SERVICES-POSTROUTING

}

function ipvs_svc_run(){
  ip addr flush dev ${dummy_if}
  ipset_init
  iptables_init
  echo 1 > /proc/sys/net/ipv4/vs/conntrack
}
# 无参数则是 keepalived 启动,也可以接收单个配置文件参数
function main(){
  if [ ! -d /proc/sys/net/ipv4/conf/${dummy_if} ];then
    ip link add ${dummy_if} type dummy
  fi

  if [ "$#" -eq 0 ];then
    ipvs_svc_run
    return
  fi

  local file fullFile ip port protocol
  for file in $@;do
    fullFile=${CONF_DIR}/$file
      awk '{if($1=="virtual_server"){printf $2" "$3" ";flag=1;};if(flag==1 && $1=="protocol"){print $2;flag=0}}' "$f" | while read ip port protocol;do
          # SVC IP port 插入 ipset 里
          ipset add ZGZ-CLUSTER-IP ${ip},${protocol,,}:${port} -exist
          # 添加 SVC IP 到 dummy 接口上
          if ! ip r g ${ip} | grep -qw lo;then
              ip addr add ${ip}/32 dev ${dummy_if}
          fi
      done
  done
  # 重新 reload 
  pkill --signal HUP keepalived
}

main $@

脚本就如上面所示,读取 keepalived 的 lvs 文件,把 VIP:PORT 加到 ipset 里,VIP 加到 dummy 接口上,之前是加到 eth0 上,但是业务网卡可能会重启影响,dummy 接口和 loopback 类似,它总是 up 的,除非你 down 掉它,SVC 地址配置在它上面不会随着物理接口状态变化而受到影响。删除掉之前 eth0 上的 VIP ip addr del 169.254.11.2/32 dev eth0,然后把前面的转成 keepalived 的配置文件测试下:


代码块
languagebash
chmod a+x /etc/keepalived/ipvs.sh
cat > /etc/keepalived/conf.d/test.conf << EOF

virtual_server 169.254.11.2 80 {
    delay_loop 3
    lb_algo rr
    lb_kind NAT
    protocol TCP
    alpha #默认是禁用,会导致在启动daemon时,所有rs都会上来,开启此选项下则是所有的RS在daemon启动的时候是down状态,healthcheck健康检查failed。这有助于其启动时误报错误

    real_server  192.168.2.111 8080 {
        weight 1
        HTTP_GET  {
            url {
              path /404
              status_code 404
            }
            connect_port    8080
            connect_timeout 2
            retry 2
            delay_before_retry 2
        }
    }

    real_server  192.168.2.222 8080 {
        weight 1
        HTTP_GET  {
            url {
              path /404
              status_code 404
            }
            connect_port    8080
            connect_timeout 2
            retry 2
            delay_before_retry 2
        }
    }
}
EOF

测试下


代码块
languagebash
# 先清理掉之前手动添加的
ipvsadm --clear

systemctl daemon-reload
systemctl restart keepalived

$ curl 169.254.11.2/www/test
192.168.2.222
$ curl 169.254.11.2/www/test
192.168.2.111
$ curl 169.254.11.2/www/test
192.168.2.222
$ curl 169.254.11.2/www/test
192.168.2.111
$ ip a s svc
4: svc: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether e6:a3:29:07:fa:57 brd ff:ff:ff:ff:ff:ff
    inet 169.254.11.2/32 scope global svc
       valid_lft forever preferred_lft forever

停掉一个 web 后在我们配置的健康检查几秒也剔除了 rs 

代码块
languagebash
$ curl 169.254.11.2/www/test
curl: (7) Failed connect to 169.254.11.2:80; Connection refused
$ curl 169.254.11.2/www/test
192.168.2.111
$ curl 169.254.11.2/www/test
192.168.2.111
$ curl 169.254.11.2/www/test
192.168.2.111
$ curl 169.254.11.2/www/test
192.168.2.111

系统的相关配置

后面重启后发现不通,发现内核模块没加载,使用 systemd-modules-load 去开机加载:


代码块
languagebash
cat  > /etc/modules-load.d/ipvs.conf << EOF
ip_vs
ip_vs_rr
ip_vs_wrr
ip_vs_sh

EOF

cat > /etc/sysctl.d/90.ipvs.conf << EOF 
# https://github.com/moby/moby/issues/31208 
# ipvsadm -l --timout
# 修复ipvs模式下长连接timeout问题 小于900即可
net.ipv4.tcp_keepalive_time=600
net.ipv4.tcp_keepalive_intvl=30
net.ipv4.vs.conntrack=1
# https://github.com/kubernetes/kubernetes/issues/70747 https://github.com/kubernetes/kubernetes/pull/71114
net.ipv4.vs.conn_reuse_mode=0
EOF

其它说明

有人 kubernetes/kubernetes#72236 发现 ipvs 下,访问 svcIP+宿主机的端口,例如 22 也能访问,这不安全,然后就有https://github.com/kubernetes/kubernetes/pull/108460了个链 KUBE-IPVS-FILTER 让 svcIP:非svcPort 无法访问,ping 也 ping 不通了。

docker 运行的方案

docker-compose 文件如下,自己把脚本挂载进去即可:

代码块
languageyaml
version: '3.5'
services:
  keepalived: 
    image: 'registry.aliyuncs.com/zhangguanzhang/keepalived:v2.2.0'
    hostname: 'keepalived-ipvs'
    restart: unless-stopped
    container_name: "keepalived-ipvs"
    labels: 
      - app=keepalived
    network_mode: host
    privileged: true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    volumes:
      - /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
      - /lib/modules:/lib/modules
      - /run/xtables.lock:/run/xtables.lock
      - ./conf.d/:/etc/keepalived/conf.d/
      - ./keepalived.conf:/etc/keepalived/keepalived.conf
      - ./always-initsh.d:/always-initsh.d
      - ./tools:/etc/tools/
    command: 
      - --dont-fork
      - --log-console
      - --log-detail
      - --use-file=/etc/keepalived/keepalived.conf
    logging:
      driver: json-file
      options:
        max-file: '3'
        max-size: 20m

目录