Docker-05 网络原理与排障:bridge、veth、NAT 和服务发现
1. Docker 网络的基本模型
默认 bridge 网络中,一个容器访问外部服务大致经过:
容器 eth0
│ veth pair
docker0 bridge ── 宿主机 eth0 ── 网关/互联网
│
iptables/nftables NAT(源地址转换)
容器拥有自己的 network namespace,因此有独立的网卡、IP、路由表、端口空间和 netfilter 视图。veth pair 是一对虚拟以太网设备,一端位于容器 namespace,另一端接入宿主机 bridge。
数据包从容器发出时,bridge 在二层转发,宿主机路由和 netfilter 决定是否转发、是否做 SNAT;返回包依赖 conntrack 恢复连接状态。
2. 查看网络对象
docker network ls
docker network inspect bridge
docker inspect -f '{{json .NetworkSettings.Networks}}' web | jq
ip link show
ip addr show docker0
ip route
ss -lntp
创建一个自定义 bridge:
docker network create \
--driver bridge \
--subnet 172.30.10.0/24 \
--gateway 172.30.10.1 \
app-net
docker run -d --name api --network app-net example/api:dev
自定义 bridge 比默认 bridge 更适合应用,因为容器之间可以使用内置 DNS 通过服务名发现;默认 bridge 的历史行为和名字解析能力较弱。
3. Docker 的网络模式
3.1 bridge
默认的隔离网络。容器有独立 IP,通过 bridge 与宿主连接;对外访问通常做 SNAT,对内暴露服务使用 -p。
docker run -d --name web --network bridge nginx:alpine
3.2 host
容器直接使用宿主机 network namespace,没有独立容器 IP,端口也不需要 -p。网络性能和可见性较好,但隔离性明显降低,端口冲突由宿主机直接决定。
docker run --network host nginx:alpine
3.3 none
只保留 loopback,适合完全不需要网络或需要手工配置网络的任务:
docker run --rm --network none alpine ip addr
3.4 container
两个容器共享同一个 network namespace,共享 IP、端口和 loopback,适用于 sidecar 类场景,但端口冲突也会共享:
docker run -d --name main example/api:dev
docker run --rm --network container:main alpine ss -lnt
3.5 overlay
跨主机网络通常需要 overlay 和控制面(例如 Swarm 或其他编排系统)。单机 Docker Compose 不会自动提供跨主机服务发现和高可用网络。
4. 端口映射和 EXPOSE
docker run -d --name web -p 127.0.0.1:8080:80 nginx:alpine
docker port web
-p [host_ip:]host_port:container_port 在宿主机发布端口。绑定到 127.0.0.1 只允许宿主机访问;省略地址通常会监听宿主机所有地址,可能意外暴露到公网。
EXPOSE 80 只是镜像元数据和文档提示,不能代替 -p。而 -P 会把所有 EXPOSE 端口随机映射到宿主机端口,适合临时测试,不适合稳定的服务入口。
5. Compose 中的 DNS 和服务发现
同一个 Compose 网络内,服务名会解析到服务对应的容器 IP:
Compose 项目中如何访问运行在宿主机上的服务(包括 host.docker.internal、Linux 的 host-gateway、监听地址和分层排障),见《Docker Compose:容器访问宿主机端口》。
services:
api:
image: example/api:dev
db:
image: postgres:16
api 容器中应连接 db:5432,而不是 localhost:5432。localhost 指的是当前容器的 network namespace;只有共享 network namespace 的容器才会共享 localhost。
容器重建后 IP 可能改变,但服务名保持不变。因此应用应该使用 DNS 名称而不是硬编码容器 IP。连接池还要处理 DNS 变化和数据库重启。
6. 容器之间的网络隔离
可以把前端和数据库放到不同网络:
services:
proxy:
image: nginx:alpine
networks: [edge, app]
api:
image: example/api:dev
networks: [app]
db:
image: postgres:16
networks: [data]
networks:
edge: {}
app: {}
data:
internal: true
internal: true 主要用于限制网络的外部连通路径;真正的安全边界仍取决于宿主机防火墙、应用认证和编排平台策略。应用网络隔离的目标应是最小可达集合,而不是简单创建很多网络。
7. DNS、代理和 MTU
容器内 DNS 通常由 Docker 提供的 stub resolver 转发到宿主机配置。检查:
docker exec api cat /etc/resolv.conf
docker exec api getent hosts db
docker exec api getent hosts example.com
常见问题:
- 宿主机能解析,容器不能解析:检查 Docker DNS、公司 VPN、代理和防火墙。
- 能解析但 HTTPS 失败:可能是 CA 证书、代理或时间问题,不一定是网络不通。
- 大包或长连接异常:检查 overlay/VPN 场景的 MTU、分片和中间设备。
不要把 /etc/resolv.conf 永久手工改在容器可写层;应通过 daemon、Compose dns 或宿主机网络配置解决。
8. 网络排障命令
8.1 从外到内
# 宿主端口是否监听
ss -lntp | rg ':8080'
# Docker 是否建立端口映射
docker port web
docker inspect web | jq '.[0].HostConfig.PortBindings'
# 容器内应用是否监听正确地址
docker exec web ss -lntp
docker exec web wget -qO- http://127.0.0.1:80
# 同网络容器之间能否访问
docker run --rm --network app-net curlimages/curl:8.10.1 \
http://api:8080/healthz
应用只监听 127.0.0.1 时,其他容器无法通过容器 IP 访问;容器服务通常应监听 0.0.0.0,再用网络和防火墙控制可达范围。
8.2 进入 network namespace
PID=$(docker inspect -f '{{.State.Pid}}' api)
sudo nsenter -t "$PID" -n ip addr
sudo nsenter -t "$PID" -n ip route
sudo nsenter -t "$PID" -n ss -lntp
宿主机的路由表不等于容器内的路由表。排查容器网络时要同时看 namespace 内视图、宿主 bridge、路由和 netfilter 规则。
8.3 抓包
sudo tcpdump -ni any port 8080
sudo tcpdump -ni docker0 host 172.30.10.2
容器没有 tcpdump 时,在宿主机抓 veth/bridge 通常更可靠;先用 iflink、ethtool 或 nsenter 找到对应接口。
9. iptables/nftables 和 Docker
Docker 会创建用于转发、端口发布、隔离和 NAT 的规则。不同发行版可能通过 iptables-nft 兼容层展示规则;不要默认 iptables 和 nftables 是两套互不相关的规则。
sudo iptables -t nat -S DOCKER
sudo iptables -S DOCKER-USER
sudo nft list ruleset
自定义防火墙规则要注意顺序和 owner:Docker、firewalld、kube-proxy、CNI 可能同时管理网络。通常把主机侧额外访问控制放在 Docker 提供的 DOCKER-USER 链或发行版支持的防火墙入口,并记录规则归属,避免重启后被覆盖。
10. 安全建议
- 不要把数据库端口无条件发布到
0.0.0.0。 - Compose 内部服务只加入需要的网络,不要全部使用
network_mode: host。 - 关闭不必要的容器出网,使用代理或 egress 防火墙控制供应链访问。
- 不要依赖容器 IP 做认证;使用 TLS、服务身份和应用级鉴权。
- 网络连通不代表应用健康;健康检查要验证真正的依赖和业务状态。
11. 亲手追踪一对 veth
启动实验容器:
docker network create --subnet 172.30.20.0/24 network-lab
docker run -d --name network-lab-web \
--network network-lab \
-p 127.0.0.1:18080:80 \
nginx:1.27-alpine
PID=$(docker inspect -f '{{.State.Pid}}' network-lab-web)
docker inspect network-lab-web | jq '.[0].NetworkSettings.Networks'
从容器 namespace 看 eth0:
sudo nsenter -t "$PID" -n ip -d link show eth0
sudo nsenter -t "$PID" -n ip addr show eth0
sudo nsenter -t "$PID" -n ip route
sudo nsenter -t "$PID" -n cat /etc/resolv.conf
记录容器 eth0 的 iflink,它通常对应宿主侧 veth 的 ifindex:
PEER_INDEX=$(sudo nsenter -t "$PID" -n cat /sys/class/net/eth0/iflink)
ip -o link | rg "^${PEER_INDEX}:"
bridge link
现在可以同时抓宿主入口、bridge 和容器侧:
sudo tcpdump -ni any 'tcp port 18080 or tcp port 80'
curl -v http://127.0.0.1:18080
观察 TCP SYN 在宿主端口发布规则后被送往容器的 80 端口。不同 Engine/内核可能通过 nftables、iptables 兼容层或 userland proxy 的不同组合实现,排障时以当前 docker info、进程、规则集和抓包为准。
11.1 手工断网与恢复
docker network disconnect network-lab network-lab-web
docker inspect network-lab-web | jq '.[0].NetworkSettings.Networks'
curl -v --max-time 2 http://127.0.0.1:18080
docker network connect network-lab network-lab-web
curl -fsS http://127.0.0.1:18080 >/dev/null
断开网络后容器进程仍可能显示 running,这说明进程状态、网络可达和业务健康是三个不同维度。
12. 端口发布的详细数据路径
客户端访问 HOST_IP:8080 时,可以按下面的控制点理解:
客户端 SYN
↓ 宿主网卡/loopback
宿主 netfilter PREROUTING/OUTPUT
↓ DNAT 或等价转发机制
容器地址:80
↓ veth → 容器 network namespace
应用监听 socket
↓
返回包经 conntrack 反向转换
四类常见失败对应不同证据:
| 失败点 | 典型现象 | 关键证据 |
|---|---|---|
| 宿主未发布端口 | docker port 无结果 | inspect PortBindings |
| 防火墙/安全组丢包 | 客户端超时 | 两端抓包、counter、安全组日志 |
| 应用监听错误端口或只监听 loopback | 到容器 IP 被拒绝/超时 | 容器 namespace 内 ss -lntp |
| 应用协议/TLS 错误 | TCP 建连但 HTTP/TLS 失败 | curl -v、应用日志、证书检查 |
ports: ["8080:80"] 通常绑定所有宿主地址;管理面、数据库和本地开发入口更安全的写法是 127.0.0.1:8080:80,公网再经受控代理/防火墙暴露。
13. conntrack、MTU 与连接池的生产问题
13.1 conntrack 表耗尽
Docker NAT 连接通常被 conntrack 跟踪。高并发短连接、DNS 风暴或攻击可能让表接近上限:
sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max
sudo conntrack -S 2>/dev/null || true
症状可能是随机超时、丢包,而不是所有容器同时完全断网。先减少不必要短连接、修正连接池与超时,再基于内存和流量评估表上限;只调大参数会把根因推迟。
13.2 MTU 黑洞
VPN、VXLAN/overlay 和云隧道会增加报文头。底层 MTU 没留空间时,小包/Ping 正常,大 HTTP 响应或 TLS 握手可能卡住:
ip link show
docker network inspect network-lab | jq '.[0].Options'
ping -M do -s 1400 TARGET_IP
tracepath TARGET_IP
不要用“能 ping 通”证明业务网络正常。抓包检查重传和 ICMP fragmentation-needed,统一规划宿主、Docker network 和隧道 MTU。
13.3 DNS 与长连接
服务容器重建后 IP 会改变。应用如果只在启动时解析一次 DNS 并永久缓存旧 IP,或连接池不检测断链,就可能持续请求旧实例。生产客户端应有合理 DNS 策略、连接健康检查、超时、重试和退避;重试必须考虑请求幂等。
13.4 清理实验
docker rm -f network-lab-web
docker network rm network-lab
14. 本篇小结
Docker 网络不是一张“虚拟网卡”这么简单,而是 network namespace、veth、bridge、路由、netfilter、NAT 和 DNS 的组合。端口问题按“宿主监听 → 映射规则 → 容器监听 → 应用响应”排查,服务发现优先使用 Compose 服务名,跨主机则交给真正的编排网络。