一文读懂 K8s 容器网络命名空间:为什么说 NetNS 才是容器网络的基石
很多人学 Kubernetes 网络,一上来就被 Calico、Flannel、Overlay、BGP 这些高大上的名词搞晕了。各种路由表、隧道协议堆在一起,像个黑盒。
其实,不管上层网络插件(CNI)怎么变,底层的技术红线永远只有一条:Linux 网络命名空间(Network Namespace,简称 NetNS)。
今天我们不画复杂的架构图,直接从 Linux 内核底层出发,用最直观的命令行实验,带你手动“手撕”一个容器网络,看完你就会彻底明白:为什么说 NetNS 才是整个容器网络的灵魂。
一、 什么是 Network Namespace?
简单来说,NetNS 是 Linux 内核提供的一种网络隔离机制。
在默认情况下,Linux 系统的网络栈(网卡、路由表、防火墙规则、Socket 等)是全局的,所有进程共享。而 NetNS 可以把这个网络栈“分身”。
每个被创建出来的 NetNS,都拥有自己独立且完全隔离的:
- 网卡(Network Devices)
- 路由表(IPv4/IPv6 Routing Tables)
- 防火墙规则(iptables / nftables)
- 套接字(Sockets)
这也就是为什么,你在同一个 VM 上启动两个容器,它们都可以监听 0.0.0.0:80 端口而不会发生端口冲突。因为它们在不同的 NetNS 里,各自有独立的端口空间。
二、 动手实验:不用 Docker,用纯命令构建容器网络
光说不练假把式。我们不借助任何容器运行时,只用 Linux 自带的 ip 命令,手动创建两个隔离的网络空间,并让他们通网。
通过这个实验,你会看清 CNI 插件在幕后到底对你的 Pod 做了什么。
1. 创建两个独立的“容器空间”
我们创建两个 NetNS,分别命名为 ns1 和 ns2:
# 创建命名空间
sudo ip netns add ns1
sudo ip netns add ns2
# 查看已创建的命名空间
ip netns list
此时,ns1 和 ns2 就像两台断网的孤立主机。你可以尝试在 ns1 里执行 ifconfig,会发现里面空空如也,只有个处于 DOWN 状态的本地回环网卡 lo。
# 在 ns1 中查看网卡
sudo ip netns exec ns1 ip link list
2. 修一条“网线”:Veth Pair
两个孤立的空间要怎么通信?Linux 提供了一种虚拟设备叫 Veth Pair(Virtual Ethernet Pair)。
它就像一根双向通行的虚拟网线,一头插在 ns1,另一头插在 ns2。
我们先创建这根“网线”:
# 创建一对 veth 设备,分别命名为 veth1 和 veth2
sudo ip link add veth1 type veth peer name veth2
此时,这两张网卡还在主机的默认网络空间里(可以通过 ip link 看到)。接下来,我们把“网线”的两头分别甩进两个命名空间:
# 把 veth1 放入 ns1,把 veth2 放入 ns2
sudo ip link set veth1 netns ns1
sudo ip link set veth2 netns ns2
3. 配置 IP 并激活网卡
“网线”插好了,但现在还没有配置 IP,网卡也处于禁用状态。我们进到空间里去配置它们:
# 配置 ns1 端的网卡
sudo ip netns exec ns1 ip addr add 10.1.1.1/24 dev veth1
sudo ip netns exec ns1 ip link set veth1 up
sudo ip netns exec ns1 ip link set lo up
# 配置 ns2 端的网卡
sudo ip netns exec ns2 ip addr add 10.1.1.2/24 dev veth2
sudo ip netns exec ns2 ip link set veth2 up
sudo ip netns exec ns2 ip link set lo up
4. 测试连通性
万事俱备,现在我们在 ns1 里去 ping ns2 的 IP:
sudo ip netns exec ns1 ping -c 3 10.1.1.2
输出结果:
PING 10.1.1.2 (10.1.1.2) 56(84) bytes of data.
64 bytes from 10.1.1.2: icmp_seq=1 ttl=64 time=0.045 ms
64 bytes from 10.1.1.2: icmp_seq=2 ttl=64 time=0.038 ms
成功通网!这就是最原始的容器两点通信模型。
三、 Kubernetes 是如何利用 NetNS 的?
看完上面的实验,我们再来看 Kubernetes。K8s 的基本调度单位是 Pod,而不是单容器。那么 Pod 的网络又是怎么解决的?
这里有三个核心结论,也是面试中高频出现的底层原理:
1. 为什么同一个 Pod 里的多个容器可以共享 IP?
在 K8s 中,一个 Pod 内部可能运行着多个容器(比如业务容器和 Sidecar 容器)。它们之所以能通过 localhost 直接通信,是因为它们共享了同一个 Network Namespace。
当 K8s 创建 Pod 时,它首先会启动一个叫 Pause 容器(也叫 Infra 容器)的隐藏角色。
- Pause 容器启动后,第一件事就是创建并占住一个 NetNS。
- 随后启动的业务容器,都会通过
Containerd或Docker的join方式,强制加入到这个 Pause 容器的 NetNS 中。 - 也就是说,一个 Pod 里只有一张虚拟网卡,大家共用,IP 自然也是同一个。
2. CNI 插件到底在干嘛?
在上面的实验中,我们用一条 Veth Pair 连接了两个空间。但在实际生产中,一台物理机上可能有上百个 Pod,两两配对建 Veth Pair 显然不现实(复杂度是 $O(N^2)$)。
这时候就需要一个“交换机”。在单机上,这个交换机叫 Bridge(网桥)。
CNI 插件(如 Flannel/Calico)的核心工作就是:
- 在宿主机上创建一个虚拟网桥(如
cni0或docker0)。 - 每当有新 Pod 创建,CNI 就会拉一根 Veth Pair,“网线”一头插在 Pod 的 NetNS 里(一般重命名为
eth0),另一头插在宿主机的网桥上。 - 跨主机的通信,则通过路由表、VxLAN 隧道或者 BGP 协议,把数据包从宿主机的物理网卡发送出去。
四、 SRE 实战:如何“潜入” Pod 的网络空间抓包排障?
在日常排查 K8s 网络故障时(比如网络抖动、DNS 解析失败),我们经常需要在 Pod 内抓包。但是为了保证容器镜像的安全与轻量化,生产环境的容器里通常既没有 tcpdump,也没有 iproute2 工具。
怎么办?利用 NetNS 的隔离特性,我们可以直接在宿主机上,进入该 Pod 的网络空间进行抓包和调试!
这是一个非常实用的高级排障技巧:
步骤 1:找到容器在宿主机上的 PID
通过容器运行时的工具(以 crictl 为例,docker 同理)获取容器的进程 ID(PID):
# 找到容器 ID
crictl ps | grep your-pod-name
# 获取容器的 PID
crictl inspect --output go-template='{{.info.pid}}' <CONTAINER-ID>
假设我们拿到的 PID 是 12345。
步骤 2:使用 nsenter 潜入网络空间
nsenter 是 Linux 提供的一个极其强大的调试工具,它可以让你“进入”指定进程的各种 Namespace。
# -t 12345: 指定目标进程 PID
# -n: 只进入网络命名空间(Network Namespace)
sudo nsenter -t 12345 -n ip link show
执行上面这条命令后,你会发现输出的网卡信息完全是该 Pod 内部的网卡,而不是宿主机的!
步骤 3:在宿主机上对 Pod 抓包
既然我们已经通过 nsenter 接入了 Pod 的网络,那我们就可以直接利用宿主机上功能完善的 tcpdump 抓取这个 Pod 的流量:
# 进入 Pod 的网络空间,并在其 eth0 网卡上抓包,将数据保存到宿主机
sudo nsenter -t 12345 -n tcpdump -i eth0 -w /tmp/pod_traffic.pcap
不需要在容器里安装任何脏工具,优雅、安全、高效。
总结
Kubernetes 网络看似庞大复杂,但扒开 CNI 的外衣,底层的核心依然是 Linux 原生的 NetNS 和 Veth Pair。
- NetNS 实现了网络栈的隔离。
- Pause 容器 实现了 Pod 内多容器的网络共享。
- CNI 插件 充当了网线的铺设工和路由器的搬运工。
搞懂了这些,下次不管是排查 K8s 的网络丢包,还是去卷 CNI 的源码,你心里都有底了。