记一次诡异的 Docker 容器"串包"故障排查

12 阅读4分钟

记一次诡异的 Docker 容器"串包"故障排查

容器名叫 tj-trade,docker ps 也正常 Up,微服务调用却一直报 UnknownHostException: trade-service。进去一看日志,好家伙——它运行的居然是 user-service 的代码。


1. 故障现象

Spring Cloud 微服务架构,Nacos 做注册中心,Jenkins 手动逐个构建部署。

某天发现 tj-coursetrade-service 时报错:

feign.RetryableException: trade-service executing GET http://trade-service/...
Caused by: java.net.UnknownHostException: trade-service
No servers available for service: trade-service

docker ps 显示 tj-trade 容器正在运行:

CONTAINER ID   IMAGE             STATUS          PORTS
93ce8914e791   tj-trade:latest   Up 32 minutes   0.0.0.0:8088->8088/tcp

端口对,状态对,看起来一切正常。那问题出在哪?


2. 排查过程

第一直觉:看 Nacos 注册

curl http://192.168.150.101:8848/nacos/v1/ns/instance/list?serviceName=trade-service

返回:

{"name":"DEFAULT_GROUP@@trade-service", "hosts": []}

Nacos 里没有 trade-service 的实例。 容器在跑,但没有成功注册。

第二直觉:看容器日志

docker logs tj-trade --tail 50

关键日志出现:

com.tianji.user.UserApplication          : The following 1 profile is active: "dev"
Tomcat initialized with port(s): 8082
nacos registry, DEFAULT_GROUP user-service 192.168.150.101:8082 register finished
Application 'user-service' is running!

真相大白。 tj-trade 这个容器里运行的是 user-service 的 jar 包。它向 Nacos 注册的是 user-service,所以 trade-service 在注册中心里根本不存在。


3. 根因分析

项目结构

tjxt-parent/
├── tj-user/          # user-service
│   └── target/tj-user.jar
├── tj-trade/         # trade-service
│   └── target/tj-trade.jar
├── tj-course/        # course-service
│   └── ...
├── docker-compose.yml
└── Dockerfile        # 通用 Dockerfile,放在项目根目录

通用 Dockerfile(有隐患)

FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/*.jar app.jar        # ⚠️ 通配符,复制 target 下第一个匹配的 jar
ENTRYPOINT ["sh", "-c", "java -jar app.jar"]

Jenkins 构建步骤(简化)

// 每个服务 job 大致相同,只是模块名不同
stage('Build') {
    sh 'mvn clean package -pl tj-trade -am -DskipTests'
    sh 'docker build -t tj-trade:latest .'      // ← build context 是根目录
    sh 'docker-compose up -d tj-trade'
}

串包场景还原

因为 Jenkins 上要手动挨个点部署 11 个微服务,在快速操作时,时序变成了这样:

时间轴 →
                    mvn clean    mvn package   docker build
tj-user:   ██████████████████████████████████████████████
                          target/ 里有 tj-user.jar
                                                      
                    mvn clean    docker build(跑早了!)
tj-trade:  ██████████████████████████████████████████████
                          ↑ 此时 target/ 还没生成 tj-trade.jar
                            但 Dockerfile 的 COPY target/*.jar
                            读到了上一个构建残留的 tj-user.jar

核心问题是三个因素叠加

因素说明
Docker build context 是项目根目录COPY target/*.jar 看的是根 target,不是模块 target
Maven 多模块聚合构建-pl tj-trade -am 也会编译依赖模块,根 target 可能混入其他 jar
快速点击 + workspace 没隔离上一个 job 的产物还没被 clean 掉,下一个 job 就启动了

4. 解决方案

方案一:使用模块专属 Dockerfile(推荐)

每个子模块目录下放独立的 Dockerfile:

tj-trade/
├── src/
├── target/
│   └── tj-trade.jar
├── Dockerfile          # 模块专属
└── pom.xml
# tj-trade/Dockerfile
FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/tj-trade.jar app.jar     # 精确指定文件名
ENTRYPOINT ["sh", "-c", "java -jar app.jar"]

Jenkins 构建脚本调整 build context:

sh 'docker build -t tj-trade:latest ./tj-trade'

方案二:Jenkins workspace 全量 clean

stage('Clean') {
    sh 'git clean -fdx'              // 清掉所有非 git 追踪的文件
    sh 'mvn clean'
}
stage('Build') {
    sh 'mvn package -pl tj-trade -am -DskipTests'
    sh 'docker build -t tj-trade:latest ./tj-trade'
}

方案三:在 Dockerfile 里加校验(最稳妥)

# tj-trade/Dockerfile
FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/tj-trade.jar app.jar

# 启动前校验:确认 jar 包里是正确的启动类
RUN jar tf app.jar | grep -q "com/tianji/trade/TradeApplication" \
    || (echo "ERROR: Wrong jar file!" && exit 1)

ENTRYPOINT ["sh", "-c", "java -jar app.jar"]

5. 如何快速定位类似问题

如果遇到容器在跑但服务调用失败,按这个顺序排查:

1. docker ps                    → 容器状态正常?
2. docker logs <容器> --tail 100 → 看启动类和注册名是否对
3. Nacos 控制台                  → 服务是否注册,实例数对不对
4. docker exec <容器> ls /app   → 容器里的 jar 文件名

关键线索:日志里的启动类名和注册服务名。只要这两个跟你预期的不一样,99% 是镜像打错了包。


6. 总结

这个问题的本质是 构建产物与容器镜像之间缺乏精确的一一对应关系

  • COPY target/*.jar 这种通配符写法在小项目没问题,多模块项目就成了定时炸弹
  • Jenkins 手动逐个构建时,"点得快" 只是加速了这颗炸弹爆炸
  • 让每个模块有自己的 Dockerfile,COPY 精确到具体文件名,是最简单有效的解法

对于学习项目来说,这些坑反而是最宝贵的经验——生产环境的 CI/CD 之所以要做 workspace 隔离、artifact 指纹校验,就是为了防这种事情。


踩坑于:Spring Cloud + Docker + Jenkins 学习环境 容器:tj-trade 镜像内跑着 tj-user 的代码