深入理解容器网络:Docker网络模式与K8s CNI插件工作原理剖析

2025.12.30 奇思妙想 764
33BLOG智能摘要
从Docker的桥接网络到Kubernetes的CNI插件,你是否曾被复杂的容器网络模型困扰过?为什么Pod内的容器能直接通信?神秘的ClusterIP背后隐藏着什么机制?Calico和Flannel这些网络插件究竟在底层做了什么?本文将带你层层剖析容器网络的核心原理,从四大Docker网络模式的实战演示,到K8s网络模型的三大核心要求,再到CNI插件的工作流程详解。通过亲手操作CNI配置实验,你将彻底理解数据包在容器集群中的完整旅程,下次排查网络问题时不再盲目重启,而是能精准定位路由表、CNI配置和插件日志中的关键线索。
— 此摘要由33BLOG基于AI分析文章内容生成,仅供参考。

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

深入理解容器网络: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的基础上提出了更抽象、更统一的网络模型,核心要求可以概括为三点:

  1. Pod间直接通信:每个Pod都有一个独立的IP(Pod IP),集群内任意两个Pod之间可以直接通过这个IP通信,无需NAT。
  2. Pod与Service通信:Pod可以通过稳定的Service名称(对应ClusterIP)访问到后端的Pod,这个通信通常需要经过kube-proxy设置的iptables或ipvs规则进行负载均衡和NAT。
  3. Node与Pod通信:Node上的进程(或主机)可以直接通过Pod IP访问到所有Pod。

为了实现这个模型,K8s自己并不直接动手,而是定义了一个标准接口——CNI(Container Network Interface)。

三、灵魂:CNI插件工作原理剖析

CNI插件才是K8s集群网络的真正实现者。它的工作流程可以概括为:当kubelet创建Pod的“基础设施容器”(pause容器)后,会根据配置调用指定的CNI插件,为这个Pod的Network Namespace配置网络。

一个简化的工作流程

  1. 调用:kubelet通过CRI(容器运行时接口)创建pause容器,获得其Network Namespace路径(如 /proc/12345/ns/net)。
  2. 配置:kubelet读取CNI配置文件(默认在 /etc/cni/net.d/),并调用对应的CNI二进制文件(默认在 /opt/cni/bin/),将容器ID、Namespace路径等信息作为参数传入。
  3. 执行:CNI插件(如bridge、host-local、loopback)执行具体的网络配置操作,比如创建veth pair、连接网桥、分配IP等。
  4. 返回: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子网网段
...

工作原理

  1. Flannel会为整个集群分配一个大的Pod网段(如10.244.0.0/16),并为每个Node分配一个子网(如Node1: 10.244.1.0/24)。
  2. 当Pod A(10.244.1.2)要访问Pod B(10.244.2.3)时,数据包通过cni0网桥(或docker0,取决于配置)到达宿主机。
  3. 宿主机路由表会指示,前往10.244.2.0/24的流量需要从 flannel.1 设备发出。
  4. flannel.1 将原始数据包封装进一个UDP包(VXLAN封装),外层目的IP是Pod B所在Node的IP。
  5. 对端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
...

工作原理

  1. 每个Node上的calico-node组件会通过BGP协议,向邻居(其他Node或物理路由器)宣告:“我这里有网段10.244.1.0/24,下一跳是我自己(Node1的IP)”。
  2. 于是,所有Node的路由表中都拥有了到达集群内所有Pod子网的路由。
  3. 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模式下容器名不通这事坑过我好几次,得自己建自定义网络才行