WEBKT

一文读懂 K8s 容器网络命名空间:为什么说 NetNS 才是容器网络的基石

2 0 0 0

很多人学 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,分别命名为 ns1ns2

# 创建命名空间
sudo ip netns add ns1
sudo ip netns add ns2

# 查看已创建的命名空间
ip netns list

此时,ns1ns2 就像两台断网的孤立主机。你可以尝试在 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 容器)的隐藏角色。

  1. Pause 容器启动后,第一件事就是创建并占住一个 NetNS。
  2. 随后启动的业务容器,都会通过 ContainerdDockerjoin 方式,强制加入到这个 Pause 容器的 NetNS 中。
  3. 也就是说,一个 Pod 里只有一张虚拟网卡,大家共用,IP 自然也是同一个。

2. CNI 插件到底在干嘛?

在上面的实验中,我们用一条 Veth Pair 连接了两个空间。但在实际生产中,一台物理机上可能有上百个 Pod,两两配对建 Veth Pair 显然不现实(复杂度是 $O(N^2)$)。

这时候就需要一个“交换机”。在单机上,这个交换机叫 Bridge(网桥)
CNI 插件(如 Flannel/Calico)的核心工作就是:

  1. 在宿主机上创建一个虚拟网桥(如 cni0docker0)。
  2. 每当有新 Pod 创建,CNI 就会拉一根 Veth Pair,“网线”一头插在 Pod 的 NetNS 里(一般重命名为 eth0),另一头插在宿主机的网桥上。
  3. 跨主机的通信,则通过路由表、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 原生的 NetNSVeth Pair

  • NetNS 实现了网络栈的隔离。
  • Pause 容器 实现了 Pod 内多容器的网络共享。
  • CNI 插件 充当了网线的铺设工和路由器的搬运工。

搞懂了这些,下次不管是排查 K8s 的网络丢包,还是去卷 CNI 的源码,你心里都有底了。

运维极客 Kubernetes容器网络

评论点评