← 返回蜂巢洞察

如何在不使用 Kubernetes 的情况下构建 Kubernetes 网络:手动实现 CNI 所完成的功能

在这篇文章中,你将通过亲手操作原始的Linux内核功能,逐步了解容器网络接口(CNI)的实际工作原理。而不是通过阅读YAML配置文件来学习。 容器网络接口(CNI)是Kubernetes系统中那些被称为“黑盒”的组件之一。大多数每天都在运行Kubernetes集群的人,从来都没有深入了解过它的内部机制。他们只知道自己使用的CNI的名称(比如“我们使用的是Calico”,“我们采用的是Cilium”),这种认知就如同你知道汽车上发电机的品牌一样——只是知道它的名称,并不真正了解它的工作原理。 CNI位于整个系统架构的最底层,位于kubelet之下、Pod们之下,它默默地负责转发每一个数据包。正因

在这篇文章中,你将通过亲手操作原始的Linux内核功能,逐步了解容器网络接口(CNI)的实际工作原理。而不是通过阅读YAML配置文件来学习。 容器网络接口(CNI)是Kubernetes系统中那些被称为“黑盒”的组件之一。大多数每天都在运行Kubernetes集群的人,从来都没有深入了解过它的内部机制。他们只知道自己使用的CNI的名称(比如“我们使用的是Calico”,“我们采用的是Cilium”),这种认知就如同你知道汽车上发电机的品牌一样——只是知道它的名称,并不真正了解它的工作原理。 CNI位于整个系统架构的最底层,位于kubelet之下、Pod们之下,它默默地负责转发每一个数据包。正因为它在正常情况下从不会出现明显的故障,所以几乎没有人真正了解它的具体功能。 我们不会使用`helm install cilium`这样的命令来安装Cilium,也不会应用任何配置文件。相反,我们会从头开始搭建Pod网络架构,亲自观察当数据包试图离开物理服务器时会出现什么问题,并手动进行修复。 通过这个过程,你将真正理解CNI的存在意义,而不仅仅是在理论上知道它的作用;同时,你也会明白为什么像Cilium这样的高级CNI组件会采用覆盖网络技术和eBPF技术来提升性能。 **适合人群:** - 那些每天都在使用Kubernetes,但却把Pod之间的网络连接视为某种“魔法”的开发者、平台工程师和运维人员。 - 任何曾经看到Pod的状态变为“运行中”时,就认为网络连接是自动生效的,但想知道其中具体原理的人。 **你将亲手完成的内容:** - 使用虚拟电缆(`veth`对)将两个独立的网络命名空间连接起来。 - 利用Linux桥接技术构建一个包含三个命名空间的虚拟交换机,这种技术也是传统的CNI组件在单节点上使用的手段。 - 设计一个故意出现故障的两节点网络环境,观察数据包在传输过程中是如何被丢失的,并手动修复这个问题,使其恢复正常工作。 - 清晰地了解CNI所承担的三大核心功能,以及为什么云服务提供商会要求像Cilium这样的高级CNI组件使用覆盖网络技术和eBPF技术。 **注意:** 所有这些操作都需要在Linux主机上以`root`权限进行。建议在临时创建的虚拟机或实验室环境中进行测试,千万不要在重要的生产环境中尝试。整个实验的目的就是制造一些混乱,从而从中学习到真正的知识。

目录

先决条件

在开始学习之前,您需要准备以下条件:

  • 为多节点实验准备两台位于同一网络中的Linux虚拟机,这样就可以观察数据包在真实机器边界之间的传输过程。

  • 需要使用iproute2包中的ip命令(这个工具几乎已经安装在所有现代操作系统上)。

  • 需要对IP地址、子网以及“网关”这一概念有基本的了解。不过,您并不需要成为一名网络工程师。

  • 不要使用Kubernetes。这并不是笔误——我们确实有意不使用Kubernetes来进行实验。

一个错觉:Kubernetes其实无法路由数据包

这里有一个大多数人从未面对过的真相:Kubernetes本身根本无法路由任何网络数据包。

绝对不能。Kubernetes只是一个调度工具,它负责安排Pod的运行、监控它们的状态,并将相关信息存储在etcd中。但当真正需要将数据包从一个容器传输到另一个容器时,Kubernetes完全没有相应的功能。

那么,Pod们之间是如何进行通信的呢?它们完全依赖于外部代理来连接各个节点上的虚拟网络。这个代理就是容器网络接口(CNI)。CNI在幕后默默地完成这些工作,而我们自己如果尝试直接处理这些任务,就会遇到各种问题;正是CNI提供了解决方案。

