Skip to content

Docker 旧服务端口配置失真:一次 --net=host 环境下的排查与统一维护

一、问题背景:旧服务的端口配置已经不能完全相信

现有部分 Spring Boot 服务已经运行多年。

历史上为了减少 Docker 网络配置问题,很多服务直接使用:

bash
--net=host

运行。

同时,docker run 中又保留了不少端口映射参数,例如:

bash
-p 8000:8000
-p 9998:9998
-p 20880:20880
-p 12345:12345

乍看之下,这些参数似乎已经说明了服务使用哪些端口。

但实际维护时发现并不是这样。

例如:

text
docker run 中配置:
-p 12345:12345

而 Java 进程实际上根本没有监听:

text
12345

这意味着历史 docker run 中已经混入了错误、遗留或者失效的端口配置。

另一方面,因为使用了:

bash
--net=host

这些 -p 本身也没有真正承担 Docker 端口映射职责。

于是出现了一个非常现实的维护问题:

只看 docker run,已经无法确定这个应用到底真正使用哪些端口。

最初看到的现象可以概括成一句话:

需要知道某个应用对外开放的具体端口。

但继续排查后发现,这并不只是“查一下端口”的问题,而是旧服务的部署配置和真实运行状态已经发生了偏离。

二、为什么要统一清理 --net=host

对于正常的新服务发布,更合理的方式应该是从一开始就明确:

text
服务监听哪些端口
哪些端口需要映射
哪些端口允许被外部访问

而不是默认使用:

bash
--net=host

再依赖宿主机网络直接承接所有监听端口。

但当前面对的是已经运行多年的旧服务。

这次工作的重点不是重新设计一套“新服务发布规范”,而是:

对历史服务进行事后排查和统一维护。

维护目标包括:

  • 去掉没有必要的 --net=host
  • 清理已经失真的历史 -p
  • 从运行态确认 Java 服务真实监听端口
  • 逐个确认这些端口的用途
  • 只保留真正需要从容器外访问的端口
  • docker run 重新能够准确描述这个服务的网络暴露面

最终希望达到一种更容易长期维护的状态:

配置里看到什么,运行时就是什么。

也就是尽可能做到“所见即所得”。

三、先对照历史 docker run 配置

原来的启动命令经过简化后类似:

bash
docker run \
  -e DUBBO_IP_TO_REGISTRY=121.40.178.113 \
  -e DUBBO_PORT_TO_REGISTRY=20880 \
  -e DUBBO_PORT_TO_BIND=20880 \
  --name app-xxxx \
  -p 8000:8000 \
  -p 9998:9998 \
  -p 20880:20880 \
  --net=host \
  -d \
  $image \
  --spring.profiles.active=test

从命令本身看,似乎服务需要:

text
8000
9998
20880

三个端口。

但历史上还出现过类似:

bash
-p 12345:12345

这样的配置。

实际 Java 进程却没有监听 12345

这说明:

历史 -p 已经不能直接作为真实端口清单使用。

而且在 --net=host 模式下,即使写了:

bash
-p 8000:8000

Docker 也不会通过 bridge/NAT 模型去执行:

text
宿主机 8000

容器 8000

这层映射。

真正决定宿主机上有哪些监听端口的,是容器里的 Java 进程实际打开了什么端口。

所以接下来不能继续从历史配置推断,而要反过来:

从运行态重新确认。

四、从运行态反查 Java 实际监听端口

服务使用:

text
openjdk:8u322-jdk-bullseye

作为基础镜像。

进入容器后安装 net-tools

bash
apt-get update
apt-get install -y net-tools

然后执行:

bash
netstat -tulnp

得到:

text
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:9998            0.0.0.0:*               LISTEN      1/java
tcp        0      0 0.0.0:22222           0.0.0.0:*               LISTEN      1/java
tcp        0      0 0.0.0:20880           0.0.0.0:*               LISTEN      1/java
tcp        0      0 0.0.0:8000            0.0.0.0:*               LISTEN      1/java
udp        0      0 0.0.0:40635           0.0.0.0:*                           1/java
udp        0      0 0.0.0:46705           0.0.0.0:*                           1/java
udp        0      0 0.0.0:55157           0.0.0.0:*                           1/java
udp        0      0 0.0.0:39416           0.0.0.0:*                           1/java

这里看到的结果和历史配置并不完全一致。

Java 实际监听的 TCP 端口是:

text
8000
9998
20880
22222

其中:

text
22222

并没有出现在已有的 -p 参数里。

这说明历史配置的失真并不是单向的。

既可能存在:

text
-p 中有
但 Java 实际没监听

也可能存在:

text
Java 实际监听
但 -p 中没有

五、发现第一类失真:-p 中有,服务却没有监听

最先发现的问题属于这一类:

text
docker run 中配置:
-p 12345:12345

但实际检查 Java 进程:

text
没有监听 12345

这类配置通常可能来自:

  • 历史版本遗留
  • 服务功能变化后没有同步删除
  • 启动脚本复制过程中带入
  • 曾经规划使用但后来没有真正启用

--net=host 模式下,这类错误很容易长期隐藏。

因为:

text
-p 是否准确

并不会决定服务是否真的能够监听宿主机端口。

只要 Java 自己监听成功,服务照样可以正常运行。

久而久之,docker run 中的端口列表就越来越像一份“历史记录”,而不是可信配置。

这也是这次统一维护必须解决的问题之一:

无效的端口配置应该删除,不能继续让它混在生产启动命令里。

六、发现第二类失真:服务在监听,-p 中却没有配置

