容器网络看起来复杂,但拆到底层只有四个构件:网络命名空间(netns)提供隔离,veth pair 提供连线,Linux bridge 提供二层交换,iptables/NAT 提供三层出网。Kubernetes 的 CNI 插件、Docker 的 bridge 网络、Podman 的 pod,无非是这四块积木的不同拼法。
本文先讲清每块积木的语义,再用手工命令从零搭出一个「容器能访问外网、外网能访问容器」的最小网络,最后说明 CNI 插件在这套机制上加了什么。理解了这些,再回头看 Calico、Flannel、Cilium 的差异就会清晰很多。
一、网络命名空间:隔离的边界
1.1 命名空间隔离了什么
网络命名空间(network namespace)是内核提供的一整套独立网络栈:每个命名空间有自己的网卡、路由表、ARP 表、iptables 规则、socket 与端口空间。默认情况下,进程运行在初始命名空间(root netns)里,也就是宿主机的网络栈。
每个 netns 独立拥有:
- 网络设备(lo、eth0、veth 等)
- IPv4/IPv6 路由表
- ARP / 邻居表
- iptables / nftables 规则
- socket 与端口号空间(两个 netns 可各用 80 端口)
- /proc/net 下的网络统计
不隔离的:文件系统、进程、用户(这些由其他命名空间负责)
1.2 用 ip netns 操作
ip netns 是操作命名空间的主力工具。它在 /var/run/netns/ 下为每个命名空间创建挂载点,使命名空间可以持久化并命名。
# 创建并命名一个网络命名空间
ip netns add ns1
# 进入命名空间执行命令
ip netns exec ns1 ip addr show
ip netns exec ns1 ip route
# 打开一个 shell 在里面操作
ip netns exec ns1 bash
# 列出所有命名空间,删除
ip netns list
ip netns delete ns1
# 查看进程所属的命名空间
ls -l /proc/<pid>/ns/net
1.3 命名空间的生命周期
命名空间本身没有名字,是 ip netns 用挂载点赋予了它名字。只要还有进程或挂载点引用它,命名空间就存活;全部引用消失后自动销毁,其中的网卡、规则一并消失。容器运行时正是靠「进程退出 → 命名空间引用归零 → 网络资源自动回收」来避免资源泄漏。
二、veth pair:成对的虚拟网线
2.1 veth 的语义
veth(virtual ethernet)总是成对出现,像一根双向水管:从一端进入的数据包,会从另一端出来。它本身不做任何转发决策,只是把两端连起来。把一端留在宿主机的 bridge 上,另一端塞进容器的命名空间,容器就「插」上了宿主机的虚拟交换机。
veth pair 语义:
veth0 <=============> veth1
从 veth0 发出的包 → 从 veth1 出来
从 veth1 发出的包 → 从 veth0 出来
两端可以位于不同命名空间 → 实现跨命名空间连线
# 创建一对 veth
ip link add veth0 type veth peer name veth1
# 把 veth1 移入 ns1 命名空间
ip link set veth1 netns ns1
# 在 ns1 内看到 veth1 并配 IP
ip netns exec ns1 ip addr add 10.200.0.2/24 dev veth1
ip netns exec ns1 ip link set veth1 up
ip netns exec ns1 ip link set lo up
# 宿主机侧配 IP 并启用
ip addr add 10.200.0.1/24 dev veth0
ip link set veth0 up
2.2 只靠 veth 的两点直连
仅用一对 veth 就能让宿主机与命名空间互通:宿主机在 veth0 配 10.200.0.1,命名空间在 veth1 配 10.200.0.2,双方互 ping 即通。但这只支持「一对一」,要让多个命名空间互访,就需要 bridge。
# 直连测试
ip netns exec ns1 ping -c 2 10.200.0.1
ping -c 2 10.200.0.2
三、Linux bridge:虚拟二层交换机
3.1 bridge 的作用
Linux bridge 是一个软件二层交换机:它按 MAC 地址转发帧,维护 MAC 地址表,支持泛洪。把多个 veth 的宿主机端都挂到同一个 bridge 上,所有命名空间就处于同一个二层广播域,可以互相通信。
bridge 转发逻辑:
1. 收到帧 → 学习源 MAC 与入端口
2. 查 MAC 表:命中则从对应端口转发(单播)
3. 未命中或广播 → 从除入端口外的所有端口泛洪
4. 表项老化(默认 300s)后重新学习
# 创建 bridge 并启用
ip link add br0 type bridge
ip link set br0 up
ip addr add 10.200.0.1/24 dev br0
# 把多个 veth 宿主机端挂到 bridge
ip link set veth0 master br0
ip link set vethA master br0
ip link set vethB master br0
# 查看 bridge 与 MAC 表
bridge link show
bridge fdb show br br0
3.2 为什么容器桥接后要设网关
容器(命名空间)里的默认路由要指向 bridge 的 IP(如 10.200.0.1)。这样,目标不在同网段的包会发给 bridge,由宿主机做三层转发。忘记设默认路由是「同网段能通、跨网段不通」的典型原因。
# 命名空间内设置默认路由指向 bridge
ip netns exec ns1 ip route add default via 10.200.0.1
# 同网段容器互访走二层(bridge 直接转发)
# 跨网段/出网走三层(宿主机转发 + NAT)
四、iptables/NAT:出网与端口映射
4.1 出网需要 SNAT/MASQUERADE
容器用的是私有网段(如 10.200.0.0/24),出公网时源地址必须被改写成宿主机 IP,否则回包找不到路。这就是 MASQUERADE:把容器源地址动态改写为出接口地址。
# 开启转发
sysctl -w net.ipv4.ip_forward=1
# 容器出网:对来自容器网段的流量做源地址伪装
iptables -t nat -A POSTROUTING -s 10.200.0.0/24 ! -o br0 -j MASQUERADE
# 放行转发
iptables -A FORWARD -i br0 -j ACCEPT
iptables -A FORWARD -o br0 -j ACCEPT
4.2 端口映射需要 DNAT
外网要访问容器里的服务,需要把宿主机某端口的流量改写目标为容器地址,这就是 DNAT(通常配合 -p tcp --dport)。
# 把宿主机 8080 映射到容器 10.200.0.2:80
iptables -t nat -A PREROUTING -p tcp --dport 8080 \
-j DNAT --to-destination 10.200.0.2:80
# 注意:从容器自身所在宿主机访问时,还需 OUTPUT 链的 DNAT
iptables -t nat -A OUTPUT -p tcp --dport 8080 \
-j DNAT --to-destination 10.200.0.2:80
4.3 报文路径与 conntrack
iptables 依赖连接跟踪(conntrack)记住每条流的 NAT 映射,回包才能被正确改写。排错时 conntrack -L 能看到活跃连接及其 NAT 状态,是定位「单向通」问题的关键。
| 阶段 | 链 | 典型动作 |
|---|---|---|
| 入站路由前 | PREROUTING | DNAT(端口映射) |
| 转发决策 | FORWARD | 放行/丢弃 |
| 出站路由后 | POSTROUTING | SNAT/MASQUERADE |
| 本机进程 | INPUT/OUTPUT | 本机访问策略 |
# 查看 NAT 连接跟踪表
conntrack -L | grep 10.200.0.2
# 统计某条链的命中
iptables -t nat -L POSTROUTING -v -n
五、CNI:把手工步骤自动化
5.1 CNI 规范做什么
容器编排系统不自己实现网络,而是调用 CNI(Container Network Interface)插件。CNI 定义了一个简单契约:容器运行时创建网络命名空间后,调用插件二进制,通过环境变量与 JSON 配置传入「容器 ID、netns 路径、要执行的命令(ADD/DEL/CHECK)」,插件负责把网卡、IP、路由、规则配好并返回结果。
CNI 调用约定:
环境变量:
CNI_COMMAND = ADD | DEL | CHECK | VERSION
CNI_CONTAINERID, CNI_NETNS, CNI_IFNAME
CNI_ARGS, CNI_PATH
stdin:网络配置 JSON(插件名、子网、网关等)
stdout:结果 JSON(分配的 IP、网关、DNS)
{
"cniVersion": "1.0.0",
"name": "mynet",
"type": "bridge",
"bridge": "br0",
"isGateway": true,
"ipMasq": true,
"subnet": "10.200.0.0/24",
"ipam": {
"type": "host-local",
"ranges": [[{ "subnet": "10.200.0.0/24" }]]
}
}
5.2 bridge 插件做了什么
标准 bridge 插件做的事,正是前面手工步骤的自动化:创建 bridge(若不存在)、创建 veth pair、把一端挂到 bridge、另一端放进容器 netns、配 IP 与路由、按需加 iptables 规则。host-local IPAM 插件负责从子网里分配不冲突的 IP。
5.3 不同 CNI 的差异在哪
| CNI | 数据面 | 特点 |
|---|---|---|
| bridge + host-local | bridge + veth + iptables | 最简单,单机模型 |
| Calico | 路由/BGP 或 eBPF | 无 overlay,走三层路由 |
| Flannel | VXLAN overlay | 简单易用,跨节点隧道 |
| Cilium | eBPF | 高性能,替代 iptables |
无论哪种,底层都离不开 netns、veth、路由与 NAT 这几块积木;差异在于「跨节点怎么连」——是路由、是隧道,还是 eBPF 重写数据面。想深入跨节点隧道可以看 Overlay 网络与 VXLAN/Geneve ,想了解 eBPF 重写数据面可以看 eBPF 与 XDP 数据面 。
六、手工搭一套最小容器网络
6.1 完整脚本
把前面的步骤串起来,就是一套能跑的最小容器网络:命名空间出网、外网访问容器、容器间互访。
set -e
# 1. 建 bridge
ip link add br0 type bridge
ip addr add 10.200.0.1/24 dev br0
ip link set br0 up
# 2. 建两个命名空间并连线
for i in 1 2; do
ip netns add ns$i
ip link add veth$i type veth peer name eth0 netns ns$i
ip link set veth$i master br0
ip link set veth$i up
ip netns exec ns$i ip addr add 10.200.0.$((i+1))/24 dev eth0
ip netns exec ns$i ip link set eth0 up
ip netns exec ns$i ip link set lo up
ip netns exec ns$i ip route add default via 10.200.0.1
done
# 3. 出网 NAT
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 10.200.0.0/24 ! -o br0 -j MASQUERADE
iptables -A FORWARD -i br0 -j ACCEPT
iptables -A FORWARD -o br0 -j ACCEPT
6.2 验证清单
| 验证项 | 命令 | 预期 |
|---|---|---|
| 容器互访 | ip netns exec ns1 ping 10.200.0.2 | 通(二层经 bridge) |
| 访问宿主机 | ip netns exec ns1 ping 10.200.0.1 | 通 |
| 访问外网 | ip netns exec ns1 ping 1.1.1.1 | 通(经 NAT) |
| 端口映射 | 宿主机 curl localhost:8080 | 命中容器服务 |
| 命名空间内规则 | ip netns exec ns1 iptables -L | 独立于宿主机 |
6.3 常见故障对照
| 现象 | 原因 | 排查 |
|---|---|---|
| 容器间不通 | 未挂到同一 bridge | bridge link show |
| 出网不通 | 未开转发或 NAT 规则缺失 | sysctl net.ipv4.ip_forward、iptables -t nat -L |
| 单向不通 | 回包路径被 FORWARD 拦 | conntrack -L、FORWARD 计数 |
| 端口映射从宿主机访问失败 | 缺 OUTPUT 链 DNAT | 补 -A OUTPUT 规则 |
| 命名空间残留 | 引用未清理 | ip netns list、检查 /var/run/netns |
6.4 veth 的性能特征与卸载
veth 是纯软件设备,转发要经过内核网络栈,因此开销高于物理网卡直连。但它支持 GRO/GSO/TSO 等卸载标志,能让大包在进入栈前被聚合,显著提升吞吐。排错时用 ethtool -k 确认卸载状态,用 ip -s link 看收发统计。
# 查看 veth 的卸载特性
ethtool -k veth0 | grep -E 'generic|tcp-segmentation'
# 查看收发字节、丢包、错误计数
ip -s link show veth0
ip -s link show br0
# 用 netns 内的命名空间做带宽测试
ip netns exec ns1 iperf3 -s &
iperf3 -c 10.200.0.2
| 现象 | 可能原因 | 排查 |
|---|---|---|
| 吞吐远低于预期 | 卸载未生效 | ethtool -k 对比物理网卡 |
| 大量丢包 | 队列溢出 | ip -s link 看 dropped |
| CPU 软中断高 | 单队列、无 RPS/RSS | 配置 RPS 或改用多队列 |
6.5 CNI 的 IPAM 与多网卡
CNI 不只有「连线」,还负责地址分配(IPAM)。host-local 用本地文件记录已分配 IP,适合单机;跨节点需要 calico-ipam 等分布式方案,避免不同节点分到相同网段。多网卡场景(如 SR-IOV、Macvlan)则通过多个 CNI 配置串行调用,为容器挂上多张网卡。
IPAM 方案对比:
host-local : 单机文件记录,简单,不跨节点
dhcp : 向 DHCP 服务器申请,适合传统网络
calico-ipam: 从集群地址池分配,跨节点协调
静态 : 配置里写死,适合固定拓扑
{
"cniVersion": "1.0.0",
"name": "multus-demo",
"plugins": [
{ "type": "bridge", "bridge": "br0",
"ipam": { "type": "host-local", "subnet": "10.200.0.0/24" } },
{ "type": "macvlan", "master": "eth1",
"ipam": { "type": "host-local", "subnet": "10.201.0.0/24" } }
]
}
串行插件(如 Multus)把多张网卡依次挂到同一命名空间:第一张作主网卡承载默认路由,其余作辅助网卡承载存储或管理流量,是高性能场景的常见做法。
七、小结
| 构件 | 职责 | 关键命令 |
|---|---|---|
| netns | 隔离网络栈 | ip netns add/exec |
| veth | 成对连线 | ip link add ... type veth |
| bridge | 二层交换 | ip link add ... type bridge |
| iptables | 三层转发与 NAT | -t nat POSTROUTING |
| CNI | 自动化上述步骤 | 插件二进制 + JSON 配置 |
容器网络没有魔法:它把「隔离、连线、交换、转发」四件事用 Linux 原语表达出来,再交给 CNI 插件自动化。理解这套底层,排错时就不必在抽象层里猜——可以逐层 ip link、bridge fdb、iptables -t nat -L 地验证。要把单机模型扩展到集群,可继续阅读 云原生网络与 CNI
;三层转发的完整决策过程可参考 IP 路由与转发
。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。