上周一台机器上跑了十几个容器,我让 A 容器去连 B 容器的服务,直接卡住:A 容器里 ping 一下 B 的容器 IP,报 Destination Host Unreachable;但同一个 A 容器访问宿主机上的 Nginx 完全正常,访问外网也正常。当时我第一反应是防火墙,可 docker exec 进去把 iptables 规则翻了个底朝天,愣是没找到那条 DROP。后来才搞明白:问题既不在防火墙,也不在镜像里,而是 Docker 那套 bridge 网络的默认行为同一张 bridge 上的容器之间能不能通,取决于它们到底是不是「同一个网络」,而跨 bridge 默认就是隔离的,被内核的 DOCKER-ISOLATION 链悄悄挡掉了。
如果你正在给一批服务规划容器网络,或者需要一个能自由划分子网、能自定义路由的机器先把环境跑起来,可以先看看 恒创科技(原生 IP 精品线路 / BGP 多线机房) 和 V.PS(xTom 全球云服务器,自有 AS 号与对等互联) 这两家的配置与线路说明容器网络玩得再花,最后还是要落在一台底层路由清晰、带宽说得清楚的机器上。

一、先把 Docker 的几种网络模式分清楚
很多人一说「Docker 网络」就只想到 bridge,其实 Docker 内置的网络驱动有好几种,每一种的互通规则完全不同:
bridge(默认):Docker 会在宿主机上创建一个虚拟网桥 docker0,默认网段是 172.17.0.0/16,网关在 172.17.0.1。每跑一个容器,Docker 就往这张网上挂一个 veth 对,容器那头是 eth0,宿主机这头叫 vethxxxx,接进网桥。容器拿到的是 172.17.0.x 这样的内网地址,出网靠 NAT(MASQUERADE) masquerade 出去,所以容器访问公网没问题。
用户自定义 bridge:这是重点。你用 docker network create 自己建的网,Docker 会额外内置一个 DNS 解析器 127.0.0.11,容器之间可以直接用容器名或服务名互相解析,比如 http://api:8080,这是 Compose 里服务名能当域名用的底层原因。而默认的 docker0 上是没有这个解析能力的,只能用 IP 加 --link(这个参数在现代版本里已经被废弃了)。
host:容器直接共享宿主机的网络命名空间,等于没有自己的 IP,端口直接占宿主机的。好处是性能最好、监听 127.0.0.1 的服务也能被容器访问;坏处是多端口服务会打架,也没法做网络隔离。
overlay:给 Swarm 集群用的,把多台物理机的容器网络叠成一个虚拟的大二层网络,跨主机也能用服务名互通。自己搭 K8s 时那些 CNI 插件(Flannel、Calico)干的是类似的事,只是自己实现了。
macvlan / ipvlan:让容器在物理网络上拥有独立 IP,看起来就像一台独立机器。适合要跑 DHCP、要被上游防火墙单独识别,或者跑 KVM 这种必须有自己的 MAC 地址的活儿。缺点是宿主机本身访问不了这些容器 IP(macvlan 默认如此),得额外加一个 macvlan 子接口才能解决。

二、跨 bridge 为什么默认不通
这是最容易踩的一脚。当你 docker network create 建了第二张、第三张网,然后把不同的服务分别接上去,它们就分属不同网段。此时容器之间的报文在宿主机内核层面走的是转发路径,而 Docker 会在 iptables 的 filter 表里插一条 DOCKER-ISOLATION-STAGE-1 链,规则大意是:源网段和目标网段不在同一个 Docker 网络时,直接 DROP。
这条规则是故意设计的,属于「默认拒绝」。它的目的是保证一个服务即便被攻破,也不能横向摸到另一个服务的数据。但它的存在意味着:如果你指望容器 A 在 app-net、容器 B 在 db-net,然后 A 能直连 B 的 3306 端口,那默认就是不通的,而且你在容器的 iptables 里根本看不到任何规则规则装在宿主机的命名空间里。
更隐蔽的一种情况是:两个容器明明在同一张自建 bridge 上,却还是不通。这通常是网段冲突。比如公司内网用的是 172.16.0.0/12,而 Docker 自建网络默认从 172.18.0.0/16、172.19.0.0/16 往下顺次分配,很容易和内网重叠。这种情况下容器往内网某个 IP 发包,内网的机器回包时把包丢给了路由器,容器收到了但回程路由走错,表现就是「时通时不通」或者「ping 得通但业务握手失败」。