运行态检查又发现:

text
22222

正在被 Java 进程监听。

但历史 docker run 并没有:

bash
-p 22222:22222

如果只看这一点,很容易得出另一个错误结论:

既然服务监听了 22222,那是不是应该把它补进 -p

不能这么快下结论。

因为:

text
服务监听了某个端口

并不代表:

text
这个端口就必须允许容器外访问

所以必须继续确认 22222 的来源和用途。

七、22222 是什么:继续确认 Dubbo QoS

为了再次确认监听情况,安装:

bash
apt-get install -y iproute2

然后执行:

bash
ss -tulnp

结果中仍然能够看到:

text
tcp LISTEN 0 128 0.0.0.0:22222 0.0.0.0:* users:(("java",pid=1,fd=63))

这说明:

text
22222

确实由当前 Java 进程监听。

退出容器后,从宿主机测试:

bash
telnet localhost 22222

返回:

text
Trying ::1...
Connected to localhost.
Escape character is '^]'.

   ___   __  __ ___   ___   ____
  / _ \ / / / // _ ) / _ ) / __ \
 / // // /_/ // _  |/ _  |/ /_/ /
/____/ \____//____//____/ \____/

dubbo>

看到:

text
dubbo>

后,端口来源就比较明确了:

22222 是 Dubbo QoS 端口。

Dubbo QoS 提供运行时管理能力,可以用于查看 Dubbo 服务状态以及执行相关管理命令。

这也解释了为什么:

  • 业务代码中没有直接写 22222
  • Docker 启动参数中没有明确配置它
  • Java 进程却会主动监听这个端口

八、监听了,不代表就应该映射出去

确认 22222 是 Dubbo QoS 后,需要做的是用途判断,而不是机械增加:

bash
-p 22222:22222

当前业务并不需要从容器外访问 Dubbo QoS。

因此:

text
22222

虽然属于 Java 进程真实监听端口,但没有必要成为 Docker 对外映射端口。

这里需要区分三个不同概念:

text
Java 实际监听的端口
text
容器外需要访问的端口
text
docker run 中应该配置的 -p

三者并不是完全等价的。

更合理的关系应该是:

text
Java 实际监听

确认端口用途

确认是否需要容器外访问

需要时才加入 -p

因此:

text
22222

应该保留在“服务监听端口认知”中,但不需要加入“外部访问端口清单”。

九、重新整理真正需要保留的端口列表

经过实际检查和用途确认后,可以重新整理这个服务的 TCP 端口:

端口用途是否需要 -p
8000Spring Boot 业务端口
9998当前应用使用的额外服务/管理端口
20880Dubbo 服务端口
22222Dubbo QoS
12345历史错误/遗留配置,实际未监听删除

另外,还观察到几个 UDP 监听端口:

text
40635
46705
55157
39416

仅凭这次监听结果还不能准确判断这些 UDP 端口的具体来源和用途。

因此没有因为它们出现在监听列表中,就直接增加 Docker 映射。

原则仍然是:

先确认用途,再决定是否暴露。

而不是:

监听列表里看到什么,就全部映射什么。

十、统一维护后的 Docker 端口配置

清理完成后,需要对外提供访问能力的端口最终保留为:

bash
-p 8000:8000
-p 9998:9998
-p 20880:20880

删除:

text
--net=host

同时删除:

text
没有实际监听
或者
没有对外访问需求

的历史端口配置。

这样处理以后,docker run 中的端口参数才重新具备明确含义:

text
-p 8000:8000

就意味着:

text
这个服务确实使用 8000
并且确实需要从容器外访问

而不是:

text
这里以前有人写过一个 8000
至于现在还用不用,要进去查才知道

这正是统一维护的目标。

十一、这次排查暴露出的维护问题

最初的问题只是:

需要知道某个应用对外开放的具体端口。

但真正排查下来,暴露的是旧服务配置长期缺乏统一维护的问题。

其中至少有三层状态:

text
docker run 历史配置
text
Java 运行时真实监听
text
业务真正需要开放的端口

如果三者长期不校验,就很容易出现:

text
配置有,但运行时没有

或者:

text
运行时有,但业务不需要开放

甚至:

text
服务已经改了很多年,启动脚本还保留着旧版本参数

这类问题单独看通常不会立即造成故障。

也正因为“不影响当前运行”,它们才特别容易长期留下。

直到真正进行:

text
去除 --net=host
统一 bridge 网络
收敛服务网络边界

这样的维护工作时,历史配置失真才会集中暴露出来。

十二、最终回看:让 docker run 重新成为可信配置

这次工作的起点非常具体:

需要知道某个应用对外开放的具体端口。

最直接的方法确实是进入运行中的容器,通过:

bash
netstat -tulnp

或者:

bash
ss -tulnp

查看 Java 进程真实监听状态。

但如果只停留在“查到了端口”,这次维护还没有真正完成。

最终真正需要建立的是:

text
运行态

确认真实监听

端口用途

确认是否需要外部访问

docker run

只声明真正需要的端口

统一维护之后,希望做到的是:

docker run 里看到的端口,就是这个服务真正需要对外提供的端口。

例如:

bash
-p 8000:8000
-p 9998:9998
-p 20880:20880

这些配置不再只是历史遗留字符串,而应该能够直接回答:

text
这个应用对外开放哪些端口?

以后维护人员不需要重新进入容器、翻代码、猜历史用途,才能知道服务的网络暴露情况。

也就是说,这次清理最终追求的不是“端口越少越好”,而是:

配置与运行状态一致,端口用途明确,统一维护,所见即所得。