在容器化部署这条路上摸爬滚打久了,你会发现真正决定业务稳定性的往往不是镜像多大、编排多花哨,而是那层看不见摸不着的 Docker 网络。第一次把服务从单机 docker run 搬到跨宿主机集群时,容器间连不通、端口一会儿通一会儿不通、外部访问卡顿——这些问题会把你折磨得怀疑人生。这篇内容想做的事,就是把 Docker 网络这层窗户纸彻底捅破,从内核态的原语讲到生产环境的组网实战,把原理和踩坑记录揉在一起,给后来人一条相对笔直的路。
这篇文章适合已经会基本 Docker 操作、正在被网络问题困扰的开发者或运维。读完你会明白 veth、网桥、iptables 这些底层零件是怎么协同工作的,也能直接拿走一套可落地的自定义网络方案和排查方法论。
1. Docker 网络背后到底藏着什么
很多教程上来就让你记住几种网络模式的名字和适用场景,但没人告诉你这些模式只不过是对同一组内核原语的不同排列组合。理解 Docker 网络的第一步,是先忘掉 Docker 本身,单纯看 Linux 内核提供的能力。
1.1 veth 与 network namespace:网络世界的乐高积木
Network namespace 可以理解为内核帮你创建的一间独立小房间,里面有自己的网卡、路由表、iptables 规则和协议栈。Docker 容器本质就是跑在一个独立网络命名空间里的进程,所以容器里执行 ip addr 看到的东西和宿主机完全无关,这种感觉就像两台独立的机器共享同一个物理主机。
但独立不等于隔离,容器还要和外界通信。这时候 veth pair 登场了。veth pair 不是一块网卡,而是成对出现的两张虚拟网卡,中间连着一根看不见的虚拟网线。你在这头发送数据,那头的网卡就能收到,反之亦然。Docker 创建容器时干的活就是:创建一对 veth,一头塞进容器的命名空间变成 eth0,另一头留在宿主机挂在网桥上。数据从容器 eth0 发出去,顺网线到达宿主机的 veth 端,然后被网桥交给其他设备,整个过程不需要任何用户态进程参与。
这个设计非常优雅的地方在于,它把网络栈的复杂逻辑全部封装在内核里,用户看到的就是一对普通网卡。你完全可以用 Linux 原生命令手动模拟这个过程:ip netns add、ip link add veth0 type veth peer name veth1、ip link set veth1 netns。理解了这套动作,你就理解了 Docker 容器网络的基石。
1.2 bridge、iptables 与路由:容器间的三层通路
光有网线还不够,容器之间、容器和宿主机之间还得有个交换设备把它们串起来。这就是 Linux bridge 网桥要做的事。Docker 默认创建的 docker0 网桥,作用相当于一台虚拟二层交换机,所有挂在它上面的 veth 端口都在同一个广播域里,可以基于 MAC 地址直接通信。
二层通了只解决了本地通信问题,容器访问外网还需要三层路由和数据包改写。这里有三件套缺一不可:Sysctl 参数 net.ipv4.ip_forward 必须为 1,让宿主机内核允许转发数据包;路由表上要有一条指向 docker0 网桥的容器子网路由;iptables 的 NAT 规则负责把去往外部网络的容器流量做源地址转换(MASQUERADE),让流量伪装成宿主机 IP 出网。
如果你把这三件套任何一个环节关掉,马上就能复现大家常说的"容器 ping 不通外网"问题。所以我一直建议排查网络问题先看这三个底层开关,而不是跑到应用层去猜。
2. 五大网络模式:选型背后的真正逻辑
Docker 提供五种网络模式,每一种都是某种场景下的最优解。做过几次生产项目之后,我越发觉得选模式不是靠口诀表,而是要想清楚你的业务对网络隔离性、性能和连通性的具体需求。
2.1 bridge 模式:默认选项但不等于最佳选项
bridge 模式是 docker run 的默认模式,容器会挂到 docker0 或你指定的自定义网桥上。它的核心优势是开箱即用,容器间通过 IP 通信,容器访问外网走 NAT,外部要访问容器就靠端口映射。
默认的 docker0 网桥有个让人容易忽略的短板:它不支持基于容器名的 DNS 解析。也就是说,在默认网桥里,两个容器之间只能用 IP 互连,一旦容器重建,IP 变了,配置就全乱了。生产环境我从来不建议使用默认 bridge,而是强烈推荐用户自定义 bridge 网络。自定义网络内部接入了 Docker 自带的嵌入式 DNS 服务,容器可以像域名访问一样直接通过服务名互相通信,还支持运行时动态挂载和卸载容器,灵活得多。
2.2 host、container 与 none:三匹性格迥异的野马
host 模式让容器直接用宿主机的网络命名空间,没有自己的 IP,端口也是共享的。它的性能损耗几乎为零,适合对网络延迟要求极其苛刻的场景。代价就是缺少网络层面的隔离,你无法让两个容器监听同一个端口,这在多业务并存的环境中会制造不少困扰。
container 模式较为特殊,它不创建新网络命名空间,而是复用另一个容器,常见于 sidecar 代理这类场景。比如说你要给一个业务容器加流量监控,传统做法是塞一个 agent 进去,其实完全可以让监控容器用 container 模式共享业务容器的网络栈,这样它在容器内看到的就是完整的业务流量数据,干净且不干扰原容器。none 模式则把网卡也收回了,容器里只有回环接口,适合跑一些完全不需要网络的计算任务,或者你打算在容器运行时自己手工配置复杂的网络拓扑。
3. 从零建设一个隔离的桥接网络
理论说太多容易飘,这一节我以实际搭建一套"前端 + 后端 + 数据库"三容器隔离网络为例,把每一步关键动作和为什么要这么做的理由讲透。这套环境也是我判断一个人是否真正理解 Docker 网络的试金石——照着做一遍,许多概念就通了。
3.1 创建自定义网络与子网规划
规划网络时会重点考虑两个因素:子网大小和网段是否会与宿主机现有网络冲突。生产环境我倾向于给每个项目划分独立的 /24 子网,保证最多 254 个可用地址,同时避免与公司内网段重叠——一旦重叠,路由条目会打架,届时容器访问公司内部资源会出现诡异的时通时断。
创建命令非常简单,也可以顺便把网关和 IP 范围指定明白:
docker network create \ --driver bridge \ --subnet=172.20.0.0/24 \ --gateway=172.20.0.1 \ --ip-range=172.20.0.128/25 \ demo_net这里的参数值得逐一解释。--driver bridge 明确使用桥接驱动而不是其他插件;--subnet 决定了整个网络的地址池;--gateway 需要落在子网内,通常是第一个可用地址;--ip-range 则是留给容器动态分配的地址段,我习惯预留前半段给数据库、网关这类需要固定 IP 的服务用。手工规划后的稳定性远比直接裸跑 docker network create 高得多。
3.2 部署容器并验证跨容器通信
接着把业务容器连接进去,我这里用简单的 Nginx 作为演示,但思路完全适用于任何真实服务:
docker run -d --name web-server --network demo_net nginx:alpine docker run -d --name db-server --network demo_net \ --ip 172.20.0.10 \ mysql:8进入 web-server 容器内执行 ping db-server,你会惊喜地发现不需要知道 IP,DNS 直接把容器名解析成了 172.20.0.10。这是因为 Docker 在自定义网络里注入了自带的 DNS 服务,监听地址是 127.0.0.11,容器的 /etc/resolv.conf 会自动指向它。这个机制是我放弃默认 bridge 的理由之首——基础设施为应用付出了近乎零的额外成本,却换来了极大的运维便利。
如果这时候你在宿主机上执行 ip addr,会发现多了两个 veth 接口,一头连着宿主机,一头连着容器的 eth0。再用 brctl show 查看网桥,能看到这两个接口确实挂在 demo_net 网桥上。二层三层链路全部打通,数据包在容器、网桥、宿主机之间就通畅了。
3.3 为什么外部访问必须靠端口映射
默认情况下,外部网络无法直接访问容器 IP。容器 IP 属于私有网段,宿主机在你部署时通常有防火墙规则,更关键的是数据包从宿主机进入 demo_net 网桥时,缺少对应的路由和转发规则。Docker 解决这个问题的标准方法是端口映射:
docker run -d --name web-expose \ --network demo_net \ -p 8080:80 \ nginx:alpine-p 8080:80 的动作被 iptables 翻译成了一条 DNAT 规则:宿主机任何接口进入的 TCP 8080 端口流量都被改写到容器 IP 的 80 端口。数据包先落在宿主机的网络栈上,然后通过 FORWARD 链放行,再被"翻译"到目标容器。理解这个链路后,你能解释很多灵异现象:比如宿主机防火墙忘了放行 FORWARD 链,端口映射就会时好时坏;比如容器内服务只监听了 IPv6,映射规则也帮不上忙。端口映射在虚拟机时代对应的是"端口转发"功能,但在容器网络上,它其实是一整套由 iptables 驱动的高性能地址转换机制。
4. 底层网络数据流转全解析
既然已经看到了规则长什么样,这一节我们从数据包的角度把整个过程走一遍,不仅对着命令讲,还讲清楚"为什么会产生这些行为"。这对解决复杂的网络故障特别有帮助。
4.1 容器向外访问网络的完整旅程
假设 web-server 容器要访问外网某个地址。数据包从容器 eth0 发出,源 IP 是 172.20.0.x,目的 IP 是公网地址。包到达 veth 对端,进入宿主机的网桥。网桥把包递交给宿主机的内核协议栈,此时查路由表,发现目的地址不在任何本地子网内,默认路由是走 eth0。在发送之前,iptables 的 POSTROUTING 链生效,MASQUERADE 规则会把源 IP 替换成宿主机的出口 IP。于是公网服务器看到的请求完全像宿主机发起的,回包也理所应当地先回到宿主机,再经过 NAT 转换,通过原路送回容器的 eth0。
如果容器访问外网出现"能解析 DNS 但 TCP 连接失败",最典型的故障就是 docker 到宿主机的 ip_forward 没开。一旦关闭这个内核转发开关,内核对 FORWARD 链上的包一律不做转发,"一切正常但上不了外网"的诡异网络故障就出现了。我处理过好几次这种"玄学问题",最后都落在这个内核参数上。
4.2 外部请求进入容器的路径改写
这里重点审视 DNAT 改变源 IP 之后发生了什么。外部客户端的连接到达宿主机,目的地址是宿主机 IP 的 8080 端口。进入协议栈后,iptables 改写目标 IP 为容器 IP 172.20.0.x,但源 IP 仍然是客户端的公网 IP。这个包随后来到路由选择环节,内核发现目标地址属于 demo_net 子网,会把它从对应网桥发出去。
有意思的是,如果客户端和宿主机在同一台机器上(比如在宿主机上临时用容器测试连接),这条路径稍有不同,源 IP 会变成 127.0.0.1 回环地址,导致容器应用里看到"诡异"的请求来源。很多人在调试回调、鉴权时踩过这个坑,白名单总是对不上。理解 DNAT 和路由的先后顺序,你就不会在这个细节上卡太久。
抓包挡不住的时候,在容器内用 tcpdump 观察实际流量是最直接的证据。我曾经在容器的 eth0 上抓包,发现很多"未授权"请求的源地址显示为宿主机网段,立刻意识到这是 NAT 后的效果——来源 IP 早已被宿主机改写,应用层对"真实客户端 IP"的判定逻辑需要重新设计。
5. 跨宿主机组网的方案对比与落地
单机玩明白了,不等于生产环境就稳了。生产环境一般都会有多个宿主机,容器需要跨机器互相通信。Docker 本身提供的默认能力在这个层面明显不足,需要引入额外方案。这一节对比几种主流思路,讲清楚各自的取舍。
5.1 overlay 网络:当跨机容器需要一个统一子网
overlay 网络的目标是让分布在不同宿主机上的容器感觉自己在同一个二层网络里。Docker Swarm 模式下内置的 overlay 网络驱动基于 VXLAN 技术实现:它把容器发出的二层帧封装成 UDP 包,通过宿主机之间的三层网络传输到达目的地后再解封装。这个封装/解封装动作由内核模块完成,性能比纯用户态方案好很多。
实战中要注意 VXLAN 默认使用 UDP 4789 端口做数据面传输,很多公司防火墙并不会放行这个端口,因此在测试 overlay 网络时,第一个动作应该是确认宿主机间 4789 端口可通。验证命令很简单:
nc -uvz <peer-host-ip> 4789另外,overlay 网络依赖 etcd 或内置的集群存储来同步网络配置,网络控制面时延对整个集群的行为影响很大。当集群规模较大的时候,没有独立 etcd 的默认方案可能成为瓶颈,此时要重点监控存储的响应时间。
5.2 macvlan 与物理网络直通方案
如果业务对网络性能极其敏感,或者容器需要使用固定 IP 与现有物理网络设备对接,macvlan 是更好的选择。macvlan 直接在物理网卡上创建多个虚拟子接口,每个子接口拥有独立 MAC 和 IP,容器流量不经过 NAT、不经过用户态转发,性能几乎等同于宿主机原生网络。容器看起来就是物理网络上的一台台独立主机。
但这个方案有一个众所周知的"坑":容器创建之后,宿主机和容器之间默认是无法直接通信的。因为物理网卡在主接口模式下已经被虚拟子接口占用了,宿主机自身没有合适的路径去访问那个虚拟子接口。解决办法是给物理网卡额外创建一个 macvlan 子接口并分配一个同网段的 IP,专门用于宿主机访问容器。这个细节我在至少三个项目里见到有人踩坑,现象是"容器可以互相访问、外部也能访问容器,唯独宿主机访问不了容器"。
5.3 生产网络方案的选型思路
没有任何一个网络方案是万能的。跨宿主机通信,如果你的业务对性能要求较高且团队对 VXLAN 不熟,可以考虑三层路由方案(比如 BGP 分发容器路由);如果你需要极简网络设计,overlay 是最省心的;如果必须使用固定 IP 和物理网络强一致,macvlan 是最终出路。我个人遇到过不少团队在刚开始就盲目上 overlay,结果排查故障时发现自己对封装、解封装、MTU 的细节完全陌生,反而耗时翻倍。先把单机 bridge 链路吃透,再上跨机方案,这句话值得写在工位上。
6. 那些年我处理过的 Docker 网络深坑
很多网络知识不踩坑根本记不住。这一节挑出高频出现的四大类问题,把现象、排查套路、根因和修复方法整理成速查表。这些都是从生产环境实战中得到的经验,而不是文档摘录。
6.1 容器间 ping 不通,但业务正常
这个情况最有迷惑性。业务通信走 TCP 端口,而 ping 走的是 ICMP,很多底层网络设备或云平台默认会丢弃 ICMP。此时容器的"不可达"不代表真正不通。我常用的做法是在容器内测试具体端口:
docker exec web-server nc -zv db-server 3306如果端口能连通,说明链路是通的,只是 ICMP 协议被网络策略过滤了,不需要过度焦虑。反过来,如果端口不通,再考虑是不是服务绑定地址只监听了 127.0.0.1。
6.2 端口映射偶尔失效,重启后恢复
这个坑让我最为头疼。现象是业务高峰期端口访问时不时超时,重启容器后恢复正常。排查时发现是 iptables 规则老化或 NAT 表记录被其他工具覆盖导致的,比如某些云平台的防火墙管理工具会定期刷新规则。处理方法是使用 docker network connect 重新挂载容器,或者用 iptables-restore 把 Docker 的规则规范化。更根治的思路是避免依赖 iptables 规则做高可用,把流量入口迁移到负载均衡器或者网关方案(比如服务网格边车代理)上去。
6.3 MTU 引发的外网"半通"
这个问题的典型现象是:容器能解析 DNS,能访问内网,但一访问外网网页就像被限流一样。排查到最后发现宿主机的物理网络 MTU 是 1450,而 Docker 默认是 1500,数据包在云网关处被丢弃。解决方法是给 Docker 守护进程配置统一的 MTU,比如在 daemon.json 里设置 "mtu": 1400,然后把受影响容器重建一遍。这类问题和宿主机物理链路参数耦合很深,网络规划阶段就要把 MTU 策略纳入考虑范围。
6.4 同宿主机两个容器端口冲突
host 模式共享宿主机网络栈,端口冲突是必然的。但 bridge 模式下也会出现"端口冲突"现象,往往是你在容器里监听的端口和宿主机已有端口映射规则冲突了。启动时会有明确报错,不用过分担心;但在动态调度环境下,需要提前检查端口占用情况。如果服务端口不固定,可以考虑使用动态端口映射或由容器编排平台自动分配随机高位端口。
7. Docker 网络调试工具箱与实战心得
看过这么多原理和案例,最后分享几个能明显提升排障效率的工具和习惯。这些不是公司专利,而是经年累月总结的个人偏好,谁用谁知道。
第一个工具是 nsenter,当你需要进入容器的网络命名空间时,它比 docker exec 更直接,尤其在容器内缺少必要调试工具时:
nsenter -t $(docker inspect -f '{{.State.Pid}}' web-server) -n ip addr第二是 tcpdump 配合桥接接口抓包,它能同时看到出入容器的真实流量,判断到底是宿主机层面丢了包,还是容器内服务压根没监听。我习惯先在容器 eth0 上抓,再在 veth 对端抓,通过对比两个抓包文件就能定位丢包发生在哪个环节。还有一点就是养成出发前先看 iptables-save 的习惯——很多"网络原因"其实都能在规则里找到真相,不需要反复重启重试。总结来说,Docker 网络从原理到实战,核心是搞清四个模型:namespace 隔离网络栈、veth 打通隔离、bridge 提供交换、iptables 实现转发和映射。把这四个模型想在前面,遇到任何结构诡异的问题,你都能抽丝剥茧找到那个真正出错的组件。