三、我的排查顺序(照着做基本都能定位)
第一步,确认两边到底在不在同一张网。直接看 docker network inspect,把两个容器的 Networks 段拉出来对比。如果一个只有 bridge、另一个有 app-net_bridge,那答案已经有了:把它们都 docker network connect 到同一张网即可,而不是去改防火墙。
第二步,看容器自己的地址和路由。docker exec -it xxx ip a 和 ip route,确认 IP 地址段和默认网关。如果连网关都 ping 不通,那问题在宿主机(网桥没起来、网段被占);如果网关通、目标不通,那就是隔离链或者目标容器根本没监听。
第三步,验一下是不是 ICMP 的锅。很多镜像默认不带 ping,或者宿主机防火墙只放行了 TCP。这就会造成「明明服务能调,就是 ping 不通」的假象。别拿 ping 当唯一探针,用 curl、nc -zv 直接打端口更靠谱。
第四步,看宿主机侧的规则。iptables -t filter -L DOCKER-ISOLATION-STAGE-1 -n -v,被 DROP 的包在这里能看到计数。数字在涨就说明确实是隔离链挡的。另外别忘了 ufw 和 firewalld它们会把自己 FORWARD 链的默认策略设成 DROP,Docker 的规则是插在它后面的,直接被吃掉,容器之间立刻断网。解决办法是在 /etc/ufw/after.rules 或 firewalld 的 direct 规则里放行 Docker 的转发链。
第五步,排查网段冲突。ip addr 把宿主机上所有网段列出来,看有没有和 Docker 分配的 172.17~172.31 段重叠。如果公司内网正好压在这个范围,就固定一个不冲突的网段,比如在 /etc/docker/daemon.json 里写死:
"default-address-pools": [{"base": "10.180.0.0/16", "size": 24}]
改完 systemctl restart docker 生效。顺手也把 bip(默认网桥网段)写死,避免每次重启都往后顺一位。

四、几条踩过才知道的细节
能用名字就别用 IP。自建 bridge 里的服务名解析是 Docker 内置 DNS 提供的,容器重建后 IP 会变,用名字访问就不用跟着改配置。Compose 项目里默认会为每个服务生成一张网络,名字一般是「项目名_服务名」后缀,不是裸服务名,这点经常搞混。
host 模式不是万灵药。它绕开了网桥也就绕开了隔离,出问题的时候排查会更绕。除非是性能敏感或者必须访问宿主机 127.0.0.1 上的服务,否则优先考虑自建 bridge。
macvlan 容器宿主机访问不了。需要的话在宿主机上建一个 macvlan 子接口并加一条路由,把容器网段指到这个子接口上,才能从宿主机直连容器。这个坑我是被监控系统需要主动探活容器才发现的。
docker-proxy 那个进程不是无用信息。老版本 Docker 在 -p 映射端口时会起一个 docker-proxy 进程做端口转发,新版本默认走 iptables 的 DNAT。如果你在容器里看到端口连不上,先看 ss -lntp 里有没有这个进程,再去看 nat 表的 DNAT 规则,比在 filter 表里瞎翻高效得多。
容器里的 DNS 走 127.0.0.11。它会转给宿主机的 resolv.conf。如果你在容器里 ping 得通域名但解析很慢,多半是上游 DNS 配置有问题,而不是 Docker 的锅。
回过头看那天的问题,A 容器和 B 容器被我分到了两张不同的自建网络上,隔离链忠实地把包丢了。我最终的处理是给这两个服务建一张共用的 app-net,用服务名互相访问,然后把数据库那张网单独隔离出去,只在必要的时候开一条 docker network connect。整个过程没有改一行防火墙规则。
容器网络这件事,本质上是「默认拒绝、显式授权」,跟你写防火墙策略的思路是一样的。与其跟 Docker 生成的规则较劲,不如先把网络拓扑规划清楚,让隔离发生在该发生的地方。


喜欢
顶
难过
囧
围观
无聊




