一篇 Docker 端口"时通时不通"的排查实战
三台后端节点,同一个镜像,同样的启动参数,同一个负载均衡之下------一台能通,两台不能。
而更诡异的是,容器内部服务一切正常。
这篇文章记录了我和 Docker 网络问题从相遇到告别的全过程,希望能帮到同样踩坑的人。
背景
我负责的一套文档转换平台(DocTransform)部署在单机 Docker 环境中,由三个后端实例和一个 Nginx 前置负载均衡组成:
scss
Nginx (12580)
├── DocTransform-backend-0 (172.17.0.11:2580 → 宿主机 12581)
├── DocTransform-backend-1 (172.17.0.12:2580 → 宿主机 12582)
└── DocTransform-backend-2 (172.17.0.13:2580 → 宿主机 12583)
架构本身并不复杂。但客户反复反馈一个问题:服务有时候能访问,有时候不能。
第一印象:服务挂了?
"时通时不通",直觉告诉我------是不是容器在反复重启?
可是 docker ps 的结果是:
bash
DocTransform-backend-0 Up 14 hours 0.0.0.0:12581->2580/tcp
DocTransform-backend-1 Up 14 hours 0.0.0.0:12582->2580/tcp
DocTransform-backend-2 Up 14 hours 0.0.0.0:12583->2580/tcp
三个节点全部正常运行,没有任何重启。
docker stats 显示 CPU 0.07%,内存 2.16G/31G ------ 资源充裕。
容器内部正常,宿主机不行
我习惯性地做了分层排查:从容器内部开始测。
bash
# 容器内部 ------ 正常
$ docker exec -it DocTransform-backend-0 sh
sh-4.4# curl http://127.0.0.1:2580
<!DOCTYPE html>... <title>DocTransform</title> ... 200 OK
# 宿主机直接访问容器 IP ------ 拒绝连接!
$ curl http://172.17.0.11:2580
curl: (7) Failed connect to 172.17.0.11:2580; 拒绝连接
# 宿主机走 docker-proxy ------ Connection reset by peer!
$ curl http://127.0.0.1:12581
curl: (56) Recv failure: Connection reset by peer
# 宿主机走外网 IP ------ 直接拒绝!
$ curl http://10.166.151.124:12581
curl: (7) Failed connect to 10.166.151.124:12581; 拒绝连接
四种访问方式,只有第一种------容器内部的 localhost ------能通。其他全部失败。
这里有一个极其重要的细节:172.17.0.11:2580 拒绝连接 。但容器内 ss -tlnp 清晰显示服务监听在 0.0.0.0:2580 上。
这意味着容器本身的服务层没有问题,问题是容器和宿主机之间的通信链路。
这个现象缩小了排查范围:ICMP 能通,但 TCP 不通。问题不在应用层,在网络层。
一个奇怪的发现:docker-proxy 只监听 IPv6
检查宿主机端口监听状态时,我注意到:
bash
$ ss -tlnp | grep 12581
LISTEN 0 4096 :::12581 :::* users:(("docker-proxy",pid=78926,fd=4))
等一下------只有 :::(IPv6),没有 0.0.0.0(IPv4)。
但 docker-proxy 命令行参数明确指定了 -host-ip 0.0.0.0:
bash
/usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 12581 -container-ip 172.17.0.11 -container-port 2580
配置说绑定 0.0.0.0,实际操作却只绑了 :::。这在 Docker 的端口映射逻辑中是一个已知且容易让人困惑的特性:当 Docker 配置了 IPv6 支持时,docker-proxy 有时会优先(或只)创建 IPv6 双栈 socket,表现为 :::。
而问题的关键取决于系统的 net.ipv6.bindv6only 参数:
bash
$ cat /proc/sys/net/ipv6/bindv6only
0
bindv6only=0 意味着 IPv6 socket (:::12581) 应该能同时接收 IPv4 和 IPv6 连接。理论上没问题,但实际却是时通时不通。
这时我最先想到的解决方案是关闭 userland-proxy------让 Docker 不再启动用户态的 docker-proxy 进程,纯靠内核 iptables DNAT 做端口转发。
第一次尝试:关闭 userland-proxy
json
{
"userland-proxy": false
}
然后重启 Docker。
结果呢?DocTransform 后端节点之间的直接访问正常了,但------
所有 Nginx 容器都不能访问后端了。
原因是:关闭 userland-proxy 后,DNAT 规则带了一个 !docker0 的匹配条件,只有从 docker0 外部进来的流量才会被 DNAT 到容器。而我的 Nginx 容器也运行在 docker0 网桥上,它发出的请求不匹配这条规则,于是直接去连宿主机端口发现没人监听,Connection refused。
而整个架构中,不只 Nginx 这样调用后端------后端服务自己也配置了通过宿主机 IP 调用另一个 WPS 转换服务 (convert.server.url=http://10.166.151.124:6100/),同样走不通了。而且数据库连接也失败。
关闭 userland-proxy,等于切断了整个服务网格内部的通信链路。这条路走不通。
于是我恢复 userland-proxy: true,回到原点。
第二个发现:所有端口都只监听 IPv6
我开始检查系统中运行的所有容器:
bash
$ ss -tlnp | grep -E "12581|12582|12583|6100|5000"
LISTEN 0 4096 :::12581 :::* docker-proxy
LISTEN 0 4096 :::12582 :::* docker-proxy
LISTEN 0 4096 :::12583 :::* docker-proxy
LISTEN 0 4096 :::6100 :::* docker-proxy
LISTEN 0 4096 :::5000 :::* docker-proxy
...
所有容器,所有端口映射,docker-proxy 都只监听了 IPv6。 无一例外。
这不是某个端口的偶发问题,而是 Docker 守护进程的系统性行为。
根本原因分析
这台服务器的内核版本比较老:
bash
$ uname -a
Linux localhost.localdomain 3.10.0-327.el7.x86_64
# CentOS 7,3.10 内核
CentOS 7 早期的 3.10 内核配合 Docker 的 docker-proxy 用户态代理,存在多个已知的网络问题:
-
IPv4-mapped IPv6 socket 行为不一致 :虽然
bindv6only=0理论上允许 IPv6 socket 接受 IPv4 连接,但 3.10 内核的兼容层实现可能存在竞态条件,导致在高并发或特定网络路径下,IPv4 连接握手后被重置(RST)或超时。 -
docker-proxy 的 socket 绑定偏好 :Docker 在启用 IPv6 支持时,
docker-proxy优先创建 IPv6 双栈 socket,这在大多数现代系统上没问题,但在老内核上有兼容性风险。 -
Connection reset by peer的正确定位 :我反复遇到的curl: (56) Recv failure: Connection reset by peer是这个问题的典型症状------连接在三路握手阶段成功(TCP 连接建立),但在数据交换阶段被对方重置。这说明连接到达了docker-proxy,但在docker-proxy转发给容器的过程中出错。这和"端口不通就拒绝连接"(无进程监听)是完全不同的故障模式。 -
用户态代理的额外开销 :
docker-proxy作为用户态进程,每个连接都经过:scss客户端 → docker-proxy (用户态) → 内核 → 容器相比纯内核 iptables DNAT 路径多了一层上下文切换和数据拷贝,增加了出错概率。
更广泛的架构问题
排查中还发现了两个架构层面的隐患:
1. 服务间通信依赖宿主机 IP
整个服务的调用链是:
ini
客户 → Nginx (容器内) → 10.166.151.124:12581~12583 → docker-proxy → DocTransform 后端
→ DocTransform 调用 convert.server.url=http://10.166.151.124:6100/
→ Nginx (另一组容器) → 10.166.151.124:5000~5007 → docker-proxy → EngineCore 后端
每一跳都依赖宿主机 IP + docker-proxy 端口转发。任何一跳的 docker-proxy 出问题,整个链路就断了。
而且从 Nginx 日志中可以看到:
csharp
upstream: "http://10.166.151.124:5000/" → Connection timed out
upstream: "http://10.166.151.124:5002/" → Connection timed out
upstream: "http://10.166.151.124:5003/" → Connection timed out
Nginx 容器通过宿主机 IP 访问其他容器时,所有后端都超时。
2. 多实例共享数据卷
三个 DocTransform 后端挂载了同一个宿主机目录:
json
"Source": "/data/doctransform",
"Destination": "/data/DocTransform"
共享数据卷意味着:
- 上传的临时文件互相覆盖
- 转换任务 ID 冲突
- 文件锁竞争
- 状态文件互相踩踏
当并发任务较多时,这会加剧"时通时不通"的症状------服务进程在处理文件冲突时卡死,导致新连接无法被 accept。
但这不是问题的根源------即使只有一个容器运行,偶尔也会出现端口不通的情况。
为什么 12583 能通而 12581/12582 不能?
这是客户反馈中最让我困惑的一点------三个容器配置完全一样,为什么唯独 12583 能稳定访问?
从 DocTransform-backend-2 的日志中可以看到:
arduino
wps 转换完成,转换结果是false, 耗时 6 ms
正在使用重试机制重试wps 转换
office 转换为 pdf 完成,使用 aspose 补充
转换开始 使用 aspose 将 word 转为 pdf
word 转换为 pdf 完成,耗时 178 ms
原来 DocTransform 有 fallback 机制 :当 WPS 转换(通过 10.166.151.124:6100)失败时,它会自动降级到 Aspose 作为补充方案。
这意味着:12583 能响应的请求,可能恰好是那些不需要 WPS 转换、或者 Aspose 能直接处理的。 而 12581/12582 接到的请求需要 WPS 转换,在 WPS 服务本身也不稳定的情况下,处理线程被卡住,最终表现为完全不可用。
最终的诊断结论
整个问题的全貌如下:
arduino
docker-proxy 只绑定 IPv6 socket (:::)
│
├── 外部 IPv4 客户访问 → 时通时不通(bindv6only=0 兼容性不稳定)
│
├── 容器间通过宿主机 IP 调用 → 走 docker0 网桥,不匹配 DNAT 规则(关闭 userland-proxy 后更明显)
│
├── WPS 转换服务自身也不稳定(同样的 docker-proxy 问题)
│ └── EngineCore 的 Nginx 通过 10.166.151.124:5000~5007 连后端全部超时
│
├── 三个后端共享同一数据卷 → 文件冲突加剧故障
│
└── 应用层有 fallback 机制 → 12583 因请求路径差异表现出"能通"的假象
根因 :Docker 在 CentOS 7 早期 3.10 内核上的 docker-proxy socket 绑定行为异常,导致外部通过 IPv4 访问时稳定性极差。
最终解决方案
下面是我根据实际排查得出的建议方案(并非都已在生产实施,整理出来供参考):
方案一:升级内核 / Docker 版本(治本)
CentOS 7 的 3.10.0-327 内核太老了。升级到较新的内核(如 ELRepo 的 5.x/6.x 主线内核)或升级 Docker 版本,内核层面修复 IPv6 socket 映射的稳定性和 docker-proxy 的 socket 行为。
方案二:自定义网络 + 容器名 DNS(架构改造)
将所有容器加入同一个自定义 bridge 网络,通过容器名互相访问,彻底绕过 docker-proxy:
bash
# 创建自定义网络
docker network create docplatform-net
# 所有容器加入该网络
docker run -d --network docplatform-net --name DocTransform-backend-0 ...
docker run -d --network docplatform-net --name DocTransform-frontend ...
Nginx upstream 直接写容器名:
nginx
upstream do transform {
server DocTransform-backend-0:2580;
server DocTransform-backend-1:2580;
server DocTransform-backend-2:2580;
}
应用配置中的 convert.server.url 也改为容器名:http://EngineCore-frontend:6100/。
这是 Docker 官方推荐的最佳实践:容器之间通过容器名通信,只有对外暴露的端口才用 -p 映射。
方案三:独立数据卷
给每个后端实例挂载独立目录,避免文件冲突:
bash
docker run -d -v /data/doctransform-0:/data/DocTransform ... backend-0
docker run -d -v /data/doctransform-1:/data/DocTransform ... backend-1
docker run -d -v /data/doctransform-2:/data/DocTransform ... backend-2
方案四:当前恢复 userland-proxy(保底)
如果一时无法做架构改造,恢复 userland-proxy: true 就是最短路径让一切回到可用状态。然后在 docker-proxy 仍然运行的条件下,通过方案二(自定义网络 + 容器名通信)慢慢改造架构,让容器间的通信逐步脱离对 docker-proxy 的依赖。
这次排查的收获
-
分层排查永远是最快的路径------从容器内部到宿主机,从 ICMP 到 TCP,一层层缩小范围,避免了在错误层面浪费时间。
-
docker-proxy的 IPv6 socket 绑定问题是 CentOS 7 + Docker 的经典暗坑 ------表现为:::port而不是0.0.0.0:port。bindv6only=0只是理论上让 IPv6 socket 接 IPv4 连接,实际行为取决于内核版本。 -
Connection reset by peer和Connection refused是完全不同的症状------前者意味着连接建立后被重置(docker-proxy 转发阶段出问题),后者意味着根本没有进程在监听(端口映射没生效或应用没启动)。 -
"12583 能通而其他不能"不一定是配置差异------当应用有 fallback 机制时,不同的请求路径可能导致完全不同的可用性表现。
-
架构层面最好让容器通过容器名(DNS)相互访问,而不是通过映射到宿主机的端口再走一层 docker-proxy。自定义网络 + 容器名通信是 Docker 推荐的生产模式。
这篇博客基于一次真实的 Docker 生产环境排查经历整理而成。排查发生在 2026 年 7 月,涉及 CentOS 7.3.10 内核 + Docker(docker-proxy)的端口映射异常问题。如果你也遇到过类似的现象,希望这篇文章能帮你少走一些弯路。