深入理解容器网络:从Docker的桥接到K8s的CNI

大家好,我是33blog的博主。今天我们来聊聊容器网络这个既基础又核心的话题。相信很多朋友在从使用Docker Compose编排几个容器,转向使用Kubernetes管理大规模集群时,都曾被其复杂的网络模型“折磨”过。为什么Pod内容器能直接通信?Service的ClusterIP到底是个啥?Calico、Flannel这些插件又在背后做了什么?今天,我就结合自己的实战和踩坑经验,带大家一层层剥开容器网络的面纱。
一、基石:Docker的四大网络模式
理解K8s网络前,必须先搞懂Docker的网络模式,因为它是容器网络的起点。Docker主要提供了四种网络模式,我们可以通过 --network 参数指定。
1. Bridge模式(默认)
这是最常用、也是最经典的模式。Docker守护进程会创建一个名为 docker0 的虚拟网桥,所有默认创建的容器都会连接到这个网桥上。
实战与踩坑:当你运行一个容器时,比如 docker run -d --name web nginx,Docker会从 docker0 的子网(通常是172.17.0.0/16)中分配一个IP给容器。容器间可以通过这个IP直接通信,但对外部网络而言,它们需要通过 docker0 网桥和宿主机的iptables NAT规则进行端口映射。
# 查看docker0网桥和容器的网络信息
$ ip addr show docker0
$ docker inspect web | grep -A 10 "NetworkSettings"
# 运行两个容器,测试桥接网络互通
$ docker run -d --name container1 alpine sleep 3600
$ docker run -it --name container2 alpine sh
/ # ping container1的IP地址 # 可以ping通
注意:默认的桥接网络下,容器间只能用IP通信,不能用容器名。要使用容器名,需要创建用户自定义的桥接网络(docker network create my-net)。
2. Host模式
使用 --network=host 后,容器将不会获得独立的Network Namespace,而是直接使用宿主机的网络栈。这意味着容器的IP就是宿主机的IP,端口也共用。
# 在容器内看到的网络接口和宿主机完全一样
$ docker run --network=host -it alpine ip addr
踩坑提示:性能最好,但完全失去了网络隔离,端口冲突的风险大大增加。在生产环境需谨慎使用。
3. Container模式
使用 --network=container:<容器名>,新创建的容器会与一个已存在的容器共享Network Namespace。K8s Pod内多个容器的网络原理正是基于此!
# 先启动一个容器
$ docker run -d --name web nginx
# 再启动一个容器,共享web容器的网络栈
$ docker run -it --network=container:web alpine sh
/ # wget -qO- localhost # 可以直接访问到nginx服务
4. None模式
使用 --network=none,容器拥有自己的Network Namespace,但Docker不会对其进行任何网络配置。它只有一个lo回环接口,需要用户手动配置网络。
二、进化:Kubernetes的网络模型与核心要求
K8s在Docker的基础上提出了更抽象、更统一的网络模型,核心要求可以概括为三点:
- Pod间直接通信:每个Pod都有一个独立的IP(Pod IP),集群内任意两个Pod之间可以直接通过这个IP通信,无需NAT。
- Pod与Service通信:Pod可以通过稳定的Service名称(对应ClusterIP)访问到后端的Pod,这个通信通常需要经过kube-proxy设置的iptables或ipvs规则进行负载均衡和NAT。
- Node与Pod通信:Node上的进程(或主机)可以直接通过Pod IP访问到所有Pod。
为了实现这个模型,K8s自己并不直接动手,而是定义了一个标准接口——CNI(Container Network Interface)。
三、灵魂:CNI插件工作原理剖析
CNI插件才是K8s集群网络的真正实现者。它的工作流程可以概括为:当kubelet创建Pod的“基础设施容器”(pause容器)后,会根据配置调用指定的CNI插件,为这个Pod的Network Namespace配置网络。
一个简化的工作流程:
- 调用:kubelet通过CRI(容器运行时接口)创建pause容器,获得其Network Namespace路径(如
/proc/12345/ns/net)。 - 配置:kubelet读取CNI配置文件(默认在
/etc/cni/net.d/),并调用对应的CNI二进制文件(默认在/opt/cni/bin/),将容器ID、Namespace路径等信息作为参数传入。 - 执行:CNI插件(如bridge、host-local、loopback)执行具体的网络配置操作,比如创建veth pair、连接网桥、分配IP等。
- 返回:CNI插件将配置结果(如分配的IP地址、网关、路由)以JSON格式返回给kubelet。
以Flannel的vxlan模式为例
Flannel是一个经典的覆盖网络(Overlay Network)插件。它会在每个Node上运行一个flanneld守护进程,并创建一个名为 flannel.1 的VXLAN隧道设备。
# 在K8s Node上查看网络设备,通常能看到类似如下输出
$ ip addr show
...
3: docker0: ...
10: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 ...
link/ether ... brd ff:ff:ff:ff:ff:ff
inet 10.244.1.0/32 scope global flannel.1 # 这是本节点的Pod子网网段
...
工作原理:
- Flannel会为整个集群分配一个大的Pod网段(如10.244.0.0/16),并为每个Node分配一个子网(如Node1: 10.244.1.0/24)。
- 当Pod A(10.244.1.2)要访问Pod B(10.244.2.3)时,数据包通过cni0网桥(或docker0,取决于配置)到达宿主机。
- 宿主机路由表会指示,前往10.244.2.0/24的流量需要从
flannel.1设备发出。 flannel.1将原始数据包封装进一个UDP包(VXLAN封装),外层目的IP是Pod B所在Node的IP。- 对端Node的
flannel.1设备收到UDP包后,解封装,取出内部的Pod数据包,再通过本机的cni0网桥送给Pod B。
踩坑提示:VXLAN封装会导致MTU减小,如果遇到一些网络性能问题或奇怪的不通,记得检查MTU设置。Calico的IPIP模式也有类似问题。
另一个主流:Calico的BGP模式
Calico则采用了完全不同的思路。它不使用Overlay隧道,而是将每个Node都当作一个网络路由器,通过BGP协议在Node间同步路由信息。
# 在启用Calico的Node上,你可以看到到每个其他Node Pod网段的路由
$ ip route
...
10.244.2.0/24 via 192.168.1.102 dev eth0 proto bird # 直接指向了Node2的物理IP
...
工作原理:
- 每个Node上的calico-node组件会通过BGP协议,向邻居(其他Node或物理路由器)宣告:“我这里有网段10.244.1.0/24,下一跳是我自己(Node1的IP)”。
- 于是,所有Node的路由表中都拥有了到达集群内所有Pod子网的路由。
- Pod A访问Pod B时,数据包根据宿主机的路由表,直接通过底层网络(不封装)发送到Pod B所在的Node,再由该Node转发给Pod。
优势与选择:Calico BGP模式性能更好(无封装开销),但要求底层网络能够承载Pod网络的路由信息,适合在自有数据中心或云环境支持BGP的场景。Flannel vxlan则更通用,对底层网络无要求。
四、动手实验:感受CNI的配置过程
要真正理解,最好的办法就是动手。我们可以在一个没有K8s的环境下,手动使用CNI工具为一个网络命名空间配置网络,模拟kubelet的动作。
# 1. 安装CNI工具(以二进制文件为例)
$ wget https://github.com/containernetworking/plugins/releases/download/v1.3.0/cni-plugins-linux-amd64-v1.3.0.tgz
$ mkdir -p /opt/cni/bin
$ tar -C /opt/cni/bin -xzf cni-plugins-linux-amd64-v1.3.0.tgz
# 2. 创建CNI配置文件
$ mkdir -p /etc/cni/net.d
$ cat <<EOF > /etc/cni/net.d/10-mynet.conf
{
"cniVersion": "0.4.0",
"name": "mynet",
"type": "bridge",
"bridge": "cni0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.22.0.0/16",
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
}
EOF
# 3. 创建一个网络命名空间(模拟pause容器)
$ ip netns add test-ns
# 4. 使用CNI工具配置这个命名空间
$ CNI_PATH=/opt/cni/bin NETCONFPATH=/etc/cni/net.d /opt/cni/bin/cnitool add mynet /var/run/netns/test-ns
# 5. 查看配置结果
$ ip netns exec test-ns ip addr show # 应该能看到分配的IP
$ ip netns exec test-ns ping -c 2 8.8.8.8 # 测试网络连通性
# 6. 清理
$ CNI_PATH=/opt/cni/bin NETCONFPATH=/etc/cni/net.d /opt/cni/bin/cnitool del mynet /var/run/netns/test-ns
$ ip netns delete test-ns
通过这个实验,你能清晰地看到,CNI插件就是一个标准的、可执行的黑盒,kubelet只负责调用它并传入参数,剩下的网络细节完全由插件决定。
总结
容器网络从Docker单一的桥接模式,发展到K8s抽象出的Pod统一IP模型,再通过CNI接口将实现完全插件化、解耦,体现了云原生设计的高明之处。理解Docker网络是基础,理解K8s网络模型是目标,而理解CNI插件(无论是Flannel、Calico还是Cilium)的工作原理,则是打通任督二脉的关键。希望这篇剖析能帮助你下次在排查网络问题时,不再是盲目地重启Pod或节点,而是能有方向地去查看路由表、CNI配置和插件日志。网络之路漫漫,我们一起踩坑,一起成长。


Docker bridge模式下容器名不通这事坑过我好几次,得自己建自定义网络才行