目录
Please enable Javascript to view the contents

K8s网络-流量路径全解析(Pod/Service/Ingress/Egress)

 ·  ☕ 14 分钟

系列导航

本系列从 Pod 网络连通入手,逐步展开到 K8s 网络全景。

① 概念 → ② Flannel → ③ Calico → ④ 流量路径 → ⑤ Cilium → ⑥ 对比 → ⑦ 排障 ‖ ⑧ 开发 → ⑨ 多网卡 → ⑩ AI 演进

顺序文章定位
概念与入门基础——主机网络、Docker 网络、CNI 标准、方案分类树
Flannel 详解CNI 实现——Overlay(UDP/VXLAN)+ Underlay(HostGW)与抓包
Calico 详解CNI 实现——Overlay(VXLAN/IPIP)+ Underlay(BGP)+ NetworkPolicy
本篇 - 流量路径全解析全貌——Pod/Service/Ingress/Egress,揭示 CNI 的边界
Cilium 详解(超CNI)超 CNI——eBPF 数据平面(Service + NetworkPolicy)+ CNI 连通(VXLAN/BGP)
插件对比与选型选型——5 插件横向对比 + 决策树
排障思路与常用命令运维——工具链 + 场景排查 + 性能
核心路径 ↑扩展展望 ↓
CNI 插件开发指南扩展——基于 CNI 规范开发自定义插件
多网卡方案详解进阶——Multus + SR-IOV/ipvlan 多网口实战
AI 时代网络演进展望——GPU 网络、eBPF 加速、未来方向

前面几篇讲「网络怎么搭」,本篇换一个视角——从不同通信维度分析流量实际怎么走。以下以 Flannel(网桥模型)+ kube-proxy 为基础,讲清各对象间流量的原理与概念;Calico 的实现差异见文末 §7,Cilium 见下一篇。


0. 前置知识:四表五链与数据包流向

理解流量路径前,先明确数据包在内核网络栈的流向。Netfilter 在 5 个位置挂载 iptables 链,包经过哪些链由「流向」决定,不经过的链不会起作用。

k8s-netfilter-iptables

作用位置处理的流量
PREROUTING入站第一站(路由决策前)所有入站包(DNAT 通常在此)
INPUT路由决策后,目标是本机到达本机的包
FORWARD路由决策后,目标不是本机被转发的包
OUTPUT本机进程产生的包出站包(DNAT 也在此)
POSTROUTING出站最后一站所有出站包(SNAT/MASQUERADE 通常在此)

0.1 四表 × 五链

iptables 按「功能」分表、按「位置」分链,规则就是表内规则挂在链上。四张表各管一类事,挂载的链由各自功能决定:

挂载的链记忆诀窍(内核对数据处理的时间顺序)
rawPREROUTING + OUTPUT「最早干预」:在 conntrack 之前提前「赦免」某些包,让它们不被记录。只出现在包进入系统(PREROUTING)和本机产生(OUTPUT)的最源头
mangle全部 5 条链「全能修改器」:修改包头(TOS、TTL、打标记),无论包走到哪个生命阶段,只要想改就能改,所以 5 条全挂
natPREROUTING / INPUT / OUTPUT / POSTROUTING(唯独缺 FORWARD)「地址转换」:DNAT(改目标)在路由前(PREROUTING)和本机发出前(OUTPUT);SNAT(改源)在离开前(POSTROUTING)。不在 FORWARD 是因为 FORWARD 只做转发不做地址转换,内核不允许在转发路径上修改目标地址(避免路由混乱)
filterINPUT / FORWARD / OUTPUT(唯独缺 PREROUTING / POSTROUTING)「过滤拦截」:收归我的(INPUT)、我转发的(FORWARD)、我发出的(OUTPUT)才需要拦截。PREROUTING 还没决定发给谁,POSTROUTING 已经准备走,这两处不适合做丢弃(DROP)或放行(ACCEPT)

同一链上四表按固定优先级执行:raw → mangle → nat → filter(不是按挂载顺序)。

对本系列的影响:kube-proxy 的 KUBE-* 链就挂在 nat 表(DNAT 在 PREROUTING/OUTPUT,MASQ 在 POSTROUTING),NetworkPolicy 则落在 filter 表(INPUT/FORWARD/OUTPUT)。

四条主要流向:

