Kubernetes 中 IPVS Service 模式

一、K8S Pod 网络

Kubernetes 的网络模型采用了 Overlay 网络架构,这意味着在现有网络上构建一个虚拟网络。具体来说,POD 网络(如 10.244.0.0/16)仅限于 Kubernetes 节点间的访问,外部机器无法直接访问该网络,它们的流量会通过默认路由走出,而不会进入 POD 网络。

在这里插入图片描述

在此网络模型中,cni0 是 Kubernetes 的 CNI 插件接口规范之一。需要注意的是,容器运行时并不仅限于 Docker,还有其他容器运行时实现。因此,Kubernetes 要求每个 Pod 中的容器通过 veth 网络接口挂载到 cbr0 网络桥上,而不限制在 docker0 网桥上。

在 Kubernetes 中,POD 是由一个或多个容器组成的。当 Pod 被创建时,首先会启动一个 pause 容器作为沙箱容器,其他容器通过 --net container:<pause_id> 附加到这个沙箱容器上。

二、Service 工作原理

Kubernetes Service 提供了一种机制,通过 Service IP 和 Service Port 将流量动态转发到 Pod 的实际 IP 和端口。它的工作流程如下所示:

在这里插入图片描述

Kubernetes 服务相关的工作进程和大体工作流程如下:

在这里插入图片描述

  1. kubelet:负责管理 Pod 的增、删、改操作,并进行健康检查。健康检查方式包括 HTTP、TCP 和 exec,如果探针失败,kubelet 会标记 Pod 为不健康并上报给 kube-apiserver。

  2. kube-proxy:监听到 Pod 状态变化(如 Pod 就绪、删除或不健康)后,会根据这些变化更新本节点的 iptables 或 ipvs 规则。

可以通过以下命令查看当前节点 kube-proxy 的模式:

$ curl localhost:10249/proxyMode
iptables

虽然 Service 工作在内核态的四层(TCP/UDP/STCP)负载均衡,但它依赖 IP 地址来进行流量转发。因此,Service IP 也被称为虚拟 IP(VIP)。官方文档中的默认 CIDR 是 10.96.0.0/12,如果需要更改 CIDR,记得重新计算子网范围。例如,10.95.0.0/12 实际上属于 10.80.0.0/12 子网。在启动 kube-apiserver 时,会创建与 Service CIDR 第一个主机位相对应的 Service:

$ kubectl describe svc kubernetes
Name:              kubernetes
Namespace:         default
Labels:            component=apiserver
                   provider=kubernetes
Annotations:       <none>
Selector:          <none>
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.96.0.1
IPs:               10.96.0.1
Port:              https  443/TCP
TargetPort:        6443/TCP
Endpoints:         192.168.2.111:6443,192.168.2.112:6443,192.168.2.113:6443
Session Affinity:  None
Events:            <none>

Service 的后端是 Endpoints,它的 IP 地址通常是 Pod 的 IP 或类似于 kubernetes 这样的机器 IP。如果需要将外部 IP 映射到内部的 Service 域名,可以创建一个不指定 selector 的同名 Service 和 Endpoint。需要注意的是,Service 并非唯一的访问方式,许多服务本身也有注册和发现机制,可以直接将 Pod_IP:Port 注册并使用。

2.1、iptables Service 模式

在 iptables 模式下,Kubernetes 的规则可以分为以下几类:

  1. KUBE-SERVICESnat.PREROUTING/nat.OUTPUT):这些规则位于 PREROUTINGOUTPUT 链的最开始,主要包括以下两类:

    • -d SVCIP ... --dport Port -j KUBE-SVC-xxx:将流量目标为 SVC_IP:PORT 的数据包转发到对应的 KUBE-SVC-xxx 链。
    • -m addrtype --dst-type LOCAL:将本地网卡的数据包分派到 KUBE-NODEPORTS 链。
  2. KUBE-NODEPORTS:根据目标端口匹配 NodePort 端口:

    • 数据包进入对应的 KUBE-SVC-xxx 链(当 externalTrafficPolicy=Cluster 时)。
    • 数据包进入 KUBE-XLB-xxx 链(当 externalTrafficPolicy=Local 时)。
  3. KUBE-SVC-xxx:与服务(Service)对应,数据包将随机进入 KUBE-SEP-xxx 链。

  4. KUBE-XLB-xxx:与负载均衡相关的服务,数据包可以进入 KUBE-SEP-xxx 链,或者被丢弃。

  5. KUBE-SEP-xxx:与 endpoint 中的 IP 地址对应,数据包会被 DNAT 转发到对应的 Pod IP。

  6. KUBE-FIREWALLfilter.INPUT/filter.OUTPUT):用于丢弃 0x8000 标记的数据包,通常在 externalTrafficPolicy=Local 配置下使用。

  7. KUBE-MARK-MASQ:标记数据包为 0x4000,表示需要进行 SNAT(源地址转换)。

  8. KUBE-MARK-DROP:标记数据包为 0x8000,表示丢弃该数据包。

  9. KUBE-POSTROUTINGnat.POSTROUTING):对标记为 0x4000 的数据包执行 MASQUERADE(源地址伪装)。