以下是证明CNI确实承担了这一关键功能的例子:使用kubeadm创建一个新的Kubernetes集群,然后查看各个节点的状态:

$ kubectl get nodes
NAME       STATUS     ROLES                  AGE   VERSION   INTERNAL-IP     EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION                        CONTAINER-RUNTIME
no-cni     NotReady   control-plane,master   52s   v1.35.0   192.168.117.2   <none>        Ubuntu 24.04.3 LTS   7.0.11-orbstack-00360-gc9bc4d96ac70   containerd://2.1.6
worker-1   NotReady   <none>                 46s   v1.35.0   192.168.117.3   <none>        Ubuntu 24.04.3 LTS   7.0.11-orbstack-00360-gc9bc4d96ac70   containerd://2.1.6
worker-2   NotReady   <none>                 40s   v1.35.0   192.168.117.4   <none>        Ubuntu 24.04.3 LTS   7.0.11-orbstack-00360-gc9bc4d96ac70   containerd://2.1.6

NotReady。所有的节点都是这个状态。控制平面正常运行,etcd也处于开启状态,调度器也在工作,但整个集群就是无法进入Ready状态。如果查看每个节点的具体信息,就能清楚地知道到底缺少什么:

Conditions:
  Type             Status  LastHeartbeatTime                 LastTransitionTime                Reason                       Message
  ----             ------  -----------------                 ------------------                ------                       -------
  Ready            False   Sat, 18 Jul 2026 10:25:51 +0200   Sat, 18 Jul 2026 10:25:20 +0200   KubeletNotReady              container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized

再读一遍:除非你安装了CNI,否则你的集群 仍然无法进入“准备就绪”状态。不是“差不多准备好了”,也不是“除了网络连接之外其他方面都已准备妥当”——确切地说,就是未准备就绪,没有别的解释。直到有外部插件出现并负责处理那些Kubernetes本身拒绝处理的数据包,情况才会改变。

一个没有CNI的集群就像一座没有接入任何线路的电话交换机:所有的操作员都在那里,随时可以工作,但却无法接通任何电话。

那么,在这种情况下,大多数人会怎么做呢?他们会从入门指南中复制一行命令来使用:

kubectl apply -f https://.../calico.yaml

他们看着节点状态变为Ready,然后就继续下一步操作。对于大多数工程师来说,这个组件就是让他们集群能够正常运行的关键所在。他们只是凭经验来使用它,因为这种方法确实有效,所以他们从未去深入了解过这个组件的具体原理。

这一点非常重要,因为“网络连接就是自动就能用的”这种想法其实非常危险。一旦出现问题(比如某个Pod无法访问其他服务、节点间的数据传输突然中断,或者在进行云迁移时数据包丢失),你就会发现自己面对的是一个根本不了解的系统。因此,我们必须从基础开始,真正弄清楚它的原理。

基础:虚拟以太网对

要理解容器网络的工作原理,首先必须了解Linux是如何实现网络隔离的。

当创建一个容器(或Kubernetes Pod)时,内核会为它创建一个独立的网络命名空间。可以把这个新的网络命名空间想象成一个与外界完全隔绝的岛屿——没有桥梁可以与大陆相连。默认情况下,这个命名空间对外部世界是完全封闭的:没有接口、没有IP地址,也没有路由表。它无法与任何外部设备进行通信,也没有任何设备能够与它建立连接。

一个新的网络命名空间就像一座与外界隔绝的岛屿。

那么,我们该如何让这个“岛屿”与外界建立连接呢?答案就是使用一种名为虚拟以太网对的内核机制。

虚拟以太网对实际上就是一根虚拟的网络电缆。无论什么数据包进入其中一端,都会立即从另一端输出出来,即使这两个端位于不同的网络命名空间中也是如此。只要将其中一端连接到“岛屿”上,另一端连接到“大陆”上,连接就立刻建立了。

让我们来直接连接两个独立的网络命名空间:redblue吧。


# 第一步:创建独立的网络命名空间
sudo ip netns add red
sudo ip netns add blue

# 第二步:创建虚拟以太网对
sudo ip link add veth-red type veth peer name veth-blue

# 第三步:将虚拟以太网对的两端分别连接到对应的网络命名空间中
sudo ip link set veth-red netns red
sudo ip link set veth-blue netns blue

# 第四步:为虚拟网络接口分配IP地址并使其处于激活状态
sudo ip netns exec red ip addr add 10.0.0.1/24 dev veth-red
sudo ip link set veth-red up