① 外部来本机(入站):  eth0 → PREROUTING → INPUT → 进程A
② 外部过本机(转发):  eth0 → PREROUTING → FORWARD → POSTROUTING → cni0/eth0
③ 本机去外部(出站):  进程B → OUTPUT → POSTROUTING → eth0 → 外部
④ 本机进程互访(环回):进程A → OUTPUT → POSTROUTING → lo → PREROUTING → INPUT → 进程B

要点:

  • ④ 本机进程互访(如 curl 127.0.0.1:8080、访问本机物理网卡 IP):包从 OUTPUT 发出,内核发现目标就是本机自己,交给 lo 环回接口;lo 把包当作「新收到的包」重新进入入站路径(PREROUTING → INPUT)。整个过程相当于出门绕一圈又从后门进来。
  • lo 只出现在本机进程互访:跨 Pod、跨节点的流量都不经过 lo。
  • Unix Domain Socket(如 /var/run/docker.sock)不经过这套流程:不走 TCP/IP 协议栈,不触发任何 iptables 链。

对本系列的影响(K8s Service):

  • 绝大多数 Service 流量是「①/②」形态:从 Pod 或外部进入,kube-proxy 在 PREROUTING 做 DNAT。
  • 宿主机进程访问 Service(如 kubelet 调 API)是「③」形态:从 OUTPUT 进入,kube-proxy 在 OUTPUT 做 DNAT。
  • 注意:宿主机进程访问 ClusterIP 时,OUTPUT 做 DNAT 后目标是 Pod IP(不是本机地址),包走 cni0/eth0 直接到后端,不绕 lo。只有后端是「同节点 hostNetwork Pod」时,DNAT 后目标 = 本机 IP,才走 lo。
  • 这正是 kube-proxy 把规则同时挂 PREROUTING + OUTPUT 的原因:两类不同来源的流量都要劫持,而不是同一包的两段流程(conntrack 已记录 NAT,不会对同一连接二次 DNAT)。

1. 容器 ↔ 容器(同 Pod 内)

Pod 内所有容器共享同一个 network namespace,共用一个 IP 和 MAC 地址。

Pod (10.244.1.5)
  ├─ 容器A :8080
  └─ 容器B :9090

容器间通过 localhost 直接通信,不经过 CNI、不经过路由:

1
2
# 容器A 中访问容器B
curl http://localhost:9090

这是唯一不走 CNI 的通信路径。Pod 内容器共享 IP,端口不能冲突。

2. Pod ↔ Pod

2.1 同节点 Pod 通信

同节点 Pod 通信的本质:两个 Pod 的 veth pair 都接进同一台宿主机内核,由内核直接转发,流量不出物理网卡。

最经典的数据面结构是网桥模型(源自 ①篇 §3 的 Docker 单机网络):

PodA eth0 ──vethA── cni0 ──vethB── PodB eth0
  1. PodA 发帧:目的 MAC 填 PodB 的 MAC(先 ARP 学习)
  2. 帧经 vethA 进入宿主机,落在 cni0 网桥上
  3. cni0 查 CAM 表(MAC → 端口)→ 命中 vethB 端口
  4. 帧转发给 vethB → PodB eth0

网桥是最常见的数据面,但并非唯一——不同 CNI 的数据面结构并不完全相同,具体差异在后续的 Calico、Cilium 篇展开。这里先以网桥模型讲清同节点转发原理。

2.2 跨节点 Pod 通信

这是 CNI 插件的核心战场。无论具体插件,跨节点 Pod 通信不外乎两种机制(详见 ②篇):

Overlay(封装):在原始 IP 包外再封一层,走物理网络,对端解封后还原

PodA → veth → cni0 → VTEP(flannel.1 / tunl0)封装 → eth0 → 物理网络
  → 对端 eth0 → VTEP 解封装 → cni0 → veth → PodB
  • 好处:对物理网络零要求,只要节点间 IP 可达
  • 代价:封装/解封有开销,MTU 需额外留出封装头

Underlay(直接路由):把 Pod 子网路由直接刷进宿主机路由表,无封装

PodA → veth → cni0 → 匹配远端路由(下一跳 = 目标节点 IP)→ eth0 → 物理网络
  → 对端 eth0 → 匹配本机直连路由 → cni0 → veth → PodB
  • 好处:零封装开销,性能等于裸机
  • 代价:要求节点 L2 直连(下一跳 on-link),不能跨三层路由设备

关键技术点回顾(详见 ①篇):

  1. IPAM 给 Pod 分配唯一 IP
  2. 跨节点路由信息由 CNI 插件维护
  3. 数据包到达机制:封装(Overlay)或直接路由(Underlay)

