docker0 网桥:容器网络的内核拼图
调试容器网络问题的时候,大多数人习惯先跑 docker inspect,看一眼 IP,ping 一下,然后开始凭感觉排查。这个方式不是不行,只是效率很低。
真正有效的调试姿势,是理解 Docker 的默认网络模型建立在哪几块 Linux 内核原语上,以及这些原语之间是怎么拼在一起的。docker0 网桥是一个很好的入口。
三块拼图
Docker 默认网络模式(bridge 模式)本质上是三个 Linux 内核机制的组合:网络命名空间、veth 对、Linux 网桥,加上 iptables 做 NAT。
网络命名空间把容器的网络视图完全隔离开。容器里的 eth0 和宿主机的 eth0 是两个完全独立的接口,有各自的路由表、iptables 规则、socket 空间。从内核角度看,它们活在不同的宇宙里。
veth 对是连接这两个宇宙的管道。veth 是虚拟以太网设备,总是成对创建,两端强绑定:往一端写数据,另一端立刻收到,就像一根虚拟网线。Docker 启动容器时,会创建一对 veth,把一端放进容器命名空间(重命名为 eth0),另一端留在宿主机命名空间(命名为 vethXXXXXX)。
docker0 网桥是宿主机侧的汇聚点。Linux 网桥工作在 L2,维护一张 MAC 地址转发表(FDB),学习每个端口的 MAC 地址。所有容器的 vethXXXXXX 接口都被挂到这个网桥上,容器之间的流量直接在 L2 转发,不经过 IP 路由,延迟低,开销小。
用 ip 命令可以直接观察这个结构:
1 | |
出站流量:iptables 的 MASQUERADE
容器发往外网的流量,要经过一个额外的步骤:SNAT(源地址转换)。容器的 IP 是私有地址(默认 172.17.0.0/16),直接发出去外面不认。
Docker 在宿主机的 iptables 里写了一条 MASQUERADE 规则:
1 | |
这条规则的意思是:源地址在 172.17.0.0/16,目的地址在这个子网之外的包,出去时把源 IP 替换成宿主机的出口 IP。回包按 NAT 连接跟踪表还原目标,转发回对应容器。
理解这一点很重要:如果你的宿主机没有开启 ip_forward,或者 iptables 规则被其他工具(比如某些防火墙管理脚本)清空,容器的出网就会静默失败。排查时第一步应该是:
1 | |
端口映射:另一侧的 DNAT
-p 8080:80 的背后同样是 iptables,方向反过来:DNAT(目的地址转换)。
1 | |
流量到达宿主机的 8080 端口时,被改写目标地址,转发到容器的 80 端口。这个机制在大量端口映射的场景下会有性能问题,因为每个连接都要过一遍 NAT 表——这也是为什么生产环境里一般不用端口映射,而是用 CNI 插件配合 Service/LoadBalancer 来暴露服务。
这个模型的边界
bridge 模式在单机上工作得很好,但有两个内在限制:
第一,广播域。所有接到 docker0 的容器在同一个 L2 广播域里。容器数量上来之后,ARP 广播会成为噪音,更重要的是安全边界变弱——同一宿主机上的容器默认可以相互访问,需要额外的网络策略来隔离。
第二,单机边界。veth + bridge 的结构无法跨越宿主机。多机场景需要 overlay 网络(Flannel、Calico 等 CNI 插件),本质上是在 bridge 模型的基础上,用 VXLAN 或 BGP 把 L2 帧封装着跨机器传输,或者直接在 L3 路由层解决问题。
Kubernetes 把默认网络完全交给 CNI 插件,不依赖 docker0,原因就在这里——bridge 模式不够用了。
理解 docker0 的价值不在于能背出这套模型,而在于调试网络问题时不再是凭直觉猜。网络不通,先看 veth 对是否存在,看 bridge 挂载是否正确,看 iptables 规则是否完整——这三步能定位掉 80% 的容器网络问题。