K8s 网络模型:一个包从 Pod 到 Pod 到底走了哪几步

举报
yd_254019812 发表于 2026/11/10 10:12:40 2026/11/10
【摘要】 "Service 不通"是 K8s 里最高频的故障,也是最容易查错的——因为大家往往一上来就怀疑 CNI,而真正的原因 90% 在 selector、endpoint 或 conntrack 上。 这篇把链路拆到每一跳,配一张排查顺序表。 结论先说 K8s 网络只有三条硬约束: 每个 Pod 一个独立 IP(不需要 NAT 就能被访问); 同节点 Pod 之间直连,跨节点 Pod 之间也直连...

"Service 不通"是 K8s 里最高频的故障,也是最容易查错的——因为大家往往一上来就怀疑 CNI,而真正的原因 90% 在 selector、endpoint 或 conntrack 上。

这篇把链路拆到每一跳,配一张排查顺序表。

结论先说

K8s 网络只有三条硬约束:

  1. 每个 Pod 一个独立 IP(不需要 NAT 就能被访问);
  2. 同节点 Pod 之间直连,跨节点 Pod 之间也直连(不经过 NAT);
  3. 节点上的进程能直接访问所有 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

两个前提:

  1. CNI 必须支持(Calico、Cilium 支持;Flannel 不支持,写了也不生效);
  2. 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 与连接复用
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。