sudo ip netns exec blue ip addr add 10.0.0.2/24 dev veth-blue
sudo ip link set veth-blue up

现在,通过在red内部发送ping命令来测试连接是否正常:blue

sudo ip netns exec red ping -c 2 10.0.0.2
一个新的网络命名空间就像一座孤岛:现在通过veth对与主机网络连接起来了。

最终,结果会如下所示:

  • 隔离状态已被安全地打破:该容器从一个无法访问、处于孤立状态的命名空间(没有IP地址,也没有路由功能)转变成了一个可被寻址的终端节点(地址为10.1.1.2),并且这个终端节点直接与主机网络相连。

  • 数据可以双向流动:从容器内部发出的数据包能够到达外部公共IP网络;而来自互联网的响应数据包则可以通过主机的eth0接口(地址为192.168.1.10)直接进入容器的veth对中。

  • 内核级别的桥接技术实现了零延迟传输:veth对(veth-island --> veth-mainland)就像一根虚拟管道,使得不同的Linux网络命名空间之间的数据包能够瞬间传输,而无需依赖任何外部物理硬件设备。

结论:ping命令测试成功了。你仅仅使用了一条虚拟“电缆”,就成功地将两个原本孤立的环境连接在了一起。

然而,这种方法的缺点也很明显:它的扩展性极差。因为veth对是一种点对点的连接方式,所以当容器数量增加时,所需的veth对数量也会呈指数级增长。

以下是不同数量的容器所需配置的veth对数量:

  • 2个容器:需要1对veth对

  • 3个容器:需要3对veth对

  • 4个容器:需要6对veth对

如果要有10个容器,那么就需要准备45条veth对来连接它们。

  • 电缆数量会呈指数级增长:随着容器数量的增加,所需的veth对数量会呈二次方关系上升。

  • 管理起来非常复杂:需要配置和维护大量的接口、路由规则等。

图片说明了当容器数量较多时,使用单一veth对连接所存在的问题

这就是为什么数据中心不会在每对服务器之间都铺设物理电缆的原因——它们需要使用交换机来连接这些设备。

如何利用Linux桥接技术实现本地扩展

当需要在同一台主机上连接多个接口时,直接用电缆将所有设备连接起来并不现实,此时应该使用一个中央枢纽来进行连接。在Linux内核中,这个枢纽就是Linux桥接器。它是一种软件实现的第2层虚拟交换机(通常被命名为br0cni0)。

桥接器的功能与物理交换机完全相同:它会记录所有连接接口的MAC地址,并将数据包转发到同一个广播域内的所有接口上。

这种连接方式略有不同。不再直接将各个命名空间相互连接,而是将一对veth设备的一端连接到相应的命名空间上,而将另一端插入宿主的网桥中。 图片展示了如何通过Linux网桥将各个虚拟网络区域连接起来 让我们拆除原有的配置,然后构建一个包含三个命名空间的网络结构:redbluegreen。这三个命名空间将共享同一个广播域。
# 清除之前的所有配置
sudo ip netns del red 2>/dev/null || true
sudo ip netns del blue 2>/dev/null || true
sudo ip netns del green 2>/dev/null || true
sudo ip link del br0 2>/dev/null || true

# 第一步:创建宿主网桥并使其处于激活状态
sudo ip link add br0 type bridge
sudo ip link set br0 up

# 第二步:将命名空间1(red)连接到网桥上
sudo ip netns add red
sudo ip link add veth-red type veth peer name veth-red-host
sudo ip link set veth-red netns red
sudo ip link set veth-red-host master br0
sudo ip link set veth-red-host up
sudo ip netns exec red ip addr add 10.0.0.1/24 dev veth-red
sudo ip netns exec red ip link set veth-red up

# 第三步:将命名空间2(blue)连接到网桥上
sudo ip netns add blue
sudo ip link add veth-blue type veth peer name veth-blue-host
sudo ip link set veth-blue netns blue
sudo ip link set veth-blue-host master br0
sudo ip link set veth-blue-host up
sudo ip netns exec blue ip addr add 10.0.0.2/24 dev veth-blue
sudo ip netns exec blue ip link set veth-blue up

