Docker网络完全指南:从bridge到自定义驱动,一次理清容器通信与隔离
上周帮一个朋友排查容器环境偶发超时的问题查到最后才发现根源竟然很尴尬两个本该完全隔离的项目环境被放在了同一个默认 bridge 网络里业务之间一抢带宽整个集群就跟着抖。这件事让我下定决心把 Docker 网络这部分彻底理清楚——隔离网络怎么做、路由表怎么读、iptables 到底拦截了什么、自定义网络驱动是怎么挂上去的。这篇文章就是我踩完一轮坑之后的完整复盘适合已经能熟练使用 Docker 基础命令、正准备迈向网络进阶的开发者、运维和 SRE 参考。先说结论Docker 的网络并不复杂但它把 Linux 内核里最要命的几样东西——网络命名空间、veth pair、网桥、路由、iptables 全部组合了起来。你只要把这几块拼图看懂绝大多数网络问题都能自己定位而不是靠重启容器碰运气。1. 为什么值得花时间搞懂Docker网络1.1 一次线上事故两个环境竟然互通了我之前在某公司的测试环境里遇到过一次诡异故障开发环境有一个定时任务每次执行都会把一批数据写到测试库而测试环境的另一个服务又会莫名其妙收到大量本应发给生产环境的请求。一开始大家怀疑是代码配置写错了翻了几圈配置中心都没发现问题。后来我拉出所有容器的 IP逐个对照才发现开发环境和测试环境用的都是默认 bridge 网络网段都是 172.17.0.0/16两边的容器 IP 互相可以 ping 通端口也能直连。这种假隔离在规模小的时候完全看不出来一旦容器数量一多IP 冲突、端口占用、流量互相干扰就全来了。我当时就在想如果从一开始就用 Docker 提供的自定义网络隔离能力这种事根本不会发生。所以对任何要上生产环境的人来说Docker 网络不是一个有空再学的知识点而是迟早要补的必修课。1.2 Docker网络模型的本质CNM三件套要理解 Docker 网络先记住三个核心概念它们来自 Docker 官方的 CNMContainer Network Model模型Sandbox沙箱可以理解成一个容器独享的网络堆栈包含网卡、路由表、iptables 规则。在 Linux 里它对应的就是网络命名空间network namespace。容器之所以能拥有独立的 IP就是因为每个容器都有自己的一份路由表和防火墙规则。Endpoint接入点容器内网卡和某个网络之间的连接点。典型体现就是容器里的 eth0以及它在宿主机上对应的一根 veth 虚拟网线。Network网络一组 Endpoint 的集合网络内部的 Endpoint 可以直接通信网络之间默认隔离。然后就是驱动。Docker 官方自带了好几种内置驱动bridge、host、none、overlay、macvlan、ipvlan。最常用的是 bridge 和 overlay。你打开 Docker 文档能看到的默认网络就只有三条bridge、host、none其中 bridge 就是默认驱动对应宿主机上的 docker0 网桥。理解这套模型最大的好处是你再看到docker network create -d bridge xxx的时候就知道它实际上是在宿主机上创建了一个新的 Linux bridge然后把容器的 veth 网线插到这个 bridge 上。后面所有通不通的问题本质都是路由表和防火墙规则的问题。2. 隔离网络实战从默认bridge到自定义网络2.1 为什么默认bridge满足不了生产环境很多人前期用 Docker 都是直接docker run容器落进默认的 bridge 网络里。这个网络能通信但代价不小完全没有隔离。只要在同一个宿主机上任何容器用 IP 都能访问其他容器不管你愿不愿意。没有服务发现。默认 bridge 里不提供 DNS 解析靠容器名访问是不行的早期只能靠--link单向注入环境变量和 hosts 记录维护成本极高。不可控。默认网络的网段、网关都是 Docker 自己分配的你没法指定容器 IP也没法决定子网大小。我见过最典型的翻车场景是两个项目组在同一台机器上做联调都偷懒用了默认 bridge结果两边部署的服务端口全是 8080每次联调都精准打到对方的服务上。用自定义网络把两个项目隔开这个问题的成本几乎为零只是很多人不知道。2.2 用三条命令创建隔离网络自定义网络的第一步是创建一个属于自己的 bridge 网络。建议指定子网和网关方便后面排查docker network create \ --driver bridge \ --subnet 172.20.0.0/16 \ --gateway 172.20.0.1 \ prod-net这里我特意把子网选在 172.20.0.0/16而不是默认的 172.17.0.0/16是为了避开和 docker0 网段冲突。规划容器网段时一定要先看宿主机上已有的网段否则后面会出现路由碰撞这部分的坑我在第 5 节会专门展开。然后分别把容器挂到不同网络docker run -d --name app-a --network prod-net --ip 172.20.0.10 nginx docker run -d --name app-b --network prod-net --ip 172.20.0.11 nginx docker network create \ --driver bridge \ --subnet 172.21.0.0/16 \ --gateway 172.21.0.1 \ dev-net docker run -d --name app-c --network dev-net --ip 172.21.0.10 nginx现在验证一下隔离效果# 同一网络内可以通 docker exec app-a ping -c 2 172.20.0.11 # 不同网络之间不通 docker exec app-a ping -c 2 172.21.0.10实际执行时你会发现不同网络之间的 ping 会一直卡在那里直到超时。这就是我们要的效果业务上不相干的容器在网络上老死不相往来。如果想临时放行可以用docker network connect prod-net app-c把某个容器再接入另一个网络但除非有明确需要不建议这么干。2.3 为什么不同网络不通内核里的墙隔离不是靠魔法实现的它靠的是两层机制。第一层是网桥和路由。每个自定义 bridge 网络Docker 都会创建一个独立的 Linux bridge在宿主机上体现为br-xxxx接口。app-a 的 veth 网线插在 prod-net 对应的 bridge 上app-c 的 veth 网线插在 dev-net 对应的 bridge 上。两个网桥之间默认没有转发路径桥下接口之间不连通。第二层是 iptables。就算有人手动加路由让流量能走通Docker 还有一道专门的隔离墙挡在中间。你可以在宿主机上执行iptables -L -n -t filter | grep DOCKER-ISOLATION会看到 Docker 为隔离网段生成了一条条规则凡是源地址和目标地址分属不同 docker 网桥的流量一律 DROP。所以不同网络不通是被内核防火墙明确强制的结果不是碰巧。这里有一个很多人容易漏掉的点宿主机上 Linux 默认的 FORWARD 策略往往不是 ACCEPTDocker 会把自己的规则插在 FORWARD 链的最前面并把 Docker 管理的网段放行。如果你手动动过 iptables很容易把 Docker 的链搞乱后面第五节的排查顺序会提到这个。3. 路由表检查让数据包的走向一目了然3.1 拿到钥匙进入容器的网络命名空间排查网络问题第一件事就是看路由表但很多人只会用docker exec进容器执行route -n。这没问题不过有时候容器里的镜像精简到连 iproute2 都没装route 命令根本不存在。更稳的办法是在宿主机上直接进入容器的网络命名空间# 拿到容器主进程的 PID docker inspect app-a --format {{.State.Pid}} # 进入该 PID 的网络命名空间执行命令 nsenter -t PID -n route -n执行nsenter -t PID -n route -n后你会看到类似这样的输出Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 172.20.0.1 0.0.0.0 UG 0 0 0 eth0 172.20.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0解释一下这两条路由172.20.0.0/16这条是本网段直连路由目标是本网段的包直接走 eth0不需要经过网关。0.0.0.0这条是默认路由所有去往其他网段的包下一跳都是 172.20.0.1也就是这个 bridge 网络的网关。看路由表的意义在于当你发现容器能 ping 通本网段的另一个容器却 ping 不通另一台宿主机上的容器时问题往往就出在下一跳是谁、路由宣告到了哪里上面。很多跨主机网络的排查最后都会落到宿主机之间能不能路由到这个容器网段这一个问题上。3.2 宿主机视角网桥、veth和iptables回到宿主机上用ip link show能看到一大堆 vethxxx 和 br-xxxx这就是容器网络在宿主机侧的实体。再看iptables的两张表先看 filter 表iptables -L -n -t filter --line-numbers重点关注 FORWARD 链。Docker 容器之间的流量、容器访问宿主机外部的流量都会经过 FORWARD 链。Docker 会往前插入 DOCKER 和 DOCKER-USER 链DOCKER-USER 是留给你自定义规则的地方凡是放这里面的规则不会被 Docker 重启覆盖。再看 nat 表iptables -t nat -S一般在 POSTROUTING 链里能看到一条 Docker 自动加的 MASQUERADE 规则-A POSTROUTING -s 172.20.0.0/16 ! -o br-xxxx -j MASQUERADE这条规则的意思是容器访问外网的时候把源地址换成宿主机出口网卡的地址也就是 SNAT。没有这条规则容器发出的包到了外部网络回来的包根本找不到回去的路。这也是为什么有些环境里容器能互相访问却死活上不了外网十有八九就是 MASQUERADE 被清掉了。3.3 数据包从A到B的完整旅程把上面几块拼起来一次容器 A 访问容器 B 的完整链路是这样的容器 A 的应用发出请求目标 IP 是 172.20.0.11。容器 A 的协议栈查自己的路由表发现目标是本网段于是通过 eth0 直接发送同时查 ARP 表找 172.20.0.11 对应的 MAC。数据包通过容器 A 的 eth0进入 veth 虚拟网线到达宿主机上的 br-prod 网桥。网桥查 MAC 地址表发现目标 MAC 对应的是另一根 veth直接在内核里把包转发过去。数据包从 vethB 进入容器 B 的 eth0容器 B 的协议栈解析并接收。整个过程在到达网桥之后是二层转发根本不查 IP 路由表。但一旦目标 IP 不在同一个网段比如 172.21.0.10容器 A 就会把包交给默认网关 172.20.0.1。这个网关本质上是宿主机上的一个接口宿主机内核查自己的路由表发现目标网段虽然存在但对应的是另一个 bridge于是尝试转发结果被 DOCKER-ISOLATION 规则拦下直接 DROP。这就是跨网络不通在内核里的完整解释。排查的时候用tcpdump -i br-prod在宿主机上看包是否到达网桥再tcpdump -i vethB看是否进入目标容器每一步都能精确定位丢包点非常直观。4. 自定义网络驱动从看懂到写一个最小的驱动4.1 驱动到底是怎么被Docker调用的很多人看到自定义网络驱动就发怵其实它的原理并不神秘。Docker 的网络功能由 libnetwork 提供libnetwork 定义了一套 Driver 接口。内置的 bridge、overlay 也都是这套接口的具体实现。Docker daemon 启动时会扫描/run/docker/plugins/目录下的 socket 文件当用户执行docker network create -d xxx时daemon 就会通过 HTTP 协议调用 xxx 驱动提供的接口完成网络创建、端点创建、端点加入等操作。一个驱动至少要实现以下方法接口方法作用GetCapabilities向 Docker 声明自己的特性比如是否作用于整个集群CreateNetwork创建网络时被调用通常在这里记录网段信息DeleteNetwork删除网络时回收资源CreateEndpoint创建接入点核心动作是创建 veth pair 并分配 IPDeleteEndpoint释放接入点资源Join把 veth 的一端放进容器的网络命名空间配置 IP 和路由Leave从容器的网络命名空间里移除网卡看到这个列表你会发现驱动本质上就是一个管理 Linux 网络对象的控制程序Docker 只是一个调用方。4.2 写一个最小可用的驱动骨架下面我用 Go 写一个教学用的最小驱动骨架只聚焦三个核心方法CreateNetwork、CreateEndpoint、Join。package main import ( encoding/json log net net/http ) type Driver struct { networks map[string]*Network endpoints map[string]*Endpoint } type Network struct { ID string Name string Subnet *net.IPNet Bridge string } type Endpoint struct { ID string NetworkID string IP net.IP VethHost string VethPeer string ContainerPID int } // CreateNetwork 记录网段信息真实项目里还要创建 linux bridge func (d *Driver) CreateNetwork(r *CreateNetworkRequest) error { n : Network{ID: r.NetworkID, Name: r.Name} if r.IPv4Data ! nil len(r.IPv4Data) 0 { _, subnet, _ : net.ParseCIDR(r.IPv4Data[0].Pool) n.Subnet subnet } d.networks[r.NetworkID] n return nil } // CreateEndpoint 创建 veth pair 并分配 IP这里是核心 func (d *Driver) CreateEndpoint(r *CreateEndpointRequest) (string, error) { endpointID : r.EndpointID // 真实实现netlink 创建 veth pairIPAM 分配地址 d.endpoints[endpointID] Endpoint{ ID: endpointID, NetworkID: r.NetworkID, } return endpointID, nil } // Join 把 veth 的一端移入容器网络命名空间并配置 IP func (d *Driver) Join(r *JoinRequest) error { // 真实实现setns 进入容器 netns // 将 veth 另一端改名 eth0设置 IP 和默认路由 return nil }真实驱动里CreateEndpoint 要用 netlink 创建 veth pair那么 Join 要用setns进入容器网络命名空间把网卡移进去并设置 IP、路由中间还要处理 IPAMIP 地址分配、DNS 等一堆细节。但对理解机制来说知道这三个步骤就够了记录网络、建网线、把网线插进容器。4.3 把驱动挂到Docker上驱动编译好之后让它监听一个 socket 文件例如/run/docker/plugins/my-driver.sock然后用下面的方式供 Docker 调用mkdir -p /run/docker/plugins sudo ln -s /path/to/my-driver.sock /run/docker/plugins/my-driver.sock sudo systemctl restart docker重启后执行docker network create -d my-driver --subnet 192.168.56.0/24 mynet docker run -d --network mynet --name demo nginx如果驱动协议通信正常容器就能拿到驱动分配的 IP。不过我要强调一句教学骨架能跑通和能上生产是两回事真实环境还要考虑异常恢复、多节点一致性、网络安全组等问题。但亲手挂过一遍驱动之后你对 Docker 网络的掌控感会完全不同以后看任何第三方网络方案你都能明白它是在哪一层、用什么方法实现的。4.4 生产环境驱动选型建议不是所有场景都需要自己写驱动大多数时候选对内置驱动就够了。我根据自己的经验整理了一个选型维度驱动使用范围适用场景注意点bridge单机单机上多容器互联建议用自定义子网别用默认overlay跨主机集群内跨节点互通需要提前配置底层 MTUmacvlan跨主机容器需要独立二层地址依赖交换机IP 规划要谨慎host单机追求极致性能、不需要隔离直接共享宿主机网络栈自定义取决于你固定 IP 池、自定义路由策略需要维护协议兼容性和稳定性挑选时第一看是否跨主机第二看隔离粒度第三看底层网络是否允许你分配网段。很多团队为了图省事把所有容器都塞进一个 overlay结果一到流量高峰期网络性能就崩其实换成分段 bridge 网关互访会更稳定。5. 排查实录与避坑指南5.1 容器外网不通按这个顺序查生产环境里最常见的故障就是容器内网正常外网出不去。我以前也乱猜过 DNS 问题、防火墙问题后来总结出一个固定排查顺序效率高很多排查步骤命令期望结果检查内核转发开关sysctl net.ipv4.ip_forward值为 1检查容器能否访问网关docker exec app-a ping -c2 172.20.0.1通检查 MASQUERADE 规则iptables -t nat -S | grep 172.20.0.0存在对应规则检查 DNSdocker exec app-a cat /etc/resolv.conf看到有效 nameserver一个很常见的坑是在某云环境里云平台自己改了宿主机的 iptables把 FORWARD 策略设成 DROP结果 Docker 容器全部断网。解决办法是在 DOCKER-USER 链里放行必要流量而不是直接改 FORWARD 默认策略。记住DOCKER-USER链就是留给运维自定义规则的地方Docker 不会清它。5.2 自定义子网的网段冲突是隐形炸弹规划自定义网络时网段选不好会留下非常大的隐患。我见过某团队把容器网段直接设成 10.10.0.0/16结果公司内部专线也有一个 10.10.0.0/16容器访问内部系统时数据包的下一跳直接被路由到了错误的网关大量请求超时。合理的规划方式是先收集宿主机路由表里已有的所有网段避开172.17.0.0/16docker0、云 VPC 的网段、办公网网段、其他环境占用的网段。容器网段尽量单独划一个大段比如把生产、测试、开发分别放在 172.20.0.0/16、172.21.0.0/16、172.22.0.0/16互不重叠。顺手维护一张网络规划表写清楚每个环境的子网、网关、用途。这个习惯在集群规模超过 20 台之后能帮你省下无数排查时间。5.3 Overlay网络与MTU的爱恨情仇跨主机通信的 overlay 网络数据包外面要套一层 VXLAN 封装默认 MTU 会被压到 1450。如果你的底层物理网络 MTU 是 1500容器内发出的包大小超过 1450 时就要分片。HTTP 这类应用一般还好但数据库迁移、大数据同步这类大包传输会明显变慢。遇到这类问题先确认应用协议是否带了不允许分片标记再考虑调整docker network create -d overlay \ --subnet 10.20.0.0/16 \ --opt encrypted \ --attachable \ --mtu 1400 \ ov-netMTU 设置偏小会导致吞吐下降偏大会导致黑洞。最稳妥的做法是先测量宿主机之间的实际 MTU再减去 VXLAN 的开销一般是 50 字节。这个数值在不同网络环境里差异很大不要照抄别人的配置。5.4 多网络接入带来的双默认路由问题用docker network connect把容器同时接入两个网络可以解决一部分互通需求但副作用是容器内可能出现两条默认路由。Linux 默认只使用第一条结果就是明明两个网络都能访问偏偏其中一个不通或者访问速度极慢。我遇到过一次很隐蔽的故障容器同时接了管理网络和数据网络默认路由指到了管理网导致所有对外服务都超时。排查方法很简单nsenter -t PID -n route -n看到两条0.0.0.0的默认路由时就要考虑策略路由。我的建议是一个容器尽量只挂一个网络跨网络通信让网关或服务网格去做别在容器内部堆多个网卡。隔离性、可维护性和性能都会更好。6. 写在最后三个让我少加班的小习惯把 Docker 网络的原理和实操都过了一遍之后我复盘了一下真正让我在后续排查里少踩坑的其实是三个小习惯。第一个习惯是动任何网络之前先按顺序把三样东西快照下来——宿主机的route -n、ip link、iptables -S。很多问题不是你改坏的而是别人在你之前改过有了快照才能对比出差异。第二个习惯是能用 tcpdump 抓包验证就不要靠猜。在宿主机对应网桥上抓一下包数据包有没有到、到了之后被谁丢了一眼就能看出来。这个习惯帮我找到了很多看起来像网络问题其实是应用层问题的疑难杂症。第三个习惯是把网络规划文档当成代码一样维护。每个环境的子网、网关、驱动选型、特殊策略都写在文档里踩过的坑也记上一笔。后来者照着文档建网络不再需要重复踩我踩过的那些坑。说实话Docker 网络的东西看着多核心就那几块命名空间给隔离veth 做连接bridge 做汇聚路由和 iptables 做控制。把这几块捋顺了不管以后用的是内置驱动还是第三方方案你都能快速看懂它到底在干什么。希望这篇文章能帮你少走点弯路。