K8s 网络模型:一个包从 Pod 到 Pod 到底走了哪几步
"Service 不通"是 K8s 里最高频的故障,也是最容易查错的——因为大家往往一上来就怀疑 CNI,而真正的原因 90% 在 selector、endpoint 或 conntrack 上。
这篇把链路拆到每一跳,配一张排查顺序表。
结论先说
K8s 网络只有三条硬约束:
- 每个 Pod 一个独立 IP(不需要 NAT 就能被访问);
- 同节点 Pod 之间直连,跨节点 Pod 之间也直连(不经过 NAT);
- 节点上的进程能直接访问所有 Pod(反之亦然)。
满足这三条,其它一切(CNI 插件选型、Service 实现、Ingress)都是在上面叠的壳。而 Service 不是"转发器",它只是 iptables/IPVS 里的一组 DNAT 规则——这句话能解释一半以上的"Service 不通"。
一、Pod 的网络是怎么搭出来的
每个 Pod 里都有一个看不见的容器:pause(或 sandbox)。它唯一的作用是持有网络 namespace。
Pod
├── pause 容器 ← 创建 netns,持有 IP,什么都不干
├── 业务容器 A ┐
└── 业务容器 B ┘ 共享 pause 的 netns,所以共享 IP:port(会冲突!)
创建流程:
1. kubelet 调 CRI,创建 pause 容器
2. 调 CNI 插件(ADD 指令)
3. CNI 插件:
a. 创建 veth pair:一头放进 Pod 的 netns(eth0),一头留在宿主机
b. 给 Pod 的 eth0 分配 IP(来自 PodCIDR)
c. 在宿主机加路由:<pod-ip> via <veth-peer>
d. 配置跨节点路由(取决于插件)
4. Pod 内默认路由指向 169.254.1.1(或宿主机网桥),出流量走宿主机的转发
宿主机上看到的样子:
$ ip link | grep veth
5: veth1a2b3c@if3: <BROADCAST,MULTICAST,UP> mtu 1450 ...
$ ip route
10.244.1.5 dev veth1a2b3c scope host # 本节点 Pod
10.244.2.0/24 via 192.168.1.12 dev eth0 # 跨节点 Pod 网段
注意 MTU 1450——这是 Flannel VXLAN 的典型值(1500 - 50 字节封装开销)。MTU 配错是"能 ping 通但大包卡死"的经典原因。
二、跨节点:三种实现思路
Pod IP 是集群内唯一的,但不同节点的 PodCIDR 不同,所以跨节点必须有机制让"10.244.1.x 能访问 10.244.2.x"。
| 方案 | 代表 | 原理 | 开销 | 适用 |
|---|---|---|---|---|
| Overlay(封装) | Flannel VXLAN | 把原始包封进 UDP,走节点网络 | 每包多 50 字节 + 封装/解封装 CPU | 通用,对底层网络无要求 |
| 三层路由(BGP) | Calico | 节点间用 BGP 交换 Pod 路由 | 无封装,性能最好 | 底层网络可控(同网段/可路由) |
| IPIP | Calico IPIP 模式 | IP-in-IP 封装 | 多 20 字节 | 跨子网但要求不高 |
| eBPF | Cilium | 内核态转发,绕过 iptables | 最低延迟 | 新集群,内核 ≥ 4.19 |
2.1 VXLAN 的一个包发生了什么
Pod A (node1, 10.244.1.5) → Pod B (node2, 10.244.2.7)
① Pod A 发出:src=10.244.1.5 dst=10.244.2.7
② 到 node1 的 veth → 网桥 cni0 → 查路由:10.244.2.0/24 via flannel.1
③ flannel.1(VXLAN 设备)封装:
[外层 IP: src=192.168.1.11 dst=192.168.1.12][UDP 8472][VXLAN 头][原始包]
④ 走物理网卡 eth0 发出
⑤ node2 收到 → 解封装 → 得到原始包 → 按路由投给 10.244.2.7 的 veth
50 字节开销 = 外层 IP(20) + UDP(8) + VXLAN(8) + 内层以太网头(14)。所以 VXLAN 环境下 Pod 的 MTU 必须是 1450,否则大包会被丢弃(表现:小请求正常,大响应超时或 TLS 握手卡住)。
# 检查 MTU 一致性
kubectl exec -it pod -- ip link show eth0 | grep mtu
ip link show flannel.1 | grep mtu
2.2 Calico 为什么快
Calico 不用封装(默认 BGP 模式),直接在节点上写路由:
node1: 10.244.2.0/24 via 192.168.1.12 ← 靠 BGP 学到的
node2: 10.244.1.0/24 via 192.168.1.11
包原样转发,没有额外字节、没有封装解封装。代价是要求节点之间二层/三层可达(同 VPC 内一般没问题,跨 VPC 就要开对等连接或用 IPIP)。
三、Service:iptables 里的一组 DNAT
这是最容易被误解的一环。Service 不是一个进程,没有监听端口,不做转发。
访问 ClusterIP 10.96.0.10:80
↓
① 包到达节点内核的 PREROUTING 链
② 命中 KUBE-SERVICES 链里的规则:
-d 10.96.0.10/32 --dport 80 -j KUBE-SVC-XYZ
③ KUBE-SVC-XYZ 里按概率随机选一个后端:
-m statistic --mode random --probability 0.333 -j KUBE-EP-AAA
-m statistic --mode random --probability 0.500 -j KUBE-EP-BBB
-j KUBE-EP-CCC
④ KUBE-EP-AAA 做 DNAT:
-j DNAT --to-destination 10.244.1.5:8080
⑤ conntrack 记录这条映射,回包自动做 SNAT 还原
关键推论:
| 现象 | 原因 |
|---|---|
| ClusterIP 能 ping 通吗 | 不能。它只是 iptables 规则里的一个虚拟地址,没有实体网卡 |
| 为什么负载均衡是随机的 | iptables 的 statistic 模块按概率分配,不是轮询,且不看后端负载 |
| 为什么规则多了会慢 | iptables 是线性匹配,Service 数 × Endpoint 数增长时规则数 O(n),每次连接都要遍历 |
| 为什么改了 Service 立刻生效 | kube-proxy watch 到变化,重写 iptables 规则 |
3.1 iptables vs IPVS
| | iptables | IPVS |
|---|---|---|
| 数据结构 | 线性链表 | 哈希表 |
| 查找复杂度 | O(n) | O(1) |
| 大规模 Service(>1000) | 规则爆炸,CPU 高 | 稳定 |
| 负载均衡算法 | 随机 | rr / lc / sh / wrr 等 |
| 会话保持 | 靠 conntrack | 原生支持 |
# 查看当前模式
kubectl -n kube-system logs kube-proxy-xxx | grep "Using"
# 或
ipvsadm -Ln
超过 1000 个 Service 的集群,必须用 IPVS。iptables 模式下 kube-proxy 每次同步都要重写几万条规则,会出现周期性 CPU 尖刺。
3.2 Endpoint 是真相
Service 只是个"名字 + 端口",真正决定流量去哪的是 Endpoint(或 EndpointSlice):
kubectl get endpoints my-svc -o wide
NAME ENDPOINTS AGE
my-svc 10.244.1.5:8080,10.244.2.7:8080 5m
ENDPOINTS 为空 = 流量无处可去,这就是最常见的"Service 不通"。
原因几乎总是同一个:selector 没匹配上 Pod。
# 对比 Service 的 selector 和 Pod 的 labels
kubectl get svc my-svc -o jsonpath='{.spec.selector}'
kubectl get pods --show-labels | grep my-app
常见坑:
- Pod 的 label 写了
app: MyApp,selector 写app: myapp(大小写); - Pod 处于
CrashLoopBackOff/NotReady,默认不会进 Endpoint(除非publishNotReadyAddresses: true); - readinessProbe 一直失败 → Pod 是 Running 但不是 Ready → 不进 Endpoint。
3.3 Headless Service
spec:
clusterIP: None # 没有虚拟 IP
DNS 直接返回所有 Pod IP,客户端自己做负载均衡。StatefulSet 必用(每个 Pod 有稳定的 pod-0.svc.ns.svc.cluster.local)。
gRPC 这类长连接客户端必须用 Headless,否则流量会全部落在一个 Pod 上(因为 ClusterIP 的连接建好后,conntrack 会一直指向同一个后端)。
四、Ingress:七层入口
| | Service(L4) | Ingress(L7) |
|---|---|---|
| 依据 | IP + 端口 | 域名 + 路径 + Header |
| 实现 | iptables/IPVS | Nginx / Traefik / Envoy |
| 能力 | 负载均衡 | 路由、TLS 终止、重写、限流、灰度 |
| 是否需要 | 必须 | 可选(但对外暴露几乎必用) |
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
tls:
- hosts: [example.com]
secretName: example-tls
rules:
- host: example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 8080
流量路径:
客户端 → LB(云 SLB) → NodePort/Ingress Controller Pod → 按域名+路径选 Service
→ 直接转发到 Pod IP(Ingress Controller 通常直连 Pod,跳过 ClusterIP)
注意:Nginx Ingress 默认会把 upstream 直接指向 Pod IP(service-upstream: false),所以它其实不走 ClusterIP,这解释了为什么"Service 的 iptables 规则有问题,但 Ingress 访问正常"。
五、DNS:那个让 Java 服务启动慢 30 秒的东西
CoreDNS 是集群 DNS,Service 名解析到 ClusterIP。
但有一个默认配置会带来严重的性能问题:
/etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
ndots:5 的意思是:如果域名里的点数 < 5,就当它是不完整的域名,依次加上 search 后缀去试。
于是解析一个外部域名 api.example.com(2 个点 < 5):
① api.example.com.default.svc.cluster.local → NXDOMAIN
② api.example.com.svc.cluster.local → NXDOMAIN
③ api.example.com.cluster.local → NXDOMAIN
④ api.example.com. → 成功
4 次查询,3 次是浪费,每次都要经过 conntrack 和 CoreDNS。在 Java 里大量 RPC 客户端启动时会解析域名,这个开销会叠加成几十秒。
处理办法:
# 1) 用 FQDN(末尾带点)绕过 search 列表
# jdbc:mysql://mysql.default.svc.cluster.local.:3306/db
# 2) 或者在 Pod 上改 ndots
spec:
dnsConfig:
options:
- name: ndots
value: "2"
另外 conntrack 表满也会导致 DNS 随机超时:
# 看是否有丢包
dmesg | grep "nf_conntrack: table full"
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
六、NetworkPolicy:默认全通,要自己关
K8s 网络默认没有任何隔离——同集群任何 Pod 都能访问任何 Pod。要隔离必须显式写 NetworkPolicy:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-allow-only-gateway
spec:
podSelector:
matchLabels: {app: api}
policyTypes: [Ingress]
ingress:
- from:
- podSelector: {matchLabels: {app: gateway}}
ports:
- protocol: TCP
port: 8080
两个前提:
- CNI 必须支持(Calico、Cilium 支持;Flannel 不支持,写了也不生效);
- NetworkPolicy 是白名单,一旦某个 Pod 被选中且
policyTypes含 Ingress,未列出的流量全部拒绝。
七、五步排查法
按这个顺序查,能覆盖 95% 的问题,不要一上来就看 CNI。
① Pod 本身通不通? → 同节点、跨节点分别 ping
② Service 有没有后端? → kubectl get endpoints
③ ClusterIP 通不通? → 在 Pod 里 curl ClusterIP
④ DNS 通不通? → nslookup svc
⑤ Ingress/外部入口? → curl -H "Host: ..." 节点 IP
对应的命令:
# ① Pod 网络
kubectl exec -it pod-a -- ping -c2 <pod-b-ip>
kubectl exec -it pod-a -- ping -c2 <node-b-ip>
# 不通 → 查 CNI、MTU、防火墙(UDP 8472 / IPIP 协议号 4 / BGP 179)
# ② 后端列表
kubectl get endpointslices -l kubernetes.io/service-name=my-svc -o wide
kubectl describe svc my-svc | grep -A3 Endpoints
# ③ Service 直连
kubectl run tmp --rm -it --image=busybox -- sh
/ # wget -qO- http://my-svc.default.svc.cluster.local:8080/health
# ④ DNS
kubectl exec -it pod-a -- nslookup my-svc.default.svc.cluster.local
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=50
# ⑤ Ingress
kubectl describe ingress web
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller --tail=50
curl -H "Host: example.com" http://<node-ip>/
故障对照表
| 现象 | 最可能原因 |
|---|---|
| 同节点通、跨节点不通 | CNI 隧道未建立 / 防火墙拦了 UDP 8472 / MTU 不匹配 |
| ping 通、HTTP 卡住 | MTU 问题(大包丢失),TLS 握手最容易暴露 |
| ClusterIP 不通但 Pod IP 通 | kube-proxy 未运行 / Service 没 Endpoint / 端口名不匹配 |
| Endpoint 为空 | selector 不匹配 / Pod 未 Ready |
| 偶尔超时 | conntrack 表满 / DNS ndots 导致的解析失败 |
| 一个客户端独占一个 Pod | Service 而非 Headless,长连接被 conntrack 钉住 |
| NetworkPolicy 生效后全断 | 策略写成了默认拒绝,忘了放行 DNS(UDP 53) |
八、小结
K8s 网络看起来复杂,但拆开只有四层:
Pod 网络 每个 Pod 一个 IP,靠 CNI 打通(veth + 路由/隧道)
↓
Service 一组 iptables/IPVS 的 DNAT 规则,按 Endpoint 分发
↓
Ingress 七层路由,按域名/路径选 Service(通常直连 Pod)
↓
NetworkPolicy 白名单式隔离(需要 CNI 支持)
排查时永远从下往上:先确认 Pod 通不通,再确认有没有 Endpoint,最后才怀疑 Service 和 Ingress。
记一句话就够了:"Service 不通"十次里有九次不是网络问题,是 selector 没匹配上。
系列内链
- 前置:[12-Docker与K8s/10-容器本质namespace与cgroup](10-容器本质namespace与cgroup.md)——net namespace 与 veth 的基础
- 关联:[12-Docker与K8s/05-Kubernetes核心对象Pod Deployment Service](05-Kubernetes核心对象Pod%20Deployment%20Service.md)
- 关联:[12-Docker与K8s/08-Kubernetes常见故障排查](08-Kubernetes常见故障排查.md)——Pod 层故障
- 关联:[13-网络与排障/01-TCP三次握手与四次挥手](../13-网络与排障/01-TCP三次握手与四次挥手.md)——conntrack 与连接复用
- 点赞
- 收藏
- 关注作者
评论(0)