# 第四步:将命名空间3(green)连接到网桥上
sudo ip netns add green
sudo ip link add veth-green type veth peer name veth-green-host
sudo ip link set veth-green netns green
sudo ip link set veth-green-host master br0
sudo ip link set veth-green-host up
sudo ip netns exec green ip addr add 10.0.0.3/24 dev veth-green
sudo ip netns exec green ip link set veth-green up
由于这三个命名空间都连接到了共享的br0设备上,因此它们可以通过这个虚拟网桥自由地相互通信。但是,它们在网络中究竟是如何“找到”彼此的呢?这就是ARP发挥作用的地方。 ARP即地址解析协议,它是本地网络通信的基础组件。当一台计算机(或者在我们的例子中是某个命名空间)想要通过IP地址与另一台计算机进行通信时,它首先需要获取对方的硬件地址(也就是MAC地址),这样才能在网络上发送数据包。 ARP正是实现这一功能的机制——它会发送一条广播消息,询问“谁的IP地址是X?请告诉我你的MAC地址”,然后相应的系统会回复这个请求。 得益于ARP,所有连接到br0设备上的命名空间都能够自动获取彼此的MAC地址,并且可以在它们共享的网络段内直接发送数据包。我们可以通过互相发送ping命令来验证这一机制是否正常工作:

# “红色”命名空间与“蓝色”和“绿色”命名空间之间的通信
sudo ip netns exec red ping -c 1 10.0.0.2
sudo ip netns exec red ping -c 1 10.0.0.3

# “蓝色”命名空间与“红色”和“绿色”命名空间之间的通信
sudo ip netns exec blue ping -c 1 10.0.0.1
sudo ip netns exec blue ping -c 1 10.0.0.3

# “绿色”命名空间与“红色”和“蓝色”命名空间之间的通信
sudo ip netns exec green ping -c 1 10.0.0.1
sudo ip netns exec green ping -c 1 10.0.0.2

所有六次ping操作都成功完成了。需要注意的是,整个过程中并没有使用任何路由规则。所有的命名空间都位于同一个10.0.0.0/24子网中,且都在同一台二层交换机上,因此内核是通过普通的ARP协议来解析这些网络连接的。

这正是传统的单节点CNI(旧的kubenet)的工作方式。在单一机器上,这种机制确实简单明了。

但是Kubernetes是一个分布式系统,它的设计目标是能够扩展到数千台物理机器上。那么问题就出现了:当我们的命名空间需要跨越不同的主机进行通信时,会遇到什么问题呢?

多节点边界问题

到目前为止,所有的测试都是在同一台机器上进行的。但Kubernetes并不是这样的。那么,让我们来实际操作一下吧:创建两台虚拟机,看看单节点模式在多节点环境中的局限性。别只听我说的,亲自尝试一下,你就会发现数据包在传输过程中会丢失。

以下是实验配置:

  • 虚拟机1(主机IP地址10.1.44.216):包含“红色”命名空间及其对应的Pod子网10.0.1.0/24

  • 虚拟机2(主机IP地址10.1.44.178):包含“蓝色”命名空间及其对应的Pod子网10.0.2.0/24

在开始实验之前需要注意两点。首先,每个节点都会有自己独立的Pod子网(虚拟机1使用10.0.1.0/24,虚拟机2使用10.0.2.0/24),因为如果两个节点都使用相同的IP地址段,就会导致IP冲突。

其次,由于子网不同,每个命名空间都需要一个“网关”来转发数据包,而这个网关其实就是所在主机的桥接设备。

注意:这个cleanup-multinode.sh脚本仅应在你在配置跨节点路由或veth对时出现错误的情况下使用。

