Appearance
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:8000Docker 也不会通过 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 |
|---|---|---|
8000 | Spring Boot 业务端口 | 是 |
9998 | 当前应用使用的额外服务/管理端口 | 是 |
20880 | Dubbo 服务端口 | 是 |
22222 | Dubbo 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
这个应用对外开放哪些端口?以后维护人员不需要重新进入容器、翻代码、猜历史用途,才能知道服务的网络暴露情况。
也就是说,这次清理最终追求的不是“端口越少越好”,而是:
配置与运行状态一致,端口用途明确,统一维护,所见即所得。