3. Pod ↔ Service

Service 是 K8S 的抽象层,提供稳定的虚拟 IP 和端口。Pod IP 会变,Service IP 不变。

3.1 Service 的流量路径

先明确 K8s 网络里的分工:iptables 规则由 kube-proxy 承载(管 Service 的虚拟 IP → Pod IP),路由表由 CNI 组件维护在每台节点上(管 Pod IP 的可达性)。两者接力完成 Pod ↔ Service 通信——前者做 DNAT 改写,后者负责把改写后的包送到后端 Pod。

1
2
3
4
客户端 Pod → Service ClusterIP:Port
  → iptables/ipvs 规则(kube-proxy 写入)
  → DNAT:目标 IP = Service IP 改为后端 Pod IP
  → 后续走 Pod ↔ Pod 路径(§2,CNI 维护的路由)

Service 由每台节点上的 kube-proxy 实现。ClusterIP 是虚拟 IP——不绑定任何网卡:

对比载体
Pod IP绑定 Pod 的 eth0(veth 容器端),有 MAC、能 ARP
ClusterIP无接口、无 MAC,只存在于 API Server 字段 + 内核规则

所以 ClusterIP 不做 ARP。Pod 访问它时,按默认路由发到网关(cni0),包进入宿主机栈后,在路由决策之前被 PREROUTING 链 DNAT 改写成后端 Pod IP,之后才走正常的 Pod 路由。没有设备承载这个 IP,它靠内核规则劫持可达。

3.2 DNAT 与 SNAT:什么时候改写源 IP

过 Service 的流量永远做 DNAT(虚拟 IP → Pod IP,包才进得了 Pod 网络)。但不是都做 SNAT——只有回包无法原路返回时才需要:

#场景客户端后端DNATSNAT回包路径
1Pod → ClusterIPPod同节点cni0 桥转发回源 Pod(conntrack un-DNAT)
2Pod → ClusterIPPod跨节点跨节点 Pod 路由回源 Pod
3hairpinPod就是自己源换成 cni0,避免「源=目的=自己」
4宿主机进程 → ClusterIP宿主机任意回宿主机 IP,可达
5外部 → NodePort/LB集群外任意源换成接收节点 IP,回包必须回原节点

一句话判据:要不要 SNAT,只看「回包能不能原路回到做 DNAT 的节点」。 能 → 不 SNAT(靠 conntrack 反 DNAT);不能 → SNAT 强制它回来。具体两种:① 客户端=后端自己(hairpin);② 客户端在集群外。

回包靠 conntrack 自动反向,不需要额外规则——DNAT 记录了「ClusterIP ↔ Pod IP」映射,回包时 conntrack 把源从后端 Pod IP 还原成 ClusterIP(un-DNAT)。

3.3 iptables 规则结构

以 iptables 模式为例,一个 ClusterIP Service(n 个 ready 后端)对应三类链:

1
2
3
KUBE-SERVICES ── ①入口链:匹配 -d ClusterIP:port → 跳转 KUBE-SVC
KUBE-SVC-xxx  ── ②负载均衡链:n 条(n-1 条 statistic 概率 + 1 条兜底)
KUBE-SEP-yyy  ── ③DNAT 链:每后端 2 条(条件标记 + DNAT)
规则数作用
KUBE-SERVICES1匹配 -d ClusterIP:port → 跳转 KUBE-SVC
KUBE-SVC-xxxn负载均衡(statistic 概率)
KUBE-SEP-yyy(×n)2×n①条件打 MASQ 标记 ②DNAT
入口合计3n + 1仅 DNAT 方向

三类链嵌入内核 netfilter 的完整链路:

PREROUTING ── KUBE-SERVICES ────────────────┐(①入口链)
              -d ClusterIP:port -j KUBE-SVC  │ 匹配则跳
                                             ▼
KUBE-SVC-xxx(②负载均衡链,n 条)            │
  -m statistic --probability 0.5 -j KUBE-SEP-a
  -j KUBE-SEP-b                              │(兜底)
                                             ▼
KUBE-SEP-yyy(③DNAT 链,每后端 2 条)        │
  -s <podIP>/32 -j KUBE-MARK-MASQ            │(条件标记)
  -j DNAT --to-destination <podIP:port>      │(真 DNAT)
                                             │
──── 路由决策(目的已改后端 Pod IP)─────────┘
FORWARD(转发到后端)
POSTROUTING ── KUBE-POSTROUTING
  -m mark --mark 0x4000 -j MASQUERADE(识别标记才 SNAT)