#!/usr/bin/env bash # # cleanup-multinode.sh # 用于拆除手动搭建的多节点CNI环境(包括桥接设备、命名空间、veth对以及跨节点静态路由) # 这个脚本基于“通过手动操作来理解Kubernetes CNI的工作原理”这一教学思路编写。 # # 可以在两台虚拟机上同时运行。每个操作都是幂等的,即如果某个资源本来就没有在这个主机上创建过,那么执行该操作也不会产生任何错误,因此重新运行这个脚本也是安全的。 # # 使用方法:sudo ./cleanup-multinode.sh # set -u if [[ $EUID -ne 0 ]]; then echo "此脚本需要root权限。请以管理员身份运行:sudo $0" &>&2 exit 1 fi echo "==>> 正在删除网络命名空间(同时也会删除对应的veth对)..." ip netns del red 2>/dev/null &;& echo " - 删除了命名空间'red'" || true ip netns del blue 2>/dev/null &&& echo " - 删除了命名空间'blue'" || true echo "==>> 正在删除那些没有关联主机的veth接口..." ip link del veth-red-host 2>/dev/null &&& echo " - 删除了veth-red-host" || true ip link del veth-blue-host 2>/dev/null &&& echo " - 删除了veth-blue-host" || true echo "==>> 正在删除桥接设备..." ip link del br0 2>/dev/null &&& echo " - 删除了桥接设备'br0'" || true echo "==>> 正在删除跨节点静态路由..." ip route del 10.0.1.0/24 2>/dev/null &&& echo " - 删除了通往10.0.1.0/24的路由" || true ip route del 10.0.2.0/24 2>/dev/null &&& echo " - 删除了通往10.0.2.0/24的路由" || true echo "==>> 正在禁用IP转发功能(此操作是非永久性的,重启后效果会恢复)..." sysctl -w net.ipv4.ip_forward=0 &>/dev/null # --- 可选步骤:如果你之前对系统配置进行了某些调整,现在可以撤销这些更改 --- # 在用于实验的临时虚拟机上,通常将IP转发功能设置为ACCEPT或者将rp_filter参数设置为0不会产生任何不良影响,因此这些操作是可选的。请根据需要取消注释相应的命令。 # sysctl -w net.ipv4.conf.all.rp_filter=1 >/dev/null # iptables -P FORWARD DROP echo echo "==>> 清理工作已完成。现在来检查是否还有残留物..." echo "--- 命名空间(预期结果:没有红色或蓝色命名空间)---" out=$(ip netns list); echo "${out:- (没有剩余的命名空间)}" echo "*** 桥接设备(预期结果:没有br0桥接设备)---" out=$(ip -br link show type bridge 2>/dev/null); echo "${out:- (没有剩余的桥接设备)}" echo "--- 跨节点路由(预期结果:没有路由条目)---" out=$(ip route | grep -E '10\.0\.[12]\.0/24'); echo "${out:- (没有剩余的路由条目)}"

在VM 1上(10.1.44.216),建立桥接连接,将red命名空间连接到桥上,并将该主机设置为路由器:

# 将主机设置为路由器,以便能够转发非本机发出的数据包
sudo sysctl -w net.ipv4.ip_forward=1

# 建立桥接连接,并为VM 1的子网指定网关地址
sudo ip link add br0 type bridge
sudo ip addr add 10.0.1.254/24 dev br0
sudo ip link set br0 up

# 将red命名空间连接到桥接连接上
sudo ip netns add red
sudo ip link add veth-red type veth peer name veth-red-host
sudo ip link set veth-red netns red
sudo ip link set veth-red-host master br0
sudo ip link set veth-red-host up
sudo ip netns exec red ip addr add 10.0.1.1/24 dev veth-red
sudo ip netns exec red ip link set veth-red up

# 将red命名空间的默认路由设置为桥接连接的网关地址
sudo ip netns exec red ip route add default via 10.0.1.254

在VM 2上(10.1.44.178),对blue命名空间执行相同的操作:

sudo sysctl -w net.ipv4.ip_forward=1

sudo ip link add br0 type bridge
sudo ip addr add 10.0.2.254/24 dev br0
sudo ip link set br0 up

sudo ip netns add blue
sudo ip link add veth-blue type veth peer name veth-blue-host
sudo ip link set veth-blue netns blue
sudo ip link set veth-blue-host master br0
sudo ip link set veth-blue-host up
sudo ip netns exec blue ip addr add 10.0.2.1/24 dev veth-blue
sudo ip netns exec blue ip link set veth-blue up

sudo ip netns exec blue ip route add default via 10.0.2.254

现在,这两台主机都成为了路由器,两个命名空间也都已经成功连接起来。从VM 1的red命名空间尝试向VM 2的blue命名空间发送ping请求:

# 在VM 1上执行操作
sudo ip netns exec red ping -c 3 10.0.2.1
PING 10.0.2.1 (10.0.2.1) 56(84) bytes of data.

--- 10.0.2.1 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2043ms

结果显示“100%的数据包丢失”。这些数据包确实没有到达VM 2,这一现象与之前的预测完全一致。不过现在你们已经亲眼看到了这一过程。

接下来需要验证的一点是:这些数据包确实是从VM 1发出的,只是从未到达VM 2而已。在两台机器上分别运行tcpdump工具,然后再尝试发送ping请求:

# 首先确定自己的物理网卡编号
NIC=$(ip route get 1.1.1.1 | grep -oP 'dev \K\S+')

# 在VM 1上观察数据包的传输过程
sudo tcpdump -ni "$NIC" icmp
IP 10.0.1.1 > 10.0.2.1: ICMP echo request, id 5, seq 1, length 64
IP 10.0.1.1 > 10.0.2.1: ICMP echo request, id 5, seq 2, length 64

# 在VM 2上观察结果,会发现没有数据包到达
sudo tcpdump -ni "$NIC" icmp
(没有输出)

