文章目录
-
- [Kubernetes 网络模型的核心原则](#Kubernetes 网络模型的核心原则)
- [CNI 插件体系:K8s 网络的真正实现者](#CNI 插件体系:K8s 网络的真正实现者)
-
- [CNI 的工作位置](#CNI 的工作位置)
- [CNI 插件的生命周期](#CNI 插件的生命周期)
- [同节点 Pod 通信:Veth Pair 与网桥](#同节点 Pod 通信:Veth Pair 与网桥)
-
- [Veth Pair 的工作原理](#Veth Pair 的工作原理)
- 网桥在同节点通信中的作用
- [跨节点 Pod 通信:路由与 Overlay 两种路径](#跨节点 Pod 通信:路由与 Overlay 两种路径)
-
- 路由模式:直接发送,无需封装
- [Overlay 模式:VXLAN 封装穿越复杂网络](#Overlay 模式:VXLAN 封装穿越复杂网络)
- [VXLAN 封装的完整路径](#VXLAN 封装的完整路径)
- [Flannel vs Calico:架构权衡](#Flannel vs Calico:架构权衡)
- [HostPort 与 ContainerPort:两个端口的本质区别](#HostPort 与 ContainerPort:两个端口的本质区别)
- [生产问题:CNI 插件版本不兼容导致的网络异常](#生产问题:CNI 插件版本不兼容导致的网络异常)
- [DNS 与 Service 发现:网络之上的名字解析](#DNS 与 Service 发现:网络之上的名字解析)
- 总结
前置知识:本文需要读者对 Linux 网络基础有初步了解,包括 IP 地址、子网掩码、路由表和 VLAN 的基本概念。若对 Docker 网络模式尚不熟悉,建议先阅读本系列前面的文章,或参考"网络与安全系列"的 Cilium 相关文章。
Kubernetes 网络模型的核心原则
Kubernetes 官方文档定义了网络实现的四个必须解决的问题:Pod 之间的通信、Pod 与 Service 之间的通信、Service 对外部的暴露,以及外部对 Service 的访问。前两个问题的答案完全取决于 CNI 插件的实现,后两个问题则由 Kubernetes 的 Service 机制和 Ingress Controller 处理。
理解 K8s 网络模型的关键在于:Kubernetes 本身并不实现网络,它只定义了一套规则。具体怎么让 Pod 能互相通信,取决于部署时选择了哪个 CNI 插件。CNI 插件负责 IP 地址分配、Veth Pair 创建、桥接和路由规则生成------所有这些工作都在 Pod 创建时由 CNI 插件完成。
这套模型遵循三个核心原则:
原则一:Pod 之间通信无需 NAT。同一 Pod 内的多个容器共享同一个网络命名空间,通过 localhost 通信。跨 Pod 通信时,源 Pod 看到的目标 Pod IP,就是目标 Pod 实际的 IP 地址,网络地址转换(NAT)不会介入这个过程。
原则二:Node 与 Pod 之间通信无需 NAT。节点上的进程(包括 kubelet、容器运行时、以及宿主机上的其他进程)访问 Pod IP 时,不需要经过 NAT 转换。这个原则使得节点可以直接通过 Pod IP 与 Pod 通信,是很多网络调试工具的基础。
原则三:Pod 看到的自己的 IP 与外部看到的 IP 一致。Pod 内部的 eth0 接口 IP 地址,同时也是外部网络看到的该 Pod 的地址。这意味着从 Pod 内部获取自己的 IP 地址后直接对外通信,外部看到的源地址就是这个 IP------而不是经过 NAT 转换后的某个随机地址。
CNI 插件体系:K8s 网络的真正实现者
CNI 的工作位置
当 kubelet 需要创建一个 Pod 时,它并不会直接操作网络命名空间,而是调用容器运行时(containerd 或 CRI-O),再由容器运行时调用 CNI 插件。整个调用链如下:
kubelet
(--network-plugin=cni
--cni-conf-dir=/etc/cni/net.d)
容器运行时
(containerd/cri-o)
CNI 插件
(调用可执行文件)
CNI 配置文件
(/etc/cni/net.d/*.conflist)
CNI 二进制文件
(/opt/cni/bin/*)
网络命名空间
(创建 veth0、eth0)
Pod 网络命名空间
根网络命名空间
(vethXXX + 路由规则)
kubelet 通过 CNI_CONF_DIR(默认 /etc/cni/net.d)读取插件配置,按文件名字母顺序找到第一个有效的配置文件,然后调用对应的 CNI 二进制文件完成网络设置。这个顺序很重要:如果部署了两个 CNI 插件,后加载的会覆盖前一个的配置,导致网络行为不可预期。因此一个集群通常只部署一个 CNI 插件。
CNI 插件的生命周期
CNI 插件对每个 Pod 的操作分为四个阶段:ADD(ADD) 、CHECK(检查) 、DELETE(删除) 、VERSION(版本查询)。
ADD 阶段最为关键,插件需要完成以下工作:为 Pod 命名空间创建 Veth Pair,将宿主端的 Veth 接入网桥或路由表,从 CNI 插件管理的 IP 地址池中为 Pod 分配 IP 地址,并配置必要的网络策略规则。
DELETE 阶段负责清理:删除 Veth Pair、释放 IP 地址、移除路由规则。理解这两个阶段对于排查 Pod 创建或删除时的网络问题至关重要------很多网络故障的根因就是 DELETE 阶段的清理逻辑没有正确执行。
同节点 Pod 通信:Veth Pair 与网桥
Veth Pair 的工作原理
在讨论跨节点通信之前,需要先理解同节点 Pod 通信的完整路径,因为这是理解更复杂场景的基础。
Veth Pair(Virtual Ethernet Pair)是 Linux 的成对虚拟网络设备。数据从一端进入后,必然从另一端出现------就像一根网线的两端,只是这根"网线"是纯软件实现的。在 Kubernetes 中,每个 Pod 的网络命名空间中都有一个 eth0 接口,这实际上就是 Veth Pair 的一端;另一端在宿主机的根网络命名空间中,通常命名为 vethxxxxxx。
宿主机根网络命名空间
Pod A 网络命名空间
Veth Pair
eth0
10.244.0.10
veth0a3c2b
(无 IP)
cni0 网桥
(管理 10.244.0.0/24)
eth0
192.168.1.100
需要特别注意:宿主机端的 Veth 设备本身不配置 IP 地址。它的作用仅仅是将 Pod 命名空间与宿主机根网络命名空间连接起来。真正的 IP 地址分配发生在 Pod 侧的 eth0 上。
网桥在同节点通信中的作用
同节点 Pod 之间的通信依赖 Linux 网桥(Bridge)。当 Pod A(10.244.0.10)向 Pod B(10.244.0.11)发送数据包时,宿主机上的路由决策如下:
- Pod A 的 eth0 发送数据包,源 MAC 为 Pod A,目标 MAC 为 Pod B
- 数据包通过 Veth Pair 到达宿主机端的 veth0a3c2b
- 宿主机检查路由表,发现 10.244.0.0/24 目标网络是直连网络
- 网桥 cni0 根据目标 MAC 地址查找转发表(CAM 表),找到 Pod B 对应的 Veth 端口
- 数据包从网桥的对应端口发出,进入 Pod B 的命名空间
整个过程发生在数据链路层(Layer 2),不需要经过 iptables 或 IP 路由,所以延迟极低。这是 Kubernetes 同节点 Pod 通信能够保持低延迟的技术基础。
跨节点 Pod 通信:路由与 Overlay 两种路径
跨节点通信是 K8s 网络中最复杂的部分。数据包从源节点 Pod 发出,到达目标节点 Pod,需要跨越节点之间的物理网络。这个过程有两种实现思路:纯路由(Underlay)和Overlay(覆盖网络)。
路由模式:直接发送,无需封装
在路由模式下,源 Pod 发送的数据包目标 IP 是目标 Pod 的 IP(例如 10.244.1.10),源节点通过路由表将数据包直接发送到目标节点,不需要额外的封装。
这种模式的关键前提是:每个节点的 Pod CIDR 必须对所有节点可见 。也就是说,节点之间的路由器必须知道 10.244.0.0/24 属于节点 A、10.244.1.0/24 属于节点 B,才能正确路由数据包。
Flannel 的 host-gw 后端和 Calico 的 BGP 模式都采用这种方案。以 Calico BGP 模式为例:每个节点上运行一个 Bird(BGP 守护进程),节点之间建立 BGP 对等连接,相互宣告自己负责的 Pod CIDR 路由。节点 A 的路由表中会直接包含节点 B 的 Pod CIDR 条目,下一跳是节点 B 的 IP 地址。
路由模式的优势 是性能最优:数据包直接由物理网络转发,不需要额外的封装/解封装开销。劣势是对网络环境有要求------节点之间必须是二层可达(即可以直接 ARP),或者节点 Pod CIDR 对整个物理网络可见,这在云环境和跨公网的场景中通常无法满足。
Overlay 模式:VXLAN 封装穿越复杂网络
当节点之间无法直接路由时,Overlay 模式就成了唯一的可行方案。Overlay 的核心思想是:在现有物理网络之上,构建一个虚拟的网络层。Pod IP 只在这个虚拟网络层中有意义,跨节点传输时需要将整个数据包封装到物理网络可路由的外层包中。
VXLAN(Virtual Extensible LAN)是当前最主流的 Overlay 协议。理解 VXLAN 的封装结构是理解跨节点 Pod 通信的前提:
内层(Inner,Pod 网络层)
外层(Outer,物理网络层)
外层 IP Header
Src: 192.168.1.10(节点A)
Dst: 192.168.1.20(节点B)
UDP Header
Src Port: 随机
Dst Port: 4789(VXLAN)
VXLAN Header
VNI: 1(24位虚拟网络标识符)
GBP: 可选
内层 IP Header
Src: 10.244.0.10(Pod A)
Dst: 10.244.1.10(Pod B)
TCP/UDP Header
外层 IP Header 使用的是节点的真实 IP(节点 A 和节点 B 的物理网络地址),物理网络路由器根据这个地址将数据包从节点 A 送到节点 B。VXLAN Header 中的 VNI(VXLAN Network Identifier)是 24 位的标识符,最多支持 1600 万个相互隔离的虚拟网络------这解决了 Kubernetes 多租户或大规模集群的网络隔离问题。
VXLAN 封装的完整路径
理解一个具体的 VXLAN 封装场景,有助于彻底掌握这个机制。假设节点 A 上的 Pod A(10.244.0.10)向节点 B 上的 Pod B(10.244.1.10)发送数据包,完整路径如下:
Pod B 10.244.1.10 VTEP(节点B) eth0(节点B) eth0(节点A) VTEP flannel.1 / cali0 cni0 网桥 veth0a3c2b Pod A 10.244.0.10 Pod B 10.244.1.10 VTEP(节点B) eth0(节点B) eth0(节点A) VTEP flannel.1 / cali0 cni0 网桥 veth0a3c2b Pod A 10.244.0.10 VTEP 查询本地 FDB(转发表) 找到 10.244.1.10 对应的 VNI 和节点B IP 发送数据包 Dst: 10.244.1.10 数据包进入网桥 网桥查询路由 10.244.1.10 不在本地直连网络 VXLAN 封装 外层Dst: 192.168.1.20(节点B) VNI: 1 物理网络路由 节点A → 节点B 接收 UDP 4789 数据包 识别为 VXLAN 封包 VXLAN 解封装 提取内层数据包 VNI: 1 校验通过 内层数据包 Dst: 10.244.1.10 网桥查询 MAC 表 找到 Pod B 对应端口 数据包到达 Pod B
这里有一个值得深入的点:VTEP(VXLAN Tunnel Endpoints)的 FDB(转发表)是如何填充的? 在 Flannel 中,flanneld 守护进程负责维护各节点 Pod CIDR 与节点 IP 的映射关系,并将这些信息写入内核的 FDB 表。在 Calico 中,Felix 组件完成同样的工作。如果是 Calico BGP 模式,则完全不需要 FDB------因为数据包直接通过物理网络路由,不需要 VTEP 封装。
Flannel vs Calico:架构权衡
这是选择 CNI 插件时最核心的问题。大多数对比文章只列功能清单,这里从网络工程师的视角分析三种方案的取舍:
| 维度 | Flannel (VXLAN) | Calico (BGP) | Calico (VXLAN) |
|---|---|---|---|
| 跨节点通信方式 | VXLAN 封装 | 纯路由 | VXLAN 封装 |
| 网络性能 | 中等(封装开销) | 最优(无封装) | 中等(封装开销) |
| 网络可达性要求 | 节点 IP 可达即可 | 需大二层或 BGP 对等连接 | 节点 IP 可达即可 |
| Network Policy | 不支持 | 支持 L3/L4/L7 | 支持 L3/L4/L7 |
| 大规模集群扩展性 | 好(VXLAN VNI 足够多) | 需 Route Reflector | 好 |
| 运维复杂度 | 低 | 高(BGP 配置) | 中 |
| 加密通信 | 无(需外部方案) | WireGuard(可选) | WireGuard(可选) |
一个关键误解需要澄清 :很多人认为 Calico 比 Flannel"更强大",但在某些场景下 Calico 的 BGP 模式反而是陷阱。如果节点分布在不同的可用区或需要跨公网通信,BGP 路由模式会因为物理网络不支持 BGP 宣告而完全失效,此时选择 Calico BGP 只会带来额外的运维复杂度。正确的思路是:先确认节点之间的网络环境,再决定使用哪种模式。
HostPort 与 ContainerPort:两个端口的本质区别
在 Pod 的端口配置中,HostPort 和 ContainerPort 是两个容易混淆的概念。它们出现在同一个 YAML 的 ports 字段中,但含义完全不同:
ContainerPort 是容器内部应用程序监听的端口。这是应用程序绑定到容器内网络命名空间的具体端口,Pod 中的服务通过这个端口接收流量。在 Pod 定义中声明 ContainerPort 是最佳实践,即使应用程序已经通过 EXPOSE 指令声明了端口,显式声明 ContainerPort 能让 Kubernetes 和其他工具正确识别服务端口。
HostPort 是暴露在宿主机网络接口上的端口。使用 HostPort 时,宿主机上会占用一个端口,外部流量通过 http://<NodeIP>:<HostPort> 直接到达 Pod,而不经过 kube-proxy 或 Service 层。
yaml
# HostPort 与 ContainerPort 的对比示例
spec:
containers:
- name: nginx
image: nginx
ports:
# ContainerPort:容器内 nginx 监听 80
- containerPort: 80
# HostPort:宿主机上对外暴露 30080
hostPort: 30080
为什么要区分这两个概念? ContainerPort 是容器运行时和网络命名空间的概念,与 Kubernetes 本身无关。HostPort 则是一种将 Pod 直接暴露到节点网络的方式------它绕过了 Service 层,意味着这个 Pod 不会获得 Service 提供的负载均衡、健康检查和自动故障转移能力。
生产环境中 HostPort 的典型用途是测试场景(快速验证服务)或 DaemonSet 节点级别的指标采集(让 Prometheus 直接从各节点的固定端口抓取数据)。对于正常的生产服务,强烈建议通过 ClusterIP 或 NodePort Service 暴露,而非直接使用 HostPort。
生产问题:CNI 插件版本不兼容导致的网络异常
问题场景
在 Kubernetes 集群的日常运维中,有一种故障特别隐蔽:节点内核升级后,部分 Pod 无法联网,但节点本身和网络连通性都是正常的 。kubectl 可以连接 API Server,kubectl get nodes 显示节点状态正常,但 Pod 内无法访问 Service 或其他 Pod 的 IP。
这种问题的根因几乎总是 CNI 插件与新内核版本之间的兼容性问题。
根因分析
CNI 插件的部分功能依赖于内核的网络特性。以 VXLAN 为例,VXLAN 的封装和解封装操作在内核的网络栈中完成。不同的内核版本在内核 API(如 ndo_neigh_setup、ndo_fdb_add 等)的行为上有差异。如果 CNI 插件(及其依赖的 iptables/nftables 规则)与新内核的网络 API 不兼容,就会导致封装失败或路由规则不生效。
另一个常见的兼容性问题来源是 iptables vs nftables。Linux 内核 5.0+ 开始逐步支持 nftables,部分 CNI 插件(尤其是较老的版本)在使用 iptables 规则的同时与 nftables 发生冲突,导致规则不生效。
诊断路径
否
否
是
是
否
是
否
是
有
无
否
是
Pod 内无法联网
节点间网络是否正常?
检查物理网络
节点间 ping/SSH
物理网络正常?
修复物理网络
检查节点防火墙
和安全组规则
API Server 连通?
检查 kubeconfig
API Server 健康状态
Pod DNS 是否正常?
检查 CoreDNS
和 DNS Policy
CNI 插件版本是否兼容?
查看 kubelet 日志
是否有 CNI 错误
CNI 插件与内核版本不兼容
重新安装兼容版本的 CNI
检查 iptables vs nftables 冲突
iptables -L -n
规则是否完整?
禁用 nftables
或升级 CNI 插件
检查 Veth Pair 状态
ip link show
veth 是否正常 UP
解决步骤
第一步:确认 CNI 插件版本与当前内核版本的兼容性。最直接的方式是查看 kubelet 日志中的 CNI 相关错误:
bash
# 查看 kubelet 日志中的 CNI 错误
journalctl -u kubelet --no-pager | grep -i cni
# 查看当前加载的 CNI 插件版本
ls -la /opt/cni/bin/
cat /etc/cni/net.d/*.conflist | grep -i version
第二步:如果确认是版本不兼容问题,升级 CNI 插件到与当前内核兼容的版本:
bash
# 下载 CNI 插件(以 containernetworking/plugins 为例)
CNI_PLUGINS_VERSION="v1.4.1"
ARCH="amd64"
DEST="/opt/cni/bin"
sudo mkdir -p "$DEST"
curl -L "https://github.com/containernetworking/plugins/releases/download/${CNI_PLUGINS_VERSION}/cni-plugins-linux-${ARCH}-${CNI_PLUGINS_VERSION}.tgz" | sudo tar -C "$DEST" -xz
# 重建节点上所有 Pod(触发 CNI ADD 重新执行)
kubectl delete pod --all -n kube-system
第三步:处理 iptables vs nftables 冲突。如果内核启用了 nftables 但 CNI 插件仍然使用 iptables,可能需要显式切换回 iptables:
bash
# 切换回 iptables 模式
update-alternatives --set iptables /usr/sbin/iptables-legacy
update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
# 重启 kubelet
sudo systemctl restart kubelet
预防措施 :在内核升级前,先在测试环境中验证 CNI 插件的兼容性;每次升级 Kubernetes 版本时同步升级 CNI 插件;使用 kubeadm 部署时,kubeadm init/join 的 --cri-socket 参数能够确保容器运行时版本与 Kubernetes 版本匹配。
DNS 与 Service 发现:网络之上的名字解析
虽然 Service 是 Kubernetes 的核心概念,但它的实现完全依赖底层的网络基础设施。ClusterIP Service 的工作原理是 kube-proxy 在每个节点上维护的 iptables 或 IPVS 规则表。当 Pod 访问 http://nginx.default.svc.cluster.local 时,CoreDNS 将这个名字解析为 ClusterIP(一个虚拟 IP,不存在于任何网络接口上),iptables/IPVS 规则将这个虚拟 IP 的流量 NAT 转发到实际的后端 Pod IP。
Pod A
访问 nginx 服务
CoreDNS
nginx.default.svc.cluster.local
→ 10.96.45.21
iptables/IPVS 规则
10.96.45.21 → Pod IPs
负载均衡策略
Pod B #1
Pod B #2
Pod B #3
这个三层转发(DNS 解析 → iptables/IPVS 路由 → 实际 Pod)解释了为什么 Service 发现有时候会变慢:当 DNS 缓存过期或 iptables 规则过多时,Service 发现的延迟会显著上升。在高频短连接场景下,这个问题尤为突出,IPVS 模式(相比 iptables 规则数随 Service 数量线性增长)在大规模集群中是更好的选择。
总结
Kubernetes 网络模型的精髓可以用一句话概括:CNI 插件决定了 Pod 如何通信,Service 决定了如何发现通信对象。理解这两者的关系,比记住任何具体的配置参数都重要。
在选择 CNI 插件时,关键是匹配实际的网络环境:节点之间大二层可达时,Calico BGP 路由模式提供最佳性能;节点跨越复杂网络边界时,Flannel 或 Calico 的 VXLAN 模式是更稳妥的选择。两者之间没有绝对的优劣,只有场景的匹配与否。
HostPort 绕过了 Kubernetes 的 Service 层,在测试场景外几乎没有用武之地。生产环境应该始终通过 Service 暴露应用,让 kube-proxy 接管负载均衡和故障转移。
最后,CNI 插件与内核版本的兼容性问题是最容易被忽视的故障来源。内核升级前在测试环境验证 CNI 兼容性,保持 CNI 插件版本与 Kubernetes 版本同步升级------这是避免"节点升级后网络静默崩溃"的关键。
如果这篇文章对你有帮助,欢迎点赞、收藏。如果还想了解更多 Kubernetes 运维相关的实战内容,可以关注作者的后续更新。