「容器连不上」是最难定位的一类故障,因为一次连接要穿过四层:容器自己的网络命名空间、宿主上的 veth 与网桥、iptables 的 NAT 链、以及 DNS 解析。任何一层出问题表现都是「连不上」,但排查入口完全不同。本篇按这四层给出固定的排查顺序,让每次排障都能收敛到唯一原因,而不是靠猜。
1. 先确定故障发生在哪一层
拿到「服务不可达」的报障后,先做一次分层定位,不要一上来就抓包:
# 第 0 步:确认容器是否在运行、进程是否监听
docker ps --filter name=web --format '{{.Names}}\t{{.Status}}\t{{.Ports}}'
docker exec web ss -lntp # 应用是否真的在监听 0.0.0.0
| 症状 | 最可能的层 | 首个检查命令 |
|---|---|---|
docker ps 无该容器 | 容器层 | docker ps -a 看退出原因 |
容器内 ss 无监听 | 应用层 | 看容器日志与启动命令 |
容器内 curl localhost 通、外部不通 | 端口发布 / iptables | docker port、iptables -t nat -L |
| 容器间按名字不通 | DNS 层 | docker exec web cat /etc/resolv.conf |
| 小包通、大包卡住 | MTU 层 | ping -M do -s 1472 |
| 偶发连接超时、无规律 | conntrack / 内核参数 | conntrack -S、dmesg |
一个常被忽略的前提:应用必须监听 0.0.0.0 而不是 127.0.0.1。容器内监听回环地址的服务,端口映射再多也访问不到。
docker exec web ss -lntp
# LISTEN 0 4096 0.0.0.0:8080 ← 正确
# LISTEN 0 4096 127.0.0.1:8080 ← 宿主无法转发进来
2. 进入目标命名空间:nsenter 与 netshoot
容器网络排障的第一步是「站在容器里看」。容器内通常没有 tcpdump、ip、dig 这些工具,有两种补法。
2.1 nsenter 直接切入
# 拿到容器 PID 与其网络命名空间
pid=$(docker inspect -f '{{.State.Pid}}' web)
nsenter -t "$pid" -n ip addr
nsenter -t "$pid" -n ip route
nsenter -t "$pid" -n ss -lntp
nsenter -t "$pid" -n iptables -t nat -L -n # 若容器内装了 iptables
nsenter -t <pid> -n 之后执行的所有命令都运行在容器的网络命名空间内,用的是宿主的二进制,因此不需要容器里有任何工具。这是最轻量的排查方式。
2.2 netshoot 共享命名空间
需要 tcpdump、dig、mtr、curl 一整套工具时,用 nicolaka/netshoot 起一个临时容器,通过 --net=container: 共享目标容器的命名空间:
docker run --rm -it --net container:web nicolaka/netshoot
# 容器内即可直接使用完整工具链
# 进入后典型操作
tcpdump -i eth0 -nn port 8080
dig @127.0.0.11 db
mtr -rwzbc 10 8.8.8.8
nslookup api.example.com
curl -v http://db:5432
共享命名空间的关键优势是:看到的网卡、路由、DNS 配置与目标容器完全一致,不存在「在宿主上抓包抓不到容器流量」的问题。
# 抓取所有容器流量(宿主侧,需要特权)
docker run --rm -it --net host --privileged nicolaka/netshoot \
tcpdump -i docker0 -nn
2.3 定位 veth 对
宿主上每个容器网卡都对应一对 veth,一端在容器命名空间内(eth0),另一端在宿主命名空间并挂在网桥上。找到宿主端的方法是比对索引:
# 容器内查看 eth0 的 iflink(指向宿主端 veth 的 ifindex)
nsenter -t $(docker inspect -f '{{.State.Pid}}' web) -n \
ip -o link show eth0
# 3: eth0@if12: ...
# 宿主上找 ifindex 为 12 的网卡
ip -o link show | awk -F': ' '$1 ~ /12/ {print}'
# 12: veth1a2b3c@if3: ...
# 网桥成员列表
bridge link show
拿到 veth 名后就能在宿主上直接对它抓包:
tcpdump -i veth1a2b3c -nn -e
2.4 抓包位置的选择
一次容器间通信,包会在三个位置各出现一次,抓哪个位置决定了你能看到什么:
| 抓包位置 | 能看到 | 看不到 |
|---|---|---|
| 容器内 eth0 | 应用实际收发的包 | 被宿主规则丢弃的包 |
| 宿主 veth 端 | 是否进入/离开网桥 | 网桥内部的转发决策 |
| 宿主 docker0 | 所有容器流量的汇总 | 被 DNAT 前的原始目标地址 |
判断「包到底走到哪一步」的标准做法是同时在两端抓:
# 终端 A:容器内
docker run --rm -it --net container:web nicolaka/netshoot tcpdump -i eth0 -nn port 8080
# 终端 B:宿主 veth 端
tcpdump -i veth1a2b3c -nn port 8080
# 只有 A 有输出 → 包已进入容器,问题在应用侧
# 只有 B 有输出 → 包到达网桥但未进入容器,检查网桥与 iptables
# 两边都无输出 → 问题在更上游(路由、DNAT、防火墙)
这套「两端同时抓」的方法能把故障范围一次性砍掉一半,比反复猜测高效得多。
3. 端口发布与 iptables 链路
-p 8080:80 并不是「打开一个端口」,而是在宿主上插入两条 iptables 规则。理解这条链路是排查「外部访问不到」的关键。
iptables -t nat -L DOCKER -n --line-numbers
# DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80
iptables -t filter -L DOCKER -n
# ACCEPT tcp -- 0.0.0.0/0 172.17.0.2 tcp dpt:80
iptables -t filter -L FORWARD --line-numbers
# ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
# ACCEPT all -- 172.17.0.0/16 0.0.0.0/0
# ACCEPT all -- 0.0.0.0/0 172.17.0.0/16
| 链 | 作用 | 出问题时表现 |
|---|---|---|
| nat/DOCKER | DNAT,把宿主端口转到容器 IP | 完全连不上 |
| filter/DOCKER | 放行到容器端口的流量 | 连接被拒 |
| filter/FORWARD | 放行网桥间转发 | 容器间不通 |
| filter/DOCKER-USER | 用户自定义入口,最先匹配 | 自定义策略误拦 |
3.1 DOCKER-USER 链:唯一安全的自定义入口
Docker 会持续重建 DOCKER 链,手工插入的规则会被覆盖。要加自定义规则,只能写进 DOCKER-USER:
# 只允许 10.0.0.0/8 访问 8080
iptables -I DOCKER-USER -p tcp --dport 8080 ! -s 10.0.0.0/8 -j DROP
排查时若发现规则存在但仍不通,检查它是否被 -I 插到了链首之前的位置,以及是否有更靠前的 -j RETURN。
3.2 宿主防火墙与 Docker 的冲突
常见现象是「重启防火墙后容器全不可达」:ufw/firewalld 的默认策略会清空或覆盖 Docker 插入的规则。
# 确认 FORWARD 默认策略
iptables -L FORWARD -n | head -1
# Chain FORWARD (policy DROP) ← 会导致容器间不通
# firewalld 环境下需要把 docker0 加入 trusted 区域
firewall-cmd --permanent --zone=trusted --add-interface=docker0
firewall-cmd --reload
容器网络安全策略的完整设计(NetworkPolicy、零信任、mTLS)可参考 容器网络安全与隔离 。
4. DNS:最容易误判的一层
容器内的 DNS 由 Docker 的内嵌解析器 127.0.0.11 提供(自定义网络下),它把容器名与网络别名解析成容器 IP。
docker exec web cat /etc/resolv.conf
# nameserver 127.0.0.11
# options ndots:0
| 现象 | 原因 | 处理 |
|---|---|---|
| 容器名解析不了 | 容器不在同一自定义网络 | 加入同一 network |
| 外网域名解析失败 | 宿主 resolv.conf 指向本地 stub(如 127.0.0.53) | 配置 daemon 的 dns 或换用外部 DNS |
| 解析慢、间歇超时 | ndots 与搜索域导致多次查询 | 用 FQDN(末尾加点)或调整 dns-search |
| 内嵌解析器未生效 | 使用了默认 bridge 网络 | 默认 bridge 不支持容器名解析 |
# 在 netshoot 里验证解析链路
dig @127.0.0.11 db +short
dig @8.8.8.8 api.example.com +short
# 检查 Docker 实际下发的 DNS
docker inspect -f '{{json .HostConfig.Dns}}' web
默认 bridge 网络不支持按容器名解析,这是新手最常踩的坑:docker run --network bridge 起的两个容器,只能用 --link(已废弃)或 IP 互通。正确做法是自建网络:
docker network create appnet
docker run -d --name db --network appnet postgres:16-alpine
docker run -d --name web --network appnet myapp
docker exec web getent hosts db # 解析成功
宿主侧 DNS 与 DHCP 的排查方法可参考 Linux DNS 与 DHCP 服务 。
5. MTU 与分片:小包通、大包卡
这是最隐蔽的一类故障:ping 通、curl 小请求通、但上传文件或 TLS 握手卡死。根因是路径 MTU 小于容器网卡 MTU,且 ICMP 被中间设备丢弃,导致 PMTUD(路径 MTU 发现)失效。
# 逐步增大包大小,找出不分片的临界值
ping -M do -s 1472 8.8.8.8 # 1472 + 28 = 1500
# 若 1472 失败而 1400 成功,说明路径 MTU 在 1400~1500 之间
# 查看容器与宿主的 MTU
ip link show docker0 | grep mtu
nsenter -t $(docker inspect -f '{{.State.Pid}}' web) -n ip link show eth0
常见成因与处理:
| 成因 | 表现 | 处理 |
|---|---|---|
| VPN/隧道叠加(WireGuard、IPsec) | 隧道内大包丢失 | 把 docker0 与容器 MTU 调小到 1280~1400 |
| 云厂商 overlay 网络 | 跨节点通信大包失败 | 按云文档设置 MTU |
| 中间设备丢 ICMP | PMTUD 失效 | 用 MSS clamping |
# 修改 docker0 与容器默认 MTU
# /etc/docker/daemon.json
{
"mtu": 1400
}
# 或按网络粒度
docker network create --opt com.docker.network.driver.mtu=1400 appnet
# MSS clamping(治本,让 TCP 主动减小段大小)
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu
6. conntrack 表满与连接超时
Linux 的 netfilter 为每条连接维护一条 conntrack 记录。容器场景下连接数容易暴涨(健康检查、短连接、服务网格 sidecar),一旦表满,新连接被静默丢弃。
# 查看当前用量与上限
sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
# net.netfilter.nf_conntrack_count = 131072
# net.netfilter.nf_conntrack_max = 131072
# 统计丢包事件
conntrack -S
# 关注 insert_failed 与 drop 计数
# 内核日志中的典型告警
dmesg | grep -i "conntrack: table full"
处理方式:
# 1. 提高上限(按内存估算,每条约 300 字节)
sysctl -w net.netfilter.nf_conntrack_max=524288
# 2. 缩短已建立连接的保留时间,加速回收
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=3600
# 3. 持久化
# /etc/sysctl.d/99-conntrack.conf
配套要检查 nf_conntrack_buckets(哈希桶数),它决定查找性能;桶数远小于上限时会退化成链表查找,表现为 CPU 飙高。
sysctl net.netfilter.nf_conntrack_buckets
# 建议约为 max 的 1/4
7. 故障模式对照表与 SOP
把前面的排查经验收敛成一张对照表:
| 现象 | 可能原因 | 验证命令 |
|---|---|---|
| 外部访问不到 | 端口未发布、应用监听 127.0.0.1、DOCKER-USER 拦截 | docker port web、ss -lntp、iptables -L DOCKER-USER -n |
| 容器名解析失败 | 不在同一自定义网络、内嵌解析器未启用 | docker network inspect appnet、dig @127.0.0.11 |
| 容器访问外网失败 | 宿主 DNS 不可用、iptables MASQUERADE 被覆盖 | iptables -t nat -L POSTROUTING -n |
| 大包卡死 | MTU 不匹配、ICMP 被丢 | ping -M do -s 1472 |
| 偶发超时 | conntrack 表满、端口耗尽 | conntrack -S、ss -s |
| 重启防火墙后全断 | FORWARD 默认策略变 DROP | iptables -L FORWARD -n | head -1 |
| 容器间不通但都能上网 | 网桥被隔离、icc=false | docker network inspect -f '{{.Options}}' appnet |
固定 SOP,按顺序执行可覆盖绝大多数场景:
# 1. 容器是否活着、应用是否监听
docker ps --filter name=web
docker exec web ss -lntp
# 2. 端口映射是否存在
docker port web
docker inspect -f '{{json .NetworkSettings.Ports}}' web
# 3. 站在容器里看网络
docker run --rm -it --net container:web nicolaka/netshoot
# 4. 宿主侧看转发规则
iptables -t nat -L DOCKER -n
iptables -L FORWARD -n | head -1
# 5. 抓包定位丢包点(容器内与宿主 veth 各抓一次)
tcpdump -i eth0 -nn port 8080 -c 20
# 6. 内核层线索
dmesg -T | tail -50
conntrack -S
关于网桥、自定义网络与容器间通信的机制,可参考 Docker Volume 与网络 ;更底层的 eBPF/XDP 数据路径观测手段见 eBPF 与 XDP 数据面 。
8. 小结
容器网络排障的难点不在工具,而在顺序。固定按「应用监听 → 端口发布 → 命名空间内连通性 → iptables 转发 → DNS → MTU/conntrack」这六步走,每步都有明确的判定命令,就不会出现「抓了半天包发现应用根本没起」的浪费。
三条经验值得记住:
- 用
--net container:<name>起 netshoot,永远在目标命名空间里看问题,不要试图在宿主上间接推断。 - 端口映射的规则由 Docker 管理,自定义策略只能写进
DOCKER-USER,其他位置会被覆盖。 - 遇到「小包通、大包不通」直接怀疑 MTU,用
ping -M do -s二分定位,比抓包快得多。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。