Docker 基础体系 · 第 67/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker 网络故障诊断:Namespace、Bridge、DNS、NAT、MTU 和抓包
Docker 容器网络故障通常表现为“容器访问不了服务”,但这个现象可能来自完全不同的层次:
- 容器没有正确的网络命名空间或路由;
- veth 与 Linux bridge 没有连通;
- Docker 的转发规则或宿主机防火墙丢弃了数据包;
- 容器内的嵌入式 DNS 无法解析名称;
- DNS 已经成功,但目标端口没有监听;
- NAT 或端口发布方向理解错误;
- 路径 MTU 过大,导致大包丢失;
- 抓包位置不对,误以为数据包没有发送。
因此,诊断的第一原则不是“先改 Docker 配置”,而是先把故障定位到数据路径中的某一段:
进程
↓
容器 network namespace
↓
容器 eth0 / veth
↓
宿主机 Linux bridge
↓
iptables/nftables、路由、NAT
↓
物理网卡或其他网络设备
↓
目标地址
名称解析则是一条并行但相互依赖的路径:
应用
↓
/etc/resolv.conf
↓
Docker embedded DNS(通常为 127.0.0.11)
↓
容器网络上的服务名解析,或宿主机配置的上游 DNS
下面以 Linux 上的 Docker Engine 为边界讨论。Windows 容器、Docker Desktop 的 Linux VM、Rootless Docker 和 Kubernetes CNI 会改变部分实现细节,不能直接套用所有命令和结论。
一、先建立模型:一次连接到底经过哪些组件
1. Network namespace:容器看到的不是宿主机网络
Linux network namespace 是内核隔离网络资源的机制。每个 namespace 可以拥有独立的:
- 网络设备;
- IP 地址;
- 路由表;
- 邻居表;
- iptables/nftables 规则上下文;
- socket 视图。
Docker 为普通 Linux 容器创建独立的 network namespace。容器内看到的 eth0,通常并不是宿主机真正的物理网卡,而是一个 veth pair 的一端。
一个典型拓扑如下:
容器 namespace 宿主机 namespace
+---------------------+ +-----------------------------+
| eth0: 172.18.0.2 | | vethxxxx |
| lo: 127.0.0.1 | | \ |
| route: default via | | docker0 / br-xxxx |
| 172.18.0.1 | | | |
+----------+----------+ | eth0 / ens33 |
| +-------------+---------------+
| veth pair |
+------------------------------------------+
veth pair 的两端始终连接在一起:一端收到的数据会从另一端出现。Docker 把容器一端放入容器 namespace,把宿主机一端接入 bridge。
因此,以下命令必须区分执行位置:
# 在容器 namespace 中
docker exec web ip addr
docker exec web ip route
# 在宿主机 namespace 中
ip link
ip addr
ip route
如果容器内执行:
ip addr
看到的是容器自己的 eth0;在宿主机执行同名命令,通常只能看到 veth 的宿主机端,而看不到容器内的 eth0。
2. 如何进入容器的 network namespace
docker exec 是最简单的方式:
docker exec -it web sh
如果镜像没有 shell,可以用宿主机上的 nsenter。先取得容器内主进程 PID:
pid=$(docker inspect -f '{{.State.Pid}}' web)
sudo nsenter -t "$pid" -n ip addr
sudo nsenter -t "$pid" -n ip route
sudo nsenter -t "$pid" -n cat /etc/resolv.conf
这里的 -n 表示进入目标进程的 network namespace。这个方法很重要,因为容器可能没有 ip、ss、tcpdump 等诊断工具。
查看容器与网络的 Docker 状态:
docker inspect web
docker network ls
docker network inspect appnet
docker inspect 展示的是 Docker 维护的对象状态;ip、bridge、nft 展示的是 Linux 内核和防火墙层面的实际状态。两者不一致时,通常意味着网络对象残留、规则未正确刷新、手工修改过宿主机网络,或诊断时机处于状态变更过程中。
二、Bridge:二层转发与三层路由不是同一件事
1. Linux bridge 做什么
Linux bridge 是宿主机上的二层交换设备。它根据目的 MAC 地址转发以太网帧,不根据 IP 地址选择三层路径。
Docker 常见的 bridge 网络包括:
- 默认
bridge网络,通常对应宿主机上的docker0; - 用户自定义 bridge 网络,通常对应名为
br-<network-id-prefix>的设备。
查看设备:
ip -br link
ip -br addr
bridge link
bridge fdb show br docker0
典型结果可能类似:
docker0 UP 172.17.0.1/16
veth8a1c@if2 UP
br-91d2c3 UP 172.18.0.1/16
bridge fdb show 显示 bridge 的转发表。它将 MAC 地址映射到端口,例如某个 veth。若 bridge 未学习到目标 MAC,可能发生泛洪;若端口状态错误,帧则无法到达容器。
2. 同一 bridge 上的容器通信
假设:
web: 172.18.0.2
db: 172.18.0.3
网关: 172.18.0.1
web 访问 db 的 172.18.0.3:5432 时,数据路径大致为:
- web 根据路由表判断
172.18.0.3与本机位于同一网段; - web 通过 ARP 请求
172.18.0.3对应的 MAC; - 数据包从容器
eth0发出; - 经 veth pair 到达宿主机 bridge;
- bridge 根据 MAC 转发表把帧送入 db 对应的 veth;
- db namespace 收到数据包;
- db 内核把数据交给监听
5432的 socket。
这种通信不需要经过外部物理网卡,也通常不需要 SNAT。目标地址仍然是 172.18.0.3。
可以用下面的命令分别验证各段:
docker exec web ip route
docker exec web ip neigh
docker exec web ping -c 1 172.18.0.3
docker exec web nc -vz -w 2 172.18.0.3 5432
ping 成功只说明 ICMP 可达,不说明 TCP 端口可用;nc 能建立 TCP 连接,才能证明目标端口在当前路径上可达并接受连接。
3. 默认 bridge 与用户自定义 bridge 的重要差异
用户自定义 bridge 网络通常提供 Docker embedded DNS,使容器可以用容器名或 Compose 服务名解析彼此:
docker network create appnet
docker run -d --name db --network appnet postgres:16
docker run --rm -it --network appnet busybox nslookup db
默认 bridge 网络的传统行为不同。容器通常可以通过 IP 通信,但不能依赖现代用户自定义网络提供的同等容器名解析能力。若需要服务发现,应显式创建用户自定义网络,或使用 Compose 管理网络。
一个常见错误是:
docker run -d --name api --network appnet my-api
docker run -d --name db postgres:16
此时 api 在 appnet,db 在默认 bridge。即使两个容器都运行在同一宿主机上,它们也不自动处于同一二层网络,api 不能把 db 当作同一网络中的服务名使用。
修复方式是让它们加入同一个网络:
docker network connect appnet db
或者在 Compose 中声明共同网络:
services:
api:
image: example/api
networks: [appnet]
db:
image: postgres:16
networks: [appnet]
networks:
appnet:
Compose 中的 db 是服务名。它通常会解析到该服务当前容器的网络地址,但这不是一个永久固定的 IP。容器重建后 IP 可能变化,应用必须按名称重新解析,而不应把容器 IP 写死。
三、路由、转发与 NAT:数据包为什么能出去,回包为什么能回来
1. 容器路由表决定第一跳
在容器中执行:
docker exec web ip route
常见结果:
172.18.0.0/16 dev eth0 proto kernel scope link src 172.18.0.2
default via 172.18.0.1 dev eth0
它表示:
- 访问
172.18.0.0/16内地址时,直接通过eth0发送; - 访问其他地址时,交给网关
172.18.0.1; src 172.18.0.2是默认选择的源地址。
形式化地说,对于目的地址 D,路由表选择最长前缀匹配项:
例如同时存在:
172.18.0.0/16 dev eth0
default via 172.18.0.1
访问 172.18.5.10 时匹配 /16,不会走 default;访问 8.8.8.8 时没有更具体的匹配,才走 default。
如果容器没有默认路由,常见现象是:
ping: bad address 'example.com'
这可能是 DNS 问题,也可能是 DNS 请求根本没有可用的出站路径。必须先检查:
docker exec web ip route
docker exec web cat /etc/resolv.conf
2. 宿主机必须允许转发
容器访问外部网络时,数据包通常需要从 bridge 转发到物理网卡。宿主机需要允许 IPv4 转发:
sysctl net.ipv4.ip_forward
正常的转发路径要求该值为 1:
sudo sysctl -w net.ipv4.ip_forward=1
但手工修改只是临时状态;永久配置应通过发行版的 sysctl 配置管理。Docker 通常会在需要时设置或依赖此能力,但系统启动脚本、防火墙管理器或安全基线可能再次修改它。
IPv6 要单独检查:
sysctl net.ipv6.conf.all.forwarding
开启 IPv4 转发并不等于 IPv6 也能正常转发。
3. NAT 的必要性:私有源地址如何获得回包
假设容器发送:
源:172.18.0.2:40000
目的:203.0.113.20:443
互联网中的路由器通常不会把 172.18.0.0/16 当作可返回到该 Docker 网络的公共路由。宿主机因此通常执行源地址转换,即 SNAT 或 MASQUERADE:
出站前:172.18.0.2:40000 → 203.0.113.20:443
出站后:宿主机公网IP:52000 → 203.0.113.20:443
回包到达宿主机后,连接跟踪表将:
宿主机公网IP:52000 → 203.0.113.20:443
还原为:
172.18.0.2:40000 → 203.0.113.20:443
NAT 不是单纯修改一个数据包字段,而是依赖 conntrack 保存连接映射。若 conntrack 表满、规则缺失或回程路径绕开了执行 NAT 的主机,连接可能表现为“能发出但收不到回包”。
查看相关状态:
sudo conntrack -L
sudo conntrack -S
系统未安装 conntrack 工具时,需要安装发行版对应的软件包。生产环境中直接清空连接跟踪表风险很高,因为会中断现存连接,不应把 conntrack -F 当作常规修复手段。
4. Docker 的 iptables/nftables 规则
Docker 常见地创建用于过滤、转发和 NAT 的规则链,例如:
DOCKER;DOCKER-USER;DOCKER-FORWARD;- 与 bridge 网络相关的隔离链。
具体链布局会随 Docker Engine 版本、iptables 后端和发行版变化。现代 Linux 上,iptables 命令可能由 iptables-nft 提供:命令语法仍是 iptables 风格,但规则最终由 nftables 内核框架承载。
检查两种视图:
sudo iptables -S
sudo iptables -t nat -S
sudo nft list ruleset
不要仅因为 iptables -L 输出为空,就断定没有规则;如果系统使用不同的后端,应该确认:
iptables --version
nft --version
DOCKER-USER 的意义是提供一个供管理员添加过滤规则的稳定入口。一般应把自定义限制放在那里,而不是直接改 Docker 自动生成的 DOCKER 链,因为 Docker 重启或网络重建可能覆盖自动生成部分。
例如,阻止某个外部网段访问 Docker 转发流量:
sudo iptables -I DOCKER-USER -s 203.0.113.0/24 -j DROP
sudo iptables -A DOCKER-USER -j RETURN
这条规则的实际影响取决于流量方向、接口和发行版的 FORWARD 策略。生产修改前应先备份规则,并用明确的允许规则验证,不要盲目把默认策略改为 ACCEPT 来“排除故障”,因为这可能绕过主机安全边界。
四、端口发布:DNAT、监听地址与冲突
1. -p 改变的是入站路径,不是容器监听地址
启动容器:
docker run -d --name web -p 8080:80 nginx:alpine
含义通常是:
宿主机地址:8080 → 容器地址:80
外部客户端访问宿主机 8080 时,Docker 通过 DNAT 把目标地址改为容器 IP 和端口:
客户端 → HOST:8080
↓ DNAT
172.17.0.2:80
容器内的 nginx 仍然监听 80,而不是 8080。因此下面两种失败原因不同:
docker exec web ss -lnt
若只监听:
127.0.0.1:80
那么即使发布了 -p 8080:80,来自容器网络接口的连接也可能无法到达,因为服务只绑定在容器自己的 loopback 上。应用通常需要监听 0.0.0.0:80,或明确监听容器 IP。
2. 端口冲突发生在发布端
以下命令不能同时成功:
docker run -d --name a -p 8080:80 nginx:alpine
docker run -d --name b -p 8080:80 nginx:alpine
原因不是两个容器不能都监听容器内的 80,而是同一宿主机地址族和绑定地址上的 8080 发布端口发生冲突。
查看占用:
sudo ss -lntp | grep ':8080'
docker ps --format 'table {{.Names}}\t{{.Ports}}'
如果只需要绑定宿主机回环地址,可以缩小暴露范围:
docker run -d --name web -p 127.0.0.1:8080:80 nginx:alpine
这通常意味着只有宿主机本地和能够访问宿主机 loopback 的进程可访问,不能被外部网卡直接访问。端口发布的默认绑定行为应以当前 Docker Engine 配置和平台为准,不要假设所有环境都拥有相同的暴露范围。
3. 区分三种访问方式
对发布端口的诊断,必须明确客户端位置:
A. 其他机器 → 宿主机:8080
B. 宿主机 → 127.0.0.1:8080 或宿主机地址:8080
C. 容器 → 服务容器:80,或容器 → 宿主机:8080
A 主要验证外部接口、防火墙、DNAT 和回程路径。
B 还会受到宿主机监听地址、主机防火墙和 hairpin 行为影响。
C 如果访问的是同一 Docker 网络中的服务,通常应直接使用服务名和容器端口,而不是绕到宿主机发布端口:
docker exec api curl -v http://db:5432
数据库不是 HTTP 服务,上面的命令只用于展示寻址方式;实际应使用数据库客户端或 nc 检查 TCP:
docker exec api nc -vz -w 2 db 5432
在同一 Docker 网络中,db:5432 比 host.docker.internal:published-port 更直接。后者依赖平台提供的特殊主机名和额外路径,不应作为 Linux 原生 Docker 容器间通信的默认方式。
五、DNS:容器名称解析、外部解析和搜索域
1. /etc/resolv.conf 不是 DNS 服务本身
容器内查看:
docker exec web cat /etc/resolv.conf
现代用户自定义网络中,常见结果包含:
nameserver 127.0.0.11
options ndots:0
127.0.0.11 是 Docker 为容器提供的 embedded DNS 地址。这里的 127.0.0.11 位于容器 network namespace 中;它不是直接访问宿主机的 127.0.0.11,因为每个 namespace 有独立的 loopback 和 socket 空间。
embedded DNS 通常负责两类请求:
- 用户自定义 Docker 网络内的容器名、服务名和网络别名;
- 将外部域名请求转发给 Docker 从宿主机配置推导出的上游 DNS。
因此,下面两个测试必须分开:
# 服务发现
docker exec api getent hosts db
# 外部 DNS
docker exec api getent hosts example.com
getent hosts 使用系统的 NSS 配置,比只测试 nslookup 更接近某些应用的真实解析路径。若镜像没有 getent,可以使用临时诊断容器:
docker run --rm --network container:api nicolaka/netshoot dig db
docker run --rm --network container:api nicolaka/netshoot dig example.com
--network container:api 让临时容器共享 api 的 network namespace,因此它看到的 IP、路由和 DNS 路径与 api 相同;这比单独启动一个网络不同的诊断容器更准确。
2. Service Name、网络别名与 IP 的关系
在 Compose 中:
services:
api:
image: example/api
depends_on:
- db
networks: [appnet]
db:
image: postgres:16
networks: [appnet]
networks:
appnet:
api 访问数据库时应使用:
主机名:db
端口:5432
depends_on 主要表达启动顺序或依赖关系,不能保证数据库已经完成初始化并开始监听。即使 DNS 已经解析成功,数据库仍可能返回连接拒绝。应用应实现重试或健康检查,而不是把 DNS 成功当成服务就绪。
Compose 中可以通过 aliases 增加网络别名:
services:
db:
image: postgres:16
networks:
appnet:
aliases:
- database
此时同一网络中的客户端可能解析 db 和 database。别名只在声明它的网络范围内生效。
容器重建后,服务名仍可解析,但返回的 IP 可能变化。因此:
正确:连接 db:5432,并在连接失败时重新解析/重试
错误:启动时解析 db 得到 172.18.0.3,永久缓存该 IP
3. Search Domain 与 ndots
DNS 名称不是简单的字符串替换。/etc/resolv.conf 中可能出现:
search project_default
options ndots:0
假设应用查询 db,解析器可能根据 search domain 尝试:
db.project_default
db
具体查询顺序由解析器实现和 ndots 决定。ndots:5 的常见效果是:少于 5 个点的名字可能先按相对名称拼接搜索域,再尝试绝对名称;ndots:0 则更倾向于直接查询该名称。
这会导致两个看似矛盾的现象:
getent hosts db成功;- 应用查询
db.example.com失败,或查询次数明显增加。
如果名称本身带有结尾点:
db.
它表示绝对 DNS 名称,通常不会再追加 search domain。诊断时应记录应用实际查询的名称、查询类型和返回码,而不是只在 shell 中测试一个短名称。
4. 常见 DNS 故障路径
服务名无法解析
docker exec api getent hosts db
失败时依次检查:
docker inspect api
docker inspect db
docker network inspect appnet
docker exec api cat /etc/resolv.conf
重点确认:
- 两个容器是否真的连接到同一个用户自定义网络;
- 服务名是否是 Compose 服务名,而不是容器内部应用名;
- 目标容器是否已启动并仍在该网络中;
- 是否使用了错误的网络别名;
- 是否查询了不属于该网络作用域的名称。
外部域名无法解析
先分别验证 IP 连通性和 DNS:
docker exec api ping -c 1 1.1.1.1
docker exec api getent hosts example.com
如果 IP 可达而域名失败,重点检查:
/etc/resolv.conf;- Docker daemon 的 DNS 配置;
- 宿主机上游 DNS;
- UDP/TCP 53 是否被防火墙阻断;
- DNS 响应是否超过 MTU 或被中间设备丢弃。
如果 IP 也不可达,则 DNS 可能只是第一个暴露问题的应用层现象,应该回到路由、转发、NAT 和 MTU 检查。
DNS 成功但连接失败
docker exec api getent hosts db
docker exec api nc -vz -w 2 db 5432
第一条成功、第二条失败,说明名称解析路径大体正常,应检查:
docker exec db ss -lntp
docker logs db
docker inspect db
尤其要确认服务是否只监听 127.0.0.1、端口是否实际为 5432、容器是否正在重启,以及目标服务是否因为认证或初始化失败而主动关闭连接。
六、MTU:为什么小包成功,大包或 HTTPS 却失败
1. MTU 的定义与路径条件
MTU(Maximum Transmission Unit)是某个网络接口一次能够承载的最大三层 IP 数据包大小。以太网常见 MTU 是 1500 字节,但这不是所有路径的保证。
对一条路径而言,路径 MTU 可近似表示为:
其中每个 是路径上第 个链路或隧道接口允许的最大 IP 包大小。
如果发送方构造的包大小 满足:
则该包可以在不分片的情况下通过。若:
则可能出现三种结果:
- 中间路由器分片;
- 中间路由器返回 ICMP “Fragmentation Needed”;
- 数据包被静默丢弃。
IPv4 允许部分分片,IPv6 路由器不负责分片,过大的包通常依赖 ICMPv6 Packet Too Big。防火墙阻断这些 ICMP 消息,会破坏 Path MTU Discovery。
2. Docker 中 MTU 变小的典型原因
常见路径包括:
容器 eth0 → veth → bridge → 物理网卡
如果底层使用 VXLAN、云厂商 overlay、VPN、GRE、WireGuard 或其他隧道,隧道头部会消耗额外空间。例如底层链路仍是 1500,但隧道额外占用 字节,则可用上层 MTU 大约为:
不能仅凭“宿主机物理网卡是 1500”推断容器路径一定支持 1500。
查看各层:
ip link show eth0
ip link show docker0
ip link show ens33
docker exec web ip link show eth0
Docker daemon 的 bridge MTU 可通过 daemon 配置或启动参数设置,具体配置名和支持范围应以当前 Engine 文档为准。修改后通常需要重建相关网络,已有容器不会自动获得所有网络属性变化。
3. 用 DF 测试路径
IPv4 下可用:
docker exec web ping -M do -s 1472 -c 2 1.1.1.1
1472 + 20 字节 IPv4 头 + 8 字节 ICMP 头 = 1500。如果失败,再降低 payload:
docker exec web ping -M do -s 1400 -c 2 1.1.1.1
这不是对所有目标和所有协议的完整证明,因为:
- 目标可能不响应 ICMP;
- 中间设备可能过滤 ICMP;
- IPv6 使用不同的测试参数;
- TCP 的分段和 MSS 行为不同。
更直接的 TCP 诊断可以结合抓包观察是否出现 SYN 成功但后续大段数据重传。对 TCP 而言,MSS 通常近似为:
在无 TCP option 的简化情况下,IPv4/TCP 为 MTU - 20 - 20;实际 TCP 时间戳等 option 会影响可用负载,但 MSS 通告机制通常由端点在握手时协商。
4. MTU 故障的典型症状
MTU 或 PMTUD 故障经常表现为:
ping小包成功;- TCP 三次握手成功;
- 下载小响应成功,大响应卡住;
- TLS ClientHello 或服务器证书阶段重传;
- 某些网站可访问,另一些网站失败;
- UDP 应用完全失败;
- 只有跨主机或经过 VPN 的容器失败。
此时不要先把问题归因于 DNS。DNS 查询本身可能只有几十到几百字节,而真正失败的是后续应用数据。
调整 MTU 的原则是让容器接口、Docker bridge、底层路径之间满足可承载条件,而不是随意把所有接口改成 1400。过小的 MTU 会增加包数量和协议开销;过大的 MTU 则会导致分片或黑洞。修改前应测量实际路径并验证所有相关网络,尤其是多网络容器。
七、抓包:必须在正确的 namespace 和接口上观察
1. tcpdump 看到的只是某个观察点
抓包不是“查看网络真相”,而是查看某个接口在某个 namespace、某个时间点看到的帧。一个数据包可能在不同位置具有不同的地址:
容器 eth0:
172.18.0.2:40000 → 203.0.113.20:443
宿主机物理网卡 ens33:
192.0.2.10:52000 → 203.0.113.20:443
前者是 NAT 前,后者是 NAT 后。若只在 ens33 上抓包,看不到容器私有地址并不表示容器没有发送。
容器内抓包:
docker exec web tcpdump -ni eth0 'host 203.0.113.20 and port 443'
如果镜像没有 tcpdump,使用共享 namespace 的诊断容器:
docker run --rm -it \
--network container:web \
nicolaka/netshoot \
tcpdump -ni eth0 'port 443'
宿主机 bridge 上抓包:
sudo tcpdump -ni docker0 'host 172.18.0.2'
用户自定义网络则可能是:
sudo tcpdump -ni br-91d2c3 'host 172.18.0.2'
宿主机物理出口抓包:
sudo tcpdump -ni ens33 'host 203.0.113.20 and port 443'
接口名必须替换为实际名称:
ip -br link
docker network inspect appnet
2. 一次出站连接应看到什么
以容器访问外部 HTTPS 为例,在容器 eth0 上通常观察到:
SYN
SYN, ACK
ACK
TLS/HTTP 数据
若在容器 eth0 上连 SYN 都没有:
- 应用可能没有真正发起连接;
- DNS 解析后选择了其他地址族;
- 路由选择失败;
- socket 被本地策略阻止。
若容器上有 SYN,但 bridge 上没有:
- veth、bridge、namespace 连接异常;
- 抓错了 bridge;
- 过滤条件写错;
- 数据包在更早的 hook 被丢弃。
若 bridge 上有 SYN,但物理网卡上没有:
- 宿主机路由、FORWARD 规则或防火墙丢弃;
- NAT 前规则未匹配;
- 物理出口并不是你猜的接口。
若物理网卡上发出 SYN,但没有 SYN-ACK:
- 上游网络、目标服务、防火墙或回程路径有问题;
- 也可能目标明确丢弃了该源地址。
若物理网卡上能看到回包,但容器内看不到:
- conntrack/NAT 反向转换异常;
- 宿主机转发规则丢弃;
- 回包路由错误;
- 抓包过滤条件忽略了 NAT 后地址。
3. 端口发布的抓包方法
启动测试容器:
docker run -d --name web -p 8080:80 nginx:alpine
在宿主机上观察入站:
sudo tcpdump -ni any 'tcp port 8080'
再从另一台机器访问:
curl -v http://HOST_IP:8080/
同时在 Docker bridge 上观察容器端口:
sudo tcpdump -ni docker0 'tcp port 80'
如果宿主机 8080 有 SYN,而 bridge 上没有转为容器 80 的流量,检查端口发布规则和防火墙;如果 bridge 上有 SYN,而容器内没有,检查目标容器和接口;如果容器内收到 SYN 但没有响应,检查 nginx 监听地址、容器内防火墙或应用状态。
tcpdump -i any 适合快速观察,但它的接口归属、重复显示和链路层信息不如在具体接口上抓包清晰。确认路径后应改用具体接口,并用时间、五元组和方向缩小过滤范围。
八、一个可重复的端到端诊断实验
下面的实验构造一个最小的服务发现和端口测试环境:
docker network create diagnet
docker run -d \
--name diag-server \
--network diagnet \
nginx:alpine
docker run --rm -it \
--name diag-client \
--network diagnet \
busybox sh
在 diag-client 内执行:
cat /etc/resolv.conf
ip addr
ip route
nslookup diag-server
wget -S -O - http://diag-server/
预期逻辑是:
ip addr显示客户端在diagnet子网中的地址;ip route显示该子网直连路由和默认路由;nslookup diag-server返回diag-server的容器 IP;wget连接该 IP 的80端口并得到 nginx 响应。
然后在宿主机查看网络对象:
docker network inspect diagnet
ip -br addr
网络检查结果应能对应到:
- 一个 Docker bridge;
- 一个网关地址;
- 两个容器端点;
- 每个容器的 IPv4 地址。
清理实验:
docker rm -f diag-server
docker network rm diagnet
如果第 3 步失败,优先调查网络归属和 embedded DNS;如果第 3 步成功、第 4 步失败,优先检查 nginx 是否运行、端口监听和 bridge 连通性;如果容器内访问成功而宿主机访问发布端口失败,则是另一条端口发布路径,不能用容器间成功来证明发布配置正确。
九、把“连接失败”拆成可验证的层次
一个有效的诊断流程应从低层到高层逐步增加条件,而不是一次执行一个复杂应用命令。
第一步:确认容器状态和网络归属
docker ps
docker inspect -f '{{.State.Status}} {{.State.Running}}' api
docker inspect -f '{{json .NetworkSettings.Networks}}' api
docker network inspect appnet
确认容器没有持续重启,并且客户端、服务端都加入了预期网络。
第二步:确认接口和路由
docker exec api ip addr
docker exec api ip route
docker exec api ip neigh
判断:
- 是否存在
eth0; - 是否有 IP;
- 是否存在目标网段的直连路由;
- 是否存在默认路由;
- 网关和目标邻居是否能解析到 MAC。
ip neigh 出现 INCOMPLETE 或 FAILED,说明 ARP/邻居发现没有完成,但原因可能是 bridge、地址冲突、接口状态或目标端点问题。
第三步:把 DNS 与 IP 连接分开
docker exec api getent hosts db
docker exec api nc -vz -w 2 db 5432
也可以直接连接解析得到的 IP:
docker exec api nc -vz -w 2 172.18.0.3 5432
对比结果:
| 名称解析 | 直接 IP | 更可能的问题 |
|---|---|---|
| 失败 | 未测试 | DNS、网络归属、embedded DNS |
| 成功 | 失败 | 端口监听、服务状态、转发/过滤 |
| 成功 | 成功 | 应用层协议、认证、TLS 或请求内容 |
| 短名称失败,FQDN 成功 | 成功 | search domain、服务名或别名配置 |
第四步:确认服务监听地址
docker exec db ss -lntp
重点区别:
127.0.0.1:5432 只接受容器自身 loopback 连接
0.0.0.0:5432 接受 IPv4 各接口地址
172.18.0.3:5432 只绑定该具体 IPv4 地址
[::]:5432 取决于 IPv6 dual-stack 配置
ss 显示监听不等于应用一定健康;还需结合应用日志和协议级客户端测试。
第五步:确认防火墙、转发和 NAT
sysctl net.ipv4.ip_forward
sudo iptables -S FORWARD
sudo iptables -S DOCKER-USER
sudo iptables -t nat -S
sudo nft list ruleset
不要只看计数为零就断定规则未匹配。规则计数可能被重置、使用了另一后端、流量走了不同地址族,或连接已经存在而没有再次经过相同路径。最好在短时间窗口内执行测试并观察计数变化:
sudo iptables -t nat -L -n -v
第六步:在两个以上观察点抓包
最少选择:
容器 eth0 + 宿主机物理出口
对于端口发布,再增加:
宿主机 bridge
用 SYN、SYN-ACK、RST、重传和 ICMP 错误判断丢包位置。TCP RST 通常说明某个节点主动拒绝或连接状态不匹配;静默超时更像丢弃、路由错误或回程失败,但仍需以抓包为证据。
第七步:最后检查 MTU 和应用协议
当小包和握手成功、较大数据失败时,执行 DF 测试并观察:
docker exec api ping -M do -s 1400 -c 2 1.1.1.1
然后使用真实协议客户端验证。例如:
docker exec api curl -v --max-time 5 https://example.com/
curl 输出可以区分 DNS、TCP、TLS 和 HTTP 阶段,但它不能代替抓包;它只能说明应用观察到的最终结果。
十、典型失败场景与反例
场景一:把容器名当作任意网络中的全局 DNS 名称
错误假设:
只要容器名是 db,任何容器都能解析 db
实际情况是名称解析具有网络作用域。容器是否能解析某名称,取决于它们是否连接到同一个提供该记录的 Docker 网络,以及名称是否是容器名、服务名或别名。
反例:
docker run -d --name db postgres:16
docker run --rm --network appnet busybox nslookup db
即使宿主机上存在名为 db 的容器,appnet 中的客户端也不一定能解析它,因为 db 未加入 appnet。
场景二:DNS 成功就认为数据库可用
getent hosts db
成功只能证明名称解析返回了地址。数据库可能:
- 尚未完成初始化;
- 只监听 loopback;
- 端口配置不同;
- 主动拒绝连接;
- 要求 TLS 或认证;
- 因连接数限制关闭新连接。
必须继续测试 TCP,再测试数据库协议。
场景三:宿主机能访问发布端口,所以容器一定能访问
宿主机访问:
curl http://127.0.0.1:8080
成功只验证了宿主机到发布端口的路径。它没有证明:
- 外部网卡可以访问;
- 容器间 DNS 正常;
- 容器访问宿主机发布端口的 hairpin 路径正常;
- IPv6 路径正常。
容器间通信优先使用共享 Docker 网络中的服务名和容器端口,并单独测试宿主机发布路径。
场景四:关闭防火墙后问题消失,于是认为 Docker 配置错误
关闭防火墙可能绕过:
- FORWARD 策略;
DOCKER-USER过滤规则;- nftables 与 iptables 规则冲突;
- 云主机安全组之外的本机过滤。
但这只是证明“过滤路径参与了故障”,不是证明 Docker 自动规则错误。正确修复应根据方向、接口、源地址、目的端口和连接状态添加最小范围规则,然后恢复安全策略。
场景五:把 MTU 统一改成 1500
1500 是常见以太网值,不是路径保证。若底层存在隧道,bridge 仍设置为 1500 可能导致大包黑洞;如果没有隧道而盲目改成 1400,又会增加额外开销。MTU 调整必须以实际路径测试、抓包和 PMTUD 行为为依据。
十一、生产环境中的状态、并发和恢复风险
1. 容器 IP 不是稳定身份
容器重建、Compose 扩缩容和网络重新创建都可能改变 IP。服务发现应依赖网络范围内的服务名或别名,连接池需要处理连接失效和重新解析。
同时,DNS 记录变化并不保证所有客户端立即感知。应用可能缓存 DNS,glibc、语言运行时、连接池和代理各自有不同缓存策略。服务已重建但客户端仍连接旧 IP,是常见的“网络看起来不稳定”现象。
2. 规则和连接跟踪存在时间状态
防火墙规则改变后,已有连接可能继续使用旧的 conntrack/NAT 映射;新连接则走新规则。因此:
旧连接成功、新连接失败
不一定矛盾。反过来也可能出现规则已修复,但旧连接状态仍然异常。
重启 Docker、删除网络或清空 conntrack 都可能中断大量业务连接。运维操作应先记录:
docker network inspect appnet > appnet-before.json
sudo iptables-save > iptables-before.txt
sudo nft list ruleset > nft-before.txt
并在低风险窗口执行。恢复不仅是“让 ping 通”,还要验证 DNS、TCP、TLS、应用协议和外部访问路径。
3. 地址重叠会产生确定性的错误路由
如果 Docker 网络使用了与宿主机办公网、VPN 或云 VPC 重叠的网段,例如都使用 172.18.0.0/16,最长前缀匹配可能把本应发往外部网络的流量送入 Docker bridge。
检查:
ip route
docker network inspect appnet
形式上,若宿主机存在:
172.18.0.0/16 dev br-xxxx
172.18.0.0/16 dev tun0
路由优先级、添加顺序和策略路由会决定实际出口;这种配置本身就容易造成不可预测的冲突。修复应重新规划 Docker address pool 和 VPN/VPC 网段,而不是依靠临时静态路由掩盖问题。
4. Rootless 和特殊平台不能直接套用宿主机 bridge 结论
Rootless Docker 可能使用用户态网络实现,宿主机上不一定出现与 rootful Docker 相同的 docker0、iptables NAT 和 veth 结构。Docker Desktop 的容器网络又位于 Linux VM 内,用户在 macOS 或 Windows 宿主机上抓物理网卡,未必能直接看到容器内部流量。
因此诊断前应先确认:
docker info
docker context show
docker info --format '{{json .SecurityOptions}}'
文章中的 namespace、bridge、iptables/nftables 路径主要适用于 Linux rootful Docker Engine;其他模式应以实际网络后端和平台边界为准。
十二、用证据构造故障结论
网络故障诊断的目标不是收集最多命令输出,而是验证数据路径上的必要条件。例如,容器访问外部 HTTPS 至少需要:
其中:
- :名称解析成功,或调用方已经获得正确 IP;
- :路由选择了有效下一跳;
- :bridge、宿主机转发和防火墙允许数据包通过;
- :TCP/UDP 传输层完成预期交互;
- :应用协议、TLS、认证和服务状态正常。
如果调用方直接使用 IP,则 不参与该次连接;如果访问同一 bridge 内的容器,可能不需要外部 NAT;如果使用 UDP,则不能用 TCP 三次握手的证据替代 UDP 验证。
一个严谨的结论应类似:
容器 api 的 DNS 查询 db 成功,db 解析为 172.18.0.3;
api eth0 上能看到 SYN,db eth0 上也收到 SYN;
db 仅监听 127.0.0.1:5432,没有监听 172.18.0.3:5432;
因此故障位于服务监听地址,而不是 Docker DNS 或 bridge。
而不是笼统地说:
Docker 网络坏了。
同样,若容器内 eth0 看到 SYN,宿主机物理网卡看到 NAT 后的 SYN,但回包只出现在物理网卡、未出现在容器 bridge,则应继续检查 conntrack、反向 NAT、FORWARD 规则和回程路由,而不能仅凭“外网已经收到请求”判断服务端有问题。
Docker 网络由 Linux namespace、veth、bridge、路由、过滤、conntrack、NAT 和 DNS 共同构成。把名称解析、二层连通、三层转发、端口发布、路径 MTU 和抓包观察点分开,才能把“连接失败”还原成一个可验证的故障路径:哪个 namespace 发出了什么、经过哪个接口、是否被转换、在哪个位置消失,以及回包是否沿着状态表返回。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker 磁盘治理:Layer、Build Cache、Volume、日志和安全清理
- 下一篇:Docker OOM 与性能诊断:cgroups、PSI、CPU Throttle、I/O 和内存
- 延伸:Docker Bridge 与 iptables/nftables:NAT、端口发布、转发和冲突
- 延伸:Docker 容器 DNS:嵌入式解析、Service Name、Search Domain 和故障
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论