一、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 服务相关的工作进程和大体工作流程如下:
-
kubelet:负责管理 Pod 的增、删、改操作,并进行健康检查。健康检查方式包括 HTTP、TCP 和 exec,如果探针失败,kubelet 会标记 Pod 为不健康并上报给 kube-apiserver。
-
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 的规则可以分为以下几类:
-
KUBE-SERVICES(
nat.PREROUTING/nat.OUTPUT
):这些规则位于PREROUTING
和OUTPUT
链的最开始,主要包括以下两类:-d SVCIP ... --dport Port -j KUBE-SVC-xxx
:将流量目标为SVC_IP:PORT
的数据包转发到对应的KUBE-SVC-xxx
链。-m addrtype --dst-type LOCAL
:将本地网卡的数据包分派到KUBE-NODEPORTS
链。
-
KUBE-NODEPORTS:根据目标端口匹配
NodePort
端口:- 数据包进入对应的
KUBE-SVC-xxx
链(当externalTrafficPolicy=Cluster
时)。 - 数据包进入
KUBE-XLB-xxx
链(当externalTrafficPolicy=Local
时)。
- 数据包进入对应的
-
KUBE-SVC-xxx:与服务(Service)对应,数据包将随机进入
KUBE-SEP-xxx
链。 -
KUBE-XLB-xxx:与负载均衡相关的服务,数据包可以进入
KUBE-SEP-xxx
链,或者被丢弃。 -
KUBE-SEP-xxx:与 endpoint 中的 IP 地址对应,数据包会被 DNAT 转发到对应的 Pod IP。
-
KUBE-FIREWALL(
filter.INPUT/filter.OUTPUT
):用于丢弃0x8000
标记的数据包,通常在externalTrafficPolicy=Local
配置下使用。 -
KUBE-MARK-MASQ:标记数据包为
0x4000
,表示需要进行 SNAT(源地址转换)。 -
KUBE-MARK-DROP:标记数据包为
0x8000
,表示丢弃该数据包。 -
KUBE-POSTROUTING(
nat.POSTROUTING
):对标记为0x4000
的数据包执行 MASQUERADE(源地址伪装)。
通常,Service 的配置不会出现问题,因此检查 iptables 规则是诊断常见问题的一种有效方法。值得注意的是,SERVICE
和 MARK
、KUBE-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.111
和 192.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 规则的数量并提高匹配效率,我们可以使用 ipset
和 iptables
的 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 IP
和 DEST PORT
同时匹配,减少了 iptables 规则的复杂度。下面是一些举例:
ipset type | iptables match-set | Packet fields |
---|---|---|
hash:net,port,net | src,dst,dst | src IP CIDR address, dst port, dst IP CIDR address |
hash:net,port,net | dst,src,src | dst IP CIDR address, src port, src IP CIDR address |
hash:ip,port,ip | src,dst,dst | src IP address, dst port, dst IP address |
hash:ip,port,ip | dst,src,src | dst IP address, src port, src ip address |
hash:mac | src | src mac address |
hash:mac | dst | dst mac address |
hash:ip,mac | src,src | src IP address, src mac address |
hash:ip,mac | dst,dst | dst IP address, dst mac address |
hash:ip,mac | dst,src | dst 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