Docker 端口"时通时不通"的排查实战

一篇 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 用户态代理,存在多个已知的网络问题:

  1. IPv4-mapped IPv6 socket 行为不一致 :虽然 bindv6only=0 理论上允许 IPv6 socket 接受 IPv4 连接,但 3.10 内核的兼容层实现可能存在竞态条件,导致在高并发或特定网络路径下,IPv4 连接握手后被重置(RST)或超时。

  2. docker-proxy 的 socket 绑定偏好 :Docker 在启用 IPv6 支持时,docker-proxy 优先创建 IPv6 双栈 socket,这在大多数现代系统上没问题,但在老内核上有兼容性风险。

  3. Connection reset by peer 的正确定位 :我反复遇到的 curl: (56) Recv failure: Connection reset by peer 是这个问题的典型症状------连接在三路握手阶段成功(TCP 连接建立),但在数据交换阶段被对方重置。这说明连接到达了 docker-proxy,但在 docker-proxy 转发给容器的过程中出错。这和"端口不通就拒绝连接"(无进程监听)是完全不同的故障模式。

  4. 用户态代理的额外开销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 的依赖。

这次排查的收获

  1. 分层排查永远是最快的路径------从容器内部到宿主机,从 ICMP 到 TCP,一层层缩小范围,避免了在错误层面浪费时间。

  2. docker-proxy 的 IPv6 socket 绑定问题是 CentOS 7 + Docker 的经典暗坑 ------表现为 :::port 而不是 0.0.0.0:portbindv6only=0 只是理论上让 IPv6 socket 接 IPv4 连接,实际行为取决于内核版本。

  3. Connection reset by peerConnection refused 是完全不同的症状------前者意味着连接建立后被重置(docker-proxy 转发阶段出问题),后者意味着根本没有进程在监听(端口映射没生效或应用没启动)。

  4. "12583 能通而其他不能"不一定是配置差异------当应用有 fallback 机制时,不同的请求路径可能导致完全不同的可用性表现。

  5. 架构层面最好让容器通过容器名(DNS)相互访问,而不是通过映射到宿主机的端口再走一层 docker-proxy。自定义网络 + 容器名通信是 Docker 推荐的生产模式。


这篇博客基于一次真实的 Docker 生产环境排查经历整理而成。排查发生在 2026 年 7 月,涉及 CentOS 7.3.10 内核 + Docker(docker-proxy)的端口映射异常问题。如果你也遇到过类似的现象,希望这篇文章能帮你少走一些弯路。

相关推荐
做个文艺程序员2 小时前
Linux第22篇:用Docker容器化你的Java SaaS应用:一次构建,随处运行
java·docker·容器
梦梦代码精4 小时前
基于ThinkPHP6 + Vue3的家政预约系统全解析:从LBS定位到自动派单的完整实现
java·docker·开源·php·代码规范
小的博客4 小时前
windows下安装Docker Desptop
运维·docker·容器
李迟4 小时前
我的docker随笔48:Docker版本号演进史
docker
阿拉雷️5 小时前
部署实战】Docker + AI Agent:让AI一键部署Spring Boot到服务器,从打包到上线只要一条指令
人工智能·spring boot·docker
雨声不在5 小时前
macos 12使用docker
macos·docker·容器
BullSmall21 小时前
Anolis OS 8.10 完整安装 Docker CE(生产可用,解决 podman 冲突)
docker·容器·podman
梦梦代码精1 天前
开源AI应用平台BuildingAI解析:插件化架构、应用市场与热门案例
人工智能·机器学习·docker·开源
IT瑞先生1 天前
Docker快速部署Mysql的三种方法——实操篇
mysql·adb·docker