“Kubernetes 无法路由哪怕一个数据包。”
这句话不是我说的,而是藏在每一个 CNI 的手工真相里。你可能会觉得很意外——我们每天敲 kubectl apply、看着 Pod 跑起来,网络好像从来没坏过,那不就是 Kubernetes 把网线都插好了吗?
还真不是。Kubernetes 的本职工作是编排:调度 Pod、监控健康状态、把状态写进 etcd。至于怎么把一个字节从容器 A 挪到容器 B,它的内核里连一行能干这事的代码都没有。零。路由?不存在。它只是一个爱写笔记的指挥家,真正的搬运工是它完全依赖的外部代理——Container Network Interface,也就是你常在集群里听到的 Calico、Cilium、Flannel 这些东西。
我们不一定需要再装一遍 Cilium 来“看看它做了什么”,而是可以彻底绕过 Kubernetes,用最原始的 Linux 内核原语,从头把 Pod 网络搭出来。那种“卡住→修好→恍然大悟”的体验,比看十篇原理文章都来得瓷实。
这篇教程就是跟你一起做这件事:先搞出一个最简单的双容器直连,再扩成单节点上的虚拟交换机,最后故意让跨物理主机的包掉在地上,然后亲手用一条路由命令把它捡起来。所有操作只需要两台 Linux 虚拟机、ip 命令和 root 权限,不需要任何 Kubernetes 组件。
读完你应该会带着一种“肌肉记忆”理解:CNI 的真身到底是什么,为什么像 Cilium 这样的高级选手必须动上 eBPF 和 overlay,云厂商又为什么替你默默扛下了这么多脏活。
我们先把网络隔离这件事玩明白。Linux 内核有个东西叫网络命名空间(network namespace),你可以粗暴地理解为“一个进程看到的网络世界”。默认情况下,机器上的所有进程共享同一个网络命名空间,它们能看到同一块网卡、同一张路由表。但当你为某个进程建出一个新的网络命名空间,它就像被塞进了一间没有门窗的空屋子——有自己的界面,却跟外界彻底失联。
在 Kubernetes 里,每个 Pod 都运行在自己的网络命名空间里。所以第一步,模仿两个 Pod:
用 ip netns add pod1 和 ip netns add pod2 造出两个空房间。这时候在各自房间里执行 ip link,你只会看到一块 loopback 接口,能 ping 127.0.0.1 自己,但对面的“室友”一概不知。
怎么把网线牵进房间?需要一个叫作 veth pair 的东西,你可以把它想象成一根两头带水晶头的跳线:一头插在 pod1 的房间里,另一头露在主人的世界(根网络命名空间)里。用 ip link add veth1 type veth peer name veth1-br 就能造出这样一根跳线。然后把 veth1 这一头塞给 pod1:ip link set veth1 netns pod1。现在 pod1 里终于出现了一块可用的以太网接口。
重复一次:给 pod2 也造一组 veth2 和 veth2-br,把 veth2 塞进 pod2。
但这根跳线目前还是“网线没插交换机”的状态——没有 IP,没有路由。在 pod1 的命名空间里给 veth1 配上地址:ip addr add 10.0.1.1/24 dev veth1,然后把它拉起来:ip link set veth1 up。pod2 那边也照做,地址用 10.0.1.2/24。到这儿,ping 一下对方的 IP,会发现还是不通。为什么呢?因为 veth pair 的另一头还在主人的世界里孤零零地挂着,没有跟任何东西桥接。
我们当然可以简单地把两端的 veth1-br 和 veth2-br 都丢进同一个桥梁,但更好的做法是先体会一下“一个节点上如何让三个以上的 Pod 互通”。这就是 Linux bridge 登场的时刻——它相当于一个软件实现的二层交换机。
创建一个 bridge 设备:ip link add br0 type bridge,把它拉起来。然后把 veth1-br 和 veth2-br 都 masters 到这个桥上:ip link set veth1-br master br0,ip link set veth2-br master br0。再把这两个悬在主人世界的接口也激活。现在,pod1 和 pod2 之间的 ping 瞬间通了。如果再建一个 pod3,同样配 10.0.1.3/24 并把它的 veth 头挂到 br0,整个小局域网内的任意两个 Pod 都能直接对话。
这个 br0 加上一堆 veth 的组合,就是早期 CNI 单节点网络的基本骨架。本质上,它只是 Linux 内核二三十年来的老功能,连一行新代码都没有。
但是,真正的集群不可能只在一台机器上。当你把 pod1 留在左边的主机 A,把 pod2 挪到右边的主机 B,问题就冒出来了。左边 pod1 的 IP 是 10.0.1.1,右边 pod2 是 10.0.2.1——它们位于不同的二层广播域,中间还隔着物理网卡。你从 pod1 发出一个 ping 10.0.2.1 的包,它会先找 10.0.2.0/24 的路由,然后毫不犹豫地把包丢到默认网关,或者干脆丢到地上,因为根命名空间里根本没有通往 10.0.2.0/24 的路由。
这就是“单节点好使,跨节点掉线”的经典断点。怎么修?手动告诉主机 A:去往 10.0.2.0/24 的包,请丢给主机 B 的物理网卡,并且让主机 B 知道怎么转进去。在主机 A 上执行 ip route add 10.0.2.0/24 via 。同时在主机 B 上也要配一条回来:ip route add 10.0.1.0/24 via 。此外,还要确保两端都用 iptables 做一点源地址伪装或者转发策略,否则私有地址的包可能会被物理网络打回来。
做完这一步,跨主机的 ping 才终于成功。但这背后的工作量已经悄悄地泄露了“CNI 三个固定动作”的每一个环节。
现在可以非常清楚地归纳,任何 CNI 都得替 Kubernetes 做完三件脏活:
第一,IP 地址管理(IPAM)。就是决定每个 Pod 应该拿哪个 IP、怎么不跟别的 Pod 冲突。你刚才手动给 pod1 和 pod2 配 10.0.1.1 和 10.0.1.2,用的就是这个逻辑。
第二,建立连接。把网络命名空间“接入”到某个转发平面里,不管这个平面是 local bridge、VXLAN overlay 还是 BGP 路由。也就是你刚才插 veth、挂 bridge、配路由这一整套手工作业。
第三,执行策略。丢一些包,允许另一些包。NetworkPolicy 说的“允许 app=web 访问 app=db”就是在这里落地。当然我们刚才没加策略,但如果现在往 iptables 里扔两条 DROP 规则,你就能立刻模拟出网络策略的效果。
理解了这三件事,你就不会再把 Calico 或者 Cilium 当成一个黑盒插件,而会清楚地看到:它们无非是把上面这些手工步骤,自动化、规模化,再引入更高效的数据面(比如 eBPF)去替代 iptables 和路由表。
那为什么到了云上,像 Cilium 这样的高级 CNI 还非要搞 overlay 和 eBPF 不可?因为在公有云环境里,VPC 网络通常不让你控制底层路由,两台虚拟机之间的子网和 ACL 都是云平台说了算。你没法像刚才在自建机房那样,随手往宿主机里塞一条静态路由。要跨越这些限制,就得把 Pod 的真实 IP 藏进外层封装——这就是 VXLAN overlay 的由来。而 eBPF 的作用,则是把原本需要走 iptables 的转发和策略逻辑直接挂到内核的可编程钩子上,在包刚进网卡的时候就做出判断,延迟极低、规模极大。
这些复杂技术并不是为了炫技,而是因为物理网络的约束在云上实在太多。当你亲手在虚拟机里修过一次静态路由,再见到 Cilium 的各类模式时,心里就有了坐标:VXLAN 是帮你在无法控制路由的地方“假装”所有 Pod 在一个二层网络;direct routing 则是当底层网络配合时,直接拿宿主机作为路由节点;而 eBPF 就是所有路径上那个跑得飞快的交通指挥员。
这次我们没有装 Kubernetes,也没运行一个 manifest,却把 Pod 网络的心跳摸得明明白白。在两根 veth 跳线、一座 Linux bridge 和一条跨主机的静态路由之间,藏着所有 CNI 的底层秘密。
下一次你再看到 Pod 跳成 Running 状态时,或许会忍不住笑出来——不是因为网络“奇迹般”通了,而是因为你知道那个默默无闻的 CNI 代理刚替你干完了多少繁琐又具体的体力活。它不神秘,只是一股脑地把 Linux 三十年的网络内力,都打进了 Kubernetes 的经脉里。
热门跟贴