那么这些数据包到底在哪里丢失了呢?让我们追踪一下它们的传输路径吧:

数据包在节点间的传输过程

为了让说明更加简洁,整个流程如下:

红色Pod(10.0.1.1)
      │
      ▼
eth0
      │
      ▼
veth-red
      │
      ▼
br0(10.0.1.254)
      │
      ▼
VM 1的路由表
(没有针对10.0.2.0/24的路由)
      │
      ▼
默认路由(0.0.0.0/0)
      │
      ▼
物理网卡(eth0)
      │
      ▼
局域网网关(192.168.1.1)
      │
      ▼
❌ 没有针对10.0.2.0/24的路由
(数据包被丢弃)
      │
      ▼
VM 2从未收到该数据包

用一句话来概括,这就是多节点环境中的边界问题:每个节点上的脚本完全无法感知集群中其他节点的存在。VM 1构建了自己的独立网络环境,VM 2也是如此,它们彼此根本不知道对方的存在。

如何通过直接路由手动解决这个问题

解决方法其实非常简单。VM 1并不需要更复杂的网络配置,它只需要一张映射关系表。我们只需向每台主机说明一个关键信息:「另一个节点的Pod子网位于该节点的物理IP地址后面。」这样,每一方都只需要设置一条静态路由即可。之前配置的所有桥接设备、命名空间以及转发规则都可以保持不变。

在VM 1上(10.1.44.216),告诉它VM 2的Pod子网位于何处:

# VM 2的Pod子网(10.0.2.0/24)可以通过VM 2的物理IP地址访问
sudo ip route add 10.0.2.0/24 via 10.1.44.178

在VM 2上(10.1.44.178),告诉它如何返回到VM 1:

# VM 1的Pod子网(10.0.1.0/24)可以通过VM 1的物理IP地址访问
sudo ip route add 10.0.1.0/24 via 10.1.44.216

就这样,只需要两行命令即可。现在在VM 1上的red节点上再次执行ping测试:

sudo ip netns exec red ping -c 3 10.0.2.1
PING 10.0.2.1 (10.0.2.1) 56(84)字节的数据。
64字节从10.0.2.1发出:icmp_seq=1 ttl=62 时间=0.412毫秒
64字节从10.0.2.1发出:icmp_seq=2 ttl=62 时间=0.388毫秒
64字节从10.0.2.1发出:icmp_seq=3 ttl=62 时间=0.401毫秒

--- 10.0.2.1的ping测试统计结果 ---
共发送3个数据包,收到3个数据包,丢包率为0%

测试成功。(注意看ttl=62这一项?说明数据包在传输过程中经过了两个路由器。)现在数据包已经能够完成整个传输路径。

数据包在命名空间之间的传输路径

简化后的流程如下:

1. Pod(10.0.1.1)
        │
        ▼
2. veth-red
        │
        ▼
3. VM 1的br0网卡(10.0.1.254)
        │
        ▼
4. VM 1的路由表
   ✅ 静态路由:
   10.0.2.0/24 → 10.1.44.178
        │
        ▼
5. VM 1的物理网卡
        │
        ▼
6> 直连路径
   VM 1 → VM 2
        │
        ▼
7> VM 2的物理网卡
        │
        ▼
8> VM 2的路由表
   ✅ 10.0.2.0/24属于直连地址
        │
        ▼
9> VM 2的br0网卡
        │
        ▼
10> 蓝色Pod(10.0.2.1)
        │
        ▼
✅ 回应数据包会沿相同的路径返回

这一行代码彻底改变了整个流程。原本,数据包会被发送到局域网中的网关设备,但现在,虚拟机1会直接将数据包转发给虚拟机2,而虚拟机2清楚地知道哪个本地命名空间拥有10.0.2.1这个IP地址。回复数据包则会通过预先设置好的路由路径返回。这样,我们就实现了一种跨节点的容器网络连接方式。

不过,回想一下当初实现这一过程是多么繁琐吧。在两个节点上,都需要手动编写大量的命令,并配置复杂的路由规则。

想象一下,如果有上千个节点,而且每个节点上的容器都在不断创建和销毁,那么每个容器都需要在集群中的其他所有节点上获得新的IP地址和路由信息。如果靠手工来完成这些操作,不仅非常繁琐,而且根本是不可能的。

那么,CNI到底是什么呢?

你刚才手动完成的所有操作——配置命名空间、创建veth对、设置桥接设备、分配IP地址以及配置路由规则——这些功能,容器网络接口都能够自动地、大规模地完成,而且一旦有新的容器被创建,这些配置就会立即生效。