通常,Service 的配置不会出现问题,因此检查 iptables 规则是诊断常见问题的一种有效方法。值得注意的是,SERVICEMARKKUBE-POSTROUTING 等规则在 ipvs 中也有类似的应用。

2.2、实现独立的 IPVS 服务

在 iptables 模式下,随着 Pod 数量的增加,iptables 的规则会呈指数级增加,导致 iptables 匹配复杂度上升。为了应对这种情况,Kubernetes 在 v1.11 版本中引入了 IPVS 模式,极大地优化了负载均衡性能。
许多关于 IPVS 的资料主要集中在如何在 Kubernetes 节点上通过 ipvsadm 创建服务,但很少涉及到相关的核心 iptables 规则。为了更好地理解,我们将在一台干净的机器上实现 IPVS 服务。

2.2.1、 准备环境

为了管理 IPVS 规则,我们需要安装 ipvsadm 工具。这里使用了两台干净的 CentOS 10 作为环境,IP 地址如下:

IP
192.168.2.111
192.168.2.222

首先,安装所需的基础工具:

yum install -y ipvsadm curl wget tcpdump ipset conntrack-tools

# 开启 IP 转发
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
2.2.2、配置思路

由于 Service 端口和 Pod 的端口不同,kube-proxy 使用的是 nat 模式。我们暂时打算添加一个如下的 Service:

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

为了简单测试,我们使用了一个 Golang 编写的简单 Web 服务 ran

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 进行后续操作。

2.2.3、LVS NAT 配置

kube-proxy 没有像 LVS 那样独立设置 NAT 网关,而是将每个节点当做自己的 NAT 网关。接下来,我们在节点上添加 169.254.11.2:80 这个 Service,使用 ipvsadm 配置:

ipvsadm --add-service --tcp-service 169.254.11.2:80 --scheduler rr

首先,我们将本地的 Web 服务作为 Real Server 添加到 IPVS 中,设置为 NAT 类型的 Real Server:

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

查看当前的 IPVS 服务列表:

$ 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 网关,VIP 地址需要绑定到本机的网卡上:

ip addr add 169.254.11.2/32 dev eth0

测试访问:

$ curl 169.254.11.2/www/test
192.168.2.111

接下来,添加另一个节点 192.168.2.222 的 Web 服务:

$ 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

再次测试访问:

$ curl 169.254.11.2/www/test

我们发现 curl 请求在 192.168.2.111192.168.2.222 之间切换,但是并没有返回 192.168.2.222 的内容。检查 IPVS 的连接状态,发现只有调度到本机时才会响应。

2.2.4、SNAT 配置

为了避免访问不通的情况,我们需要让 DNAT 到非本机的包的源 IP 设置为 192.168.2.111,即执行 SNAT 操作。
在这里插入图片描述

POSTROUTING 链上添加 SNAT 规则:

iptables -t nat -A POSTROUTING -m ipvs --vaddr 169.254.11.2 --vport 80 -j MASQUERADE

重新测试访问,结果正常:

$ curl 169.254.11.2/www/test
192.168.2.111
$ curl 169.254.11.2/www/test
192.168.2.222
2.2.5、配置 IPVS 与 Netfilter

IPVS 是基于 Linux Netfilter 框架实现的,所有数据包都会经过 Netfilter 的各个 hook 点处理。为了使 DNAT 和 SNAT 操作能够正确工作,我们需要确保 Netfilter 的状态管理功能也适用于 IPVS 模块。