入口链的分类:PREROUTING vs OUTPUT

KUBE-SERVICES 同时挂在两条链,取决于「客户端是谁」——内核 netfilter 对「转发包」和「本地产生的包」走不同的钩子:

客户端入口链后续路径本质
Pod / 其他节点转发来的流量PREROUTINGFORWARD → POSTROUTING转发路径,包「路过」本机
宿主机自身进程OUTPUTPOSTROUTING本地路径,包「发自」本机
① Pod 客户端(转发路径):
  Pod → veth → cni0
  → PREROUTING:KUBE-SERVICES → KUBE-SVC → KUBE-SEP(标记 + DNAT)
  → 路由决策 → FORWARD → 后端
  → POSTROUTING:识别标记 MASQUERADE

② 宿主机进程(本地路径):
  进程 → OUTPUT:KUBE-SERVICES → KUBE-SVC → KUBE-SEP(标记 + DNAT)
  → 路由决策(DNAT 后重新路由)
  → POSTROUTING:识别标记 MASQUERADE

区别:转发包先过 PREROUTING 再过 FORWARD;本地产生的包不过 PREROUTING/FORWARD,从 OUTPUT 直接进入。kube-proxy 要把 KUBE-SERVICES 同时挂在这两个钩子上,才能同时劫持「Pod 访问」和「宿主机进程访问」两类流量(如 kubelet 访问某 Service)。

出口方向(回包)几乎不新增规则——靠 conntrack 状态表自动反向,只有全局共享的 MASQ 规则:

「标记」和「MASQUERADE」是两回事。标记 = 给包贴 skb->mark 里的 0x4000 位(「待会要 SNAT」),真正的 SNAT 由 POSTROUTING 里 -m mark --mark 0x4000 -j MASQUERADE 执行。

分两步的根因:DNAT 在 PREROUTING(路由决策前),SNAT 必须在 POSTROUTING(路由后,MASQUERADE 按出接口选源 IP)。标记就是这两个阶段之间的「传话」——PREROUTING 贴标签,POSTROUTING 凭标签执行。

KUBE-SEP 里的标记规则带 -s <podIP>/32 条件:只有「源就是这个后端 Pod 自己」(hairpin)才打标记。普通内部访问不打标记、不做 SNAT。

3.4 iptables 模式 vs IPVS 模式

维度iptablesIPVS
转发机制Netfilter NAT 规则链Linux 内核 L4 负载均衡
负载均衡算法随机(statistic 模块,probability)多种:rr、lc、dh、sh 等
性能规则数 >1000 时性能下降明显哈希表查找,O(1),大规模规则性能好
内核态切换需要经过 Netfilter 钩子在内核空间直接处理
默认模式K8S < 1.11 默认K8S ≥ 1.11 默认(需加载 ip_vs 内核模块)

建议: 生产环境优先用 IPVS。大规模集群(>1000 Service)iptables 规则数量级增长会明显影响延迟。

3.5 流量路径示意

客户端 Pod                  kube-proxy                      后端 Pod
    │                          │                                │
    │ 1. 目标 IP = Svc:ClusterIP│                                │
    │                          │                                │
    │ 2. 内核匹配 iptables/ipvs│                                │
    │    规则:DNAT 到后端 Pod IP │                                │
    │                          │                                │
    │ 3. 目标 IP 改写为 Pod IP  │                                │
    │ ────────────────────────────────────────────────────────► │
    │                          │                                │
    │ 4. 走 CNI Pod ↔ Pod 路径 │                                │
    │                          │                                │
    │ 5. 回复包经 conntrack     │                                │
    │    un-DNAT 还原源为 ClusterIP│                             │
    │ ◄──────────────────────────────────────────────────────── │

4. 集群外 → Service(外部入站流量)

4.1 NodePort

在每个节点上开放指定端口(30000-32767),外部流量到达节点端口后转发给 Service:

1
外部客户端 → node1:30001 → DNAT + SNAT → Service → 后端 Pod

外部流量必须做 SNAT(MASQUERADE):TCP 是 4 元组,客户端只认它连的 node1:30001,而 DNAT 在 node1 做。不 SNAT,后端 Pod 会直接把回包发给外部客户端,源 IP 变成后端 egress,4 元组对不上被丢弃。SNAT 把源改成 node1 的 IP,强制回包回到 node1 做 un-DNAT,客户端才认。

注:跟后端 Pod 在不在 node1 无关——即使后端就在本节点也要 SNAT(见 §3.2 第 5 类)。

不推荐直接用于生产——端口管理不便、无法做 TLS 终结、缺少高级负载均衡能力。

4.2 LoadBalancer

云环境(AWS、GCP、Azure)或私有方案(MetalLB)为 Service 分配外部 IP:

1
外部客户端 → LoadBalancer IP:Port → 后端节点 NodePort → Service → Pod

4.3 Ingress

L7 反向代理,通过域名和路径路由到不同 Service:

1
2
3
4
5
外部请求 → Ingress Controller (nginx/traefik)
  → 根据 Host/Path 规则
  → 匹配的 Service:Port
  → iptables/ipvs DNAT
  → 后端 Pod

Ingress 典型配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
spec:
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /users
        pathType: Prefix
        backend:
          service:
            name: user-service
            port:
              number: 8080

4.4 ServiceType 总览

ServiceType外部可达典型场景实现方式
ClusterIP集群内互通默认类型,仅分配内网 IP
NodePort开发测试每节点开端口,直连节点 IP:Port
LoadBalancer生产环境云 LB 或 MetalLB 分配公网 IP
ExternalName逻辑可达DNS CNAME返回 CNAME 记录,无代理

5. Pod → 集群外(Egress 出站流量)

Pod 访问外网时,源 IP 需要被替换为宿主机 IP(SNAT):

1
2
3
Pod eth0 (10.244.1.5) → veth → cni0
  → 宿主机 iptables SNAT(源 IP 改为宿主机 IP)
  → 宿主机 eth0 → 外网

关键规则(iptables 视角):

1
iptables -t nat -A POSTROUTING -s 10.244.0.0/16 ! -o cni0 -j MASQUERADE
  • 源地址来自 Pod 子网,出接口不是 cni0,就执行 SNAT
  • 回包到达宿主机时,内核根据连接跟踪表(conntrack)做反向 DNAT,送回 Pod

适用场景: 大部分 K8S 集群默认配好 SNAT。如果 Pod 直接使用宿主机网络(hostNetwork: true),则 Pod 的源 IP 就是宿主机 IP,不需要 SNAT。

6. 服务发现

Pod 需要知道要访问哪个 Service,K8S 提供两种机制:

6.1 DNS(推荐)

CoreDNS(或 kube-dns)作为集群内 DNS 服务,watch API Server 中的 Service 变化,自动创建 DNS 记录:

1
2
3
4
Pod → DNS 查询: my-service.my-ns.svc.cluster.local
  → CoreDNS 返回 Service ClusterIP
  → Pod 向 ClusterIP 发起请求
  → kube-proxy iptables/ipvs 把流量 DNAT 到后端 Pod

DNS 记录命名格式:

1
service-name.namespace.svc.cluster.local

6.2 环境变量

Kubelet 在 Pod 启动前,为每个已存在的 Service 注入环境变量:

1
2
REDIS_SERVICE_HOST=10.96.100.5
REDIS_SERVICE_PORT=6379

限制: Service 必须在 Pod 之前创建才会注入。Pod 启动后新增的 Service,已有 Pod 不会获得环境变量。因此生产环境依赖 DNS。


7. Calico 的实现差异

前面各节用 Flannel 的网桥模型讲原理。Calico 是另一种主流数据面,核心区别在没有网桥

数据面结构: Calico 的 veth 对端(cali*)直接进宿主机网络栈,每个 Pod 一条 /32 路由(10.244.1.5/32 dev caliXXX scope link),不经过网桥。

同节点 Pod 通信: 不再走网桥 CAM,而是内核按路由表在 veth 之间做 L3 转发——PodA → caliA → 路由表匹配 PodB 的 /32 → caliB → PodB。

跨节点 Pod 通信: 两种模式

  • BGP(Underlay):把 /32 路由通过 BGP 广播给其他节点,无封装,性能高
  • IPIP/VXLAN(Overlay):底层网络不支持 BGP 时退回封装

Service / NetworkPolicy: Service 默认仍走 kube-proxy(iptables/ipvs)。Calico 的核心价值是 NetworkPolicy——Felix 组件把策略翻译成 iptables/ipset 规则下发到每个节点。

一句话:Flannel 靠网桥(L2),Calico 靠路由(L3)。同节点 Calico 也走路由、没有 CAM 转发,这是两者数据面最本质的差异。封装与 BGP 细节见 ③篇。


参考链接

分享

Hex
作者
Hex
CloudNative Developer