当你提交容器的配置信息时,CNI插件会捕获这个生命周期事件,并执行三项核心任务:

  1. 命名空间与网络接口的配置:它会创建相应的网络命名空间,生成veth对,并将其连接到桥接设备或专门的数据路径上,每次操作都会非常准确无误,不会出现任何错误。

  2. IP地址管理:它会给每个节点分配唯一的、不会冲突的子网地址,并为集群中的每一个容器分配一个独立的IP地址。你之前手动设置的“每个节点只能使用一个唯一的Pod CIDR地址”规则,CNI会自动将其执行到位。

  3. 全局路由配置:它会统一规划整个集群的路由机制,确保每个节点都能知道如何与其他节点上的容器进行通信。那些你之前手动编写的静态路由规则,现在都会被自动应用到所有节点上,并且随着节点和容器的增减而实时更新。

这就是CNI的本质:它能够以极高的效率自动完成这些复杂的操作,而且从来不会出错。

为什么Cilium能带来革命性的变化?

这里有一个让人感到惊讶的事实:我们刚才介绍的那种手动配置路由的方法,在纯硬件环境中运行得非常顺利,但在现代的公共云平台(如AWS、GCP、Azure)中,这种方法却完全无法正常使用。

为什么呢?因为云服务提供商不会允许任意的IP地址在他们的网络中自由流动。你的10.0.1.0/24子网对VPC来说没有任何意义;除非这些IP地址通过专门的云控制接口被明确注册,否则底层网络会认为这些数据包是非法的,从而直接将其丢弃。这种情况与之前讨论的“边界问题”如出一辙,只不过现在拒绝这些请求的是云服务本身。

这就是为什么像Cilium这样的先进容器网络接口能够打破传统的规则。它不再依赖那些不稳定的Linux桥接设备和手动编写的路由规则,而是采用了两种更为强大、可靠的技术机制来完成任务。

  • 覆盖网络技术(VXLAN / Geneve)。 Cilium会取用您的原始数据包,将其封装在普通的UDP数据包中,这些UDP数据包的发送地址是节点1的物理IP地址,接收地址则是节点2的物理IP地址。对于云服务提供商来说,这些数据包看起来完全就是正常的节点间通信流量,因此能够顺利通过所有的VPC安全规则。您的数据包的真实地址被隐藏在封装层之中。

  • eBPF内核编程功能。传统的网络接口代理机制会让所有数据包依次经过Linux桥接机制以及数百条iptables规则,这种处理方式效率低下,而且随着集群规模的扩大,性能会进一步下降。Cilium则通过将编译好的eBPF程序直接加载到内核中,在网络接口层进行数据包的处理。这样一来,数据包可以直接从虚拟网络设备veth传输到物理网卡上,从而实现接近线速的性能,并且还能提供强大的安全监控功能。

以下是整个发展过程的汇总表格:

功能 直接使用veth电缆 桥接加静态路由 高级CNI技术(如Cilium)
仅用于连接两个终端节点 支持 支持 支持
无法扩展到多个容器实例 不支持 仅在一个节点上可用 支持,可在整个集群中使用
无法跨越节点边界进行通信 不支持 需要为每个节点手动配置路由 自动完成路由配置
无法遵循云虚拟私有网络的规则 不支持 不支持 支持(通过VXLAN/Geneve覆盖层实现)
IP地址分配方式 需手动配置 需手动配置 自动通过IPAM系统进行分配
性能表现路径 内核层 桥接加iptables eBPF,接近线速
维护者 你需要永久亲自维护 你需要永久亲自维护 CNI会自动完成相关维护工作

请看一下最后一列和最后一行,这两行就简洁地概括了CNI技术的所有核心优势。

结论

你之前并没有系统地学习过Kubernetes的网络架构,而是亲自搭建、调试并解决了其中出现的问题。现在,你应该已经形成了以下这些认知:

  1. Kubernetes本身并不处理数据包的路由工作。它将所有网络相关任务都交由CNI来完成,而CNI会在每个节点上执行实际的物理层通信操作。

  2. veth对是一种虚拟电缆,它是容器网络的基本构建单元。对于连接两个终端节点来说非常有用,但无法满足大规模应用的需求。

  3. Linux桥接机制实际上是一种虚拟交换机,它仅通过第二层协议和ARP来连接同一主机上的不同命名空间。简而言之,这就是单节点环境下的CNI实现方式。

  4. 当跨越节点边界进行通信时,传统的网络架构会遇到严重问题。不同的子网以及物理网络的限制会导致数据包丢失,直到你为每台主机手动配置路由规则为止。

  5. 静态路由加IP转发功能可以手动解决这些问题,

    但如果你只为两个节点手动配置这些设置,就会立刻明白为什么在实际应用中不可能为成千上万的节点都这样做。
  6. CNI技术能够自动完成三项关键任务:接口配置、IP地址管理以及全局路由分配。

  7. 云环境会破坏直接的路由机制,因此Cilium才会选择使用VXLAN/Geneve覆盖层和eBPF技术,而不是传统的桥接和iptables。

