一、先别急着讲原理,让我们先聊聊”为什么网络这么难”
说实话,Kubernetes 网络模型是容器化世界里最容易被低估,也最容易让人”暴走”的部分。你想想看,以前我们在传统服务器时代,两台机器通信就是 IP 对 IP,直连或者通过路由器,简单粗暴。但现在呢?Pod 的 IP 是临时的,死了就没了;Service 是个抽象概念,没有物理实体;还有网络插件、CNI、iptables、ipvs 各种名词叠在一起,稍微有点风吹草动,排查起来简直就是大海捞针。
我见过太多工程师,遇到网络问题就是 curl 不通,然后开始盲目重启 Pod、重装 CNI 插件,最后发现根本不是什么大问题,只是对 K8s 网络模型的理解有偏差。
所以今天,我们不搞那种干巴巴的教科书式讲解,咱们就像剥洋葱一样,一层一层地把 K8s 网络从 Pod 到 Service 的通信原理给捋清楚,顺便穿插几个我亲身踩过的”血泪”案例,保证你看完不仅懂原理,还能真正会排查问题。
二、K8s 网络的”四大金律”:先建立基本认知
在深入技术细节之前,你必须记住 K8s 网络模型的四个核心设计原则。这是理解一切的基础,不懂这四点,后面全都会乱。
2.1 每个 Pod 都有独立的 IP
不管你的集群里跑着多少个 Pod,每个 Pod 都能拿到一个独立的、全局唯一的 IP 地址。这是什么概念呢?就像每个房间(Pod)都有自己的门牌号(IP),不管这个房间在哪个楼(Node)里,其他房间都能直接找到它。
# 看看你集群里 Pod 的 IP 分布
kubectl get pods -o wide
NAME READY STATUS IP NODE
nginx-7f9c6b8b5d-abc12 1/1 Running 10.244.1.5 node-1
nginx-7f9c6b8b5d-def34 1/1 Running 10.244.2.8 node-2
redis-5d8f7c6b4a-ghi56 1/1 Running 10.244.1.9 node-1
你看,nginx-abc12 和 nginx-def34 虽然在不同的 Node 上,但它们的 IP 都在同一个网段(10.244.x.x),这就是 Pod 网络的”扁平化”设计。
2.2 Pod 之间可以直接通信
不需要 NAT,不需要端口映射,Pod A 直接 curl Pod B 的 IP 就能通信。这听起来很简单,但背后的实现可没那么 trivial。
为什么说不需要 NAT?因为 K8s 的设计理念是:网络应该是透明的。你部署的应用,应该感觉不到自己跑在容器里,更感觉不到自己跑在集群里。对应用来说,它以为自己还在一台物理机上,和其他应用直接通信。
2.3 Service 提供稳定的访问入口
Pod 的 IP 是易变的,今天这个 Pod 挂了,K8s 会重启它,IP 可能就变了。如果业务直接依赖 Pod IP,那维护成本太高了。所以 K8s 引入了 Service,给一组 Pod 提供一个固定的 VIP(Virtual IP)。
apiVersion: v1
kind: Service
metadata:
name: my-nginx
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
这个 my-nginx Service 会分配一个 ClusterIP(比如 10.96.0.10),这个 IP 在整个集群生命周期内都不会变。不管后面的 nginx Pod 怎么换 IP,前端访问 10.96.0.10:80 就行。
2.4 每个 Pod 都能访问所有 Node
不管 Pod 在哪个 Node 上,它都能访问集群内其他 Node 上的任何 Pod。这实现了真正的”平面网络”(Flat Network)。
好,记住这四条。接下来,咱们看看这些原则在底层是怎么实现的。
三、Pod 网络是怎么建起来的?CNI 和网络插件的秘密
3.1 每个 Pod 都有虚拟网卡
当你创建一个 Pod 时,K8s 会调度它到某个 Node 上运行。一旦调度成功,Kubelet 就会调用 CNI(Container Network Interface)插件,给这个 Pod 创建一个网络命名空间,并插入一对虚拟网卡(veth pair)。
+---------------------+ +---------------------+
| Pod A | | Pod B |
| +---------------+ | | +---------------+ |
| | Container | | | | Container | |
| | eth0:10.244..| | | | eth0:10.244..| |
| +-------+-------+ | | +-------+-------+ |
| | | | | |
| veth pair | | veth pair |
| | | | | |
| +-------v-------+ | | +-------v-------+ |
| | Bridge | | | | Bridge | |
| | cni0 / flannel| | | | cni0 / flannel| |
| +---------------+ | | +---------------+ |
+---------------------+ +---------------------+
Node-1 Node-2
每个 Pod 的 eth0 网卡是一对 veth pair 的一端,另一端插在 Node 上的网桥(比如 cni0)里。这样,Pod 内部的流量就能通过 veth pair 到达 Node 的网络栈。
3.2 跨 Node 的通信:隧道 vs 路由
现在问题来了:Pod A 在 Node-1,Pod B 在 Node-2,它们怎么通信?
不同的网络插件(CNI)有不同的实现方式,最常见的是 Flannel 和 Calico。
Flannel 的 VXLAN 隧道模式
Flannel 默认使用 VXLAN(Virtual Extensible LAN)技术。它在每个 Node 上创建一个 flannel.1 虚拟网卡,把所有 Node 的 Pod 网络”桥接”在一起。
# 在 Node-1 上查看
ip addr show flannel.1
23: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP
link/ether 12:34:56:78:9a:bc brd ff:ff:ff:ff:ff:ff
inet 10.244.0.1/32 scope global flannel.1
valid_lft forever preferred_lft forever
inet6 fe80::1034:56ff:fe78:9abc/64 scope link
valid_lft forever preferred_lft forever
当 Pod A(10.244.1.5)要访问 Pod B(10.244.2.8)时:
- Pod A 的流量从
eth0出来,到达cni0网桥 - Node-1 的内核路由表发现目标 IP 10.244.2.8 属于 flannel 子网,于是把包发给
flannel.1 flannel.1对数据包进行 VXLAN 封装,加上外层 UDP 头部- 数据包通过物理网络(Node 之间的 Ethernet)发送到 Node-2
- Node-2 收到包后,解封装 VXLAN,把原始数据包交给
flannel.1 - 最终到达 Pod B
# 简化的 VXLAN 封装过程(伪代码)
original_packet = {
"src_ip": "10.244.1.5",
"dst_ip": "10.244.2.8",
"src_port": 12345,
"dst_port": 80,
"payload": "GET / HTTP/1.1..."
}
vxlan_packet = {
"src_ip": "192.168.1.10", # Node-1 的物理 IP
"dst_ip": "192.168.1.20", # Node-2 的物理 IP
"src_port": 4789, # VXLAN 端口
"dst_port": 4789,
"vni": 1, # VXLAN Network Identifier
"inner_packet": original_packet
}
Calico 的 BGP 路由模式
Calico 则完全不同,它不使用隧道,而是利用 BGP(Border Gateway Protocol)把 Pod 的路由信息分发到所有 Node。
# 在 Node-1 上查看路由表
ip route
# ... 其他路由 ...
10.244.2.0/24 via 192.168.1.20 dev eth0 # Node-2 的物理 IP
10.244.3.0/24 via 192.168.1.30 dev eth0 # Node-3 的物理 IP
这样,Node-1 知道如何直接路由到 Node-2 和 Node-3 的 Pod 网段,无需封装解封装,性能更好。
3.3 kube-proxy:Service 流量的”交通警察”
现在 Pod 之间能通信了,但 Service 还没讲到。这就是 kube-proxy 的舞台。
kube-proxy 是运行在每个 Node 上的网络代理,它的任务是:把对 Service ClusterIP 的访问,转发到后端的 Pod IP 上。
它有三种工作模式:
| 模式 | 实现方式 | 性能 | 特点 |
|---|---|---|---|
| userspace | 内核态用户态切换 | 低 | 最古老,已基本淘汰 |
| iptables | 内核 Netfilter 表 | 中 | 默认模式,规则数量线性增长 |
| ipvs | 内核 Netfilter hash 表 | 高 | 性能好,支持更多负载均衡算法 |
iptables 模式的秘密
当 Service 被创建时,kube-proxy 会在每个 Node 上生成一组 iptables 规则:
# 查看 kube-proxy 生成的 iptables 规则
iptables -t nat -L -n -v | grep my-nginx
Chain KUBE-SERVICES (2 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-SVC-XXXX tcp -- * * 0.0.0.0/0 10.96.0.10 /* default/my-nginx cluster IP */ tcp dpt:80
Chain KUBE-SVC-XXXX (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-SEP-AAAA all -- * * 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.33333
0 0 KUBE-SEP-BBBB all -- * * 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.50000
0 0 KUBE-SEP-CCCC all -- * * 0.0.0.0/0 0.0.0.0/0
Chain KUBE-SEP-AAAA (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 10.244.1.5 0.0.0.0/0
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp to:10.244.1.5:80
可以看到,访问 10.96.0.10:80 的流量,会被 kube-proxy 随机转发到后端的三个 Pod IP(10.244.1.5、10.244.1.6、10.244.1.7)之一。
注意:KUBE-MARK-MASQ 这条规则很重要!它会给从 Pod 出来的流量打上 MASQ(Masquerade)标记,触发 SNAT(源地址转换)。这是为了保证回程流量能正确返回——如果没有 SNAT,Pod 收到来自 Service VIP 的回复,可能会因为路由问题而丢弃。
ipvs 模式的进化
ipvs 本质上也是用内核的 Netfilter,但使用了 hash 表而不是链式规则,所以在后端 Pod 数量多时,性能更好。
# 查看 ipvs 规则
ipvsadm -L -n
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.96.0.10:80 rr
-> 10.244.1.5:80 Masq 1 0 0
-> 10.244.1.6:80 Masq 1 0 0
-> 10.244.1.7:80 Masq 1 0 0
四、从 Pod 到 Service 的完整通信路径
现在我们把所有组件串起来,看看一个完整的请求是怎么走的。
场景:Pod A 访问 Service my-nginx
假设:
- Pod A IP:10.244.1.5(在 Node-1)
- Service my-nginx ClusterIP:10.96.0.10:80
- 后端 Pod:10.244.2.8(Node-2)、10.244.3.9(Node-3)
第一步:Pod A 发起请求
# 在 Pod A 内部执行
curl http://10.96.0.10
Pod A 的流量从 eth0 出来,到达 cni0 网桥,然后进入 Node-1 的内核网络栈。
第二步:内核路由决策
Node-1 的内核检查路由表:
# Node-1 上的路由表
ip route
default via 192.168.1.1 dev eth0
10.244.0.0/16 dev cni0 proto kernel scope link src 10.244.1.1
10.244.2.0/24 via 192.168.1.20 dev eth0 # 如果走 Calico BGP
10.96.0.0/16 dev cni0 proto kernel scope link src 10.244.1.1 # ClusterIP 网段
关键来了:10.96.0.10 属于 ClusterIP 网段,内核会把这个包拦截下来,交给 iptables/ipvs 处理。这就是为什么 Service VIP 不能在 Pod 内部直接 ping 通(除非经过 kube-proxy 的规则)。
第三步:kube-proxy 拦截并转发
iptables 规则匹配到 KUBE-SERVICES 链,将包转发到 KUBE-SVC-XXXX,然后随机选择一个后端 Pod(比如 10.244.2.8)。
如果是 ipvs 模式,内核的 Netfilter 会直接通过 hash 算法选择后端。
第四步:SNAT 和转发
流量被 DNAT(目的地址转换)为 10.244.2.8:80,同时因为 KUBE-MARK-MASQ,还会进行 SNAT,源地址变为 Node-1 的 IP(192.168.1.10)。
Pod A (10.244.1.5) --> Node-1 cni0 --> kube-proxy iptables --> SNAT --> Node-1 eth0 --> 物理网络 --> Node-2 eth0 --> flannel/cni --> Pod B (10.244.2.8)
第五步:回程流量
Pod B 收到请求后,回复给 SNAT 后的源地址(192.168.1.10:XXXX)。这个包回到 Node-1,Node-1 的内核做反向 NAT,把源地址改回 10.244.2.8,目的地址改回 10.244.1.5,然后转发给 Pod A。
五、真实故障排查案例:那些年我踩过的坑
原理讲完了,接下来是重头戏——真实案例。这些都是我或者我朋友在实际生产中遇到的”疑难杂症”,希望能帮你少走弯路。
案例一:Pod 能 ping 通 Service VIP,但 curl 不通
现象描述:
”`bash
在 Pod 内部
$ ping 10.96.0.10 PING 10.96.0.10 (10.9