在这里插入图片描述
上图是 IPVS 在 netfilter 里的模型图,IPVS 也是基于 netfilter 框架的,但只工作在 INPUT 链上,通过注册 ip_vs_in 钩子函数来处理请求。因为 VIP 配置在机器上(常规的 lvs nat 的 VIP 是在 NAT GW 上,这里是自己), curl 的时候就会进到 INPUT 链,ip_vs_in 会匹配然后直接跳转触发 POSTROUTING 链,跳过 iptables 规则。这部分不懂的可以参考尾部链接(LVS原理与实现 - 实现篇)。
这是我们希望的请求流程:

# 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

启用 conntrack 功能:

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
2.2.6、利用 ipset 和 iptables 的 mark 优化

为了减少 iptables 规则的数量并提高匹配效率,我们可以使用 ipsetiptables 的 mark 结合使用。通过在 PREROUTING 阶段匹配 Service 的 IP 和端口,并打上 mark,接着在 POSTROUTING 阶段根据 mark 执行 SNAT 操作。
添加之前先思考下,lvs 做了 DNAT 后,最后包走向了 POSTROUTING 链,而且后面我们是有多个 SVC 的。此刻包的 SRC IP 会是每个对应的 VIP

# 假设没做 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

# 在 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 和 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 阶段处理
iptables -t nat -N ZGZ-SERVICES-POSTROUTING
iptables -t nat -A POSTROUTING -m comment --comment "zgz postrouting rules" -j ZGZ-SERVICES-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 里:

ipset add ZGZ-CLUSTER-IP 169.254.11.2,tcp:80 -exist

上面我们创建的 ipset 里 ip,port 和 iptables 里 --match-set 后面的 dst,dst 组合在一起就是 DEST IPDEST PORT 同时匹配,减少了 iptables 规则的复杂度。下面是一些举例:

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 链:

$ 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
$ cat /var/log/iptables.log
Aug 13 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 
Aug 13 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:

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
2.2.7、使用 keepalived 实现自动化

目前的配置仍然是手动进行的,而且缺乏健康检查功能。我们可以使用 keepalived 来实现自动化配置和健康检查。

安装 keepalived

由于 CentOS 10 默认源中的 keepalived 版本:

yum install -y keepalived
# 备份原有的配置文件
cp /etc/keepalived/keepalived.conf{,.bak}
配置 keepalived

我们需要修改 keepalived 配置,确保可以自动化管理 IPVS 服务并进行健康检查:

$ 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
# 配置项
KEEPALIVED_OPTIONS="-D"

修改 /etc/sysconfig/keepalived 文件:

KEEPALIVED_OPTIONS="-D --log-console --log-detail --use-file=/etc/keepalived/keepalived.conf"

配置文件将包含子配置文件,这样每次添加新的 Service 配置时,只需要通过 kill -HUP 信号重新加载即可。
这里做个说明,主配置文件里去 include 子配置文件,keepalivd 接收 kill -HUP 信号触发 reload,后续自动化添加 SVC 的时候添加子配置文件后发送信号即可。

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/

可以编写个脚本,在每次启动或重启 keepalived 时自动初始化配置,并将 Service 的相关信息添加到 ipset 中。
后续有时间的情况下,再补充下脚本。
脚本思路:
1、添加一个子配置文件里的相关信息到 ipset 里,另一方面也让它在重启或者启动 keepalived 的时候每次能初始化,先添加 systemd 部分。
2、读取 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 的配置文件测试下:

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

参考文档

常见问题

在 Kubernetes 网络配置中,常见的一些问题和解决方案如下:

  • Pod 访问自己的 Service 不通:尤其在一些第三方 Kubernetes 实例中,Pod 访问自己的 Service 会失败。此时需要将 cni0 设置为混杂模式,或者在 kubelet 配置中启用 hairpinMode

  • Service 负载均衡问题:有些应用(如 GRPC,基于 HTTP/2 协议的应用)可能会由于长连接的原因,导致流量无法在每个 Pod 之间均衡分配。此时需要考虑应用层的负载均衡。

  • 安装 conntrack-tools:建议在每个节点安装 conntrack-tools,以便提供 conntrack 二进制命令。kube-proxy 使用它来进行一些内核级操作。

$ yum install conntrack-tools -y
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值