下次当某个容器实例的状态变为“运行中”且网络能够正常工作时,你就会明白:其实没有什么东西是能自动就能完美运行的。正是CNI默默地、高效地完成了你之前需要手动完成的所有工作。

接下来,你应该考虑删除那些手工配置的网络脚本,将Cilium部署到真实的集群环境中,然后观察eBPF如何自动管理整个网络拓扑结构。在亲身体验过手工配置的繁琐过程之后,你一定会更加欣赏这种自动化解决方案带来的便捷与高效。

如果这些内容帮助你更清晰地了解了Kubernetes的网络架构,请随时联系我:

相关文章

技术实践

如何利用Gemini构建人工智能功能:面向开发者的提示工程实用指南

大多数关于提示工程的教学教程都遵循相同的流程:安装SDK,输入API密钥,调用 generateContent 函数,然后打印输出结果。模型会生成一些看似合理的内容,之后教学教程也就结束了。 但当你真正尝试将这个系统投入实际使用时,才会发现其实真正的准备工作根本还没有开始。 “API返回的文本”与“让用户感到可信的实际功能”之间的差距,正是需要耗费大量精力去解决的地方。 这个差距中充满了各种棘手的问题:模型生成的内容听起来和其他聊天机器人没什么两样;它会编造用户从未说过的话;它返回的数据会被用Markdown格式包裹起来;系统会在凌晨2点出现故障;而对于那些只是想得到答案的用户来说,系统展示的

阅读全文
技术实践

如何使用 shadcn/ui 在 React 中构建一个可重复使用的日期时间选择器

日期和时间选择器这类组件,在设计文件中看起来可能很简洁,但一旦开始实际开发,就会发现它们会消耗大量的资源。你需要一个日历、一个时间选择器,以及一个能够保证这两者同步的状态管理系统,通常还需要范围选择功能以及对应的多语言版本。 本指南将介绍一些现成的选择器组件,你可以直接将这些组件应用到你的React项目中:组合型日期和时间选择器、日期范围选择器以及时间选择器。 所有这些组件都可以作为 Shadcn日期和时间选择器 组件使用,你只需通过一条CLI命令即可安装它们,而无需从头开始开发。 这些组件都是基于Radix和Base UI的基础架构构建的,下面介绍的版本是使用Base UI实现的。此外,这些

阅读全文
技术实践

如何使用MONAI在超声数据上训练肿瘤分割模型

大多数分割教程都是从选择一个模型开始,将图像输入该模型中,然后调整超参数直到相关指标得到改善。但这种方法忽略了通常最为关键的一步:理解数据本身。 在本教程中,我们首先会对数据集进行详细分析,随后会根据这些分析结果来决定MONAI分割流程中的每一个设计细节。 我们将涵盖以下内容: 本教程适合谁? 关于数据集 什么是MONAI,为什么使用它? 什么是Dice评分? 第1部分——建模前的数据分析 类别平衡对分割结果的影响 患者数量对数据划分的影响 第2部分——构建分割流程 单一配置对象 按患者分组的数据划分方式 由快照自动选择的转换操作 模型、损失函数与评估指标 结果解读 预测结果可视化 失败模式比

阅读全文
技术实践

如何使用LangSmith来追踪和监控人工智能代理的行为

在本教程中,我将向您展示如何使用LangSmith来追踪和监控本地的AI代理。我们会构建一个简单的本地AI代理,然后为其启用LangSmith追踪功能,这样我们就能通过Web界面查看模型调用情况、工具使用情况以及请求处理延迟等信息。 我们将使用LangChain v1、Ollama、Qwen以及Python这些工具。除了用于实现观测功能的组件外,所有操作都在您的本地机器上完成,因此代理本身不会产生任何与模型API相关的费用。 目录 背景知识 什么是可观测性与监控? 什么是LangSmith? 开发动机与架构设计 步骤1:安装Ollama并下载模型 步骤2:安装Python相关依赖库 步骤3:启

阅读全文