Skip to content

Docker Compose 与多容器编排 ​

S4-026建议学高级约 60 分钟JDK 21企业开发进阶 · 容器与编排

1. 本节定位 ​

《Docker 基础与镜像容器》讲单个容器;本节讲**"多个容器怎么配合"**。

★ Compose 最经典的坑只有一个 ✓✓:

text
`depends_on` 只保证「启动顺序」,**不保证「服务能不能用」** ✗✗

★ 本节的实测会把它跑出来 ✓✓:

text
  场景:应用启动只用 100 ms,而数据库初始化要 1500 ms
  应用启动完毕立刻连数据库 → **Connection refused** ✗✗(数据库还差约 1339 ms 才就绪)

  四种等待策略:
  ① 不等待(只有 depends_on)        → **失败** ✗✗
  ② 固定 sleep(2000 ms)             → 成功 ✓(但数据库要 3000 ms 时就失败 ✗)
  ③ 固定 sleep(2000 ms),数据库要 3000 ms → **失败** ✗✗
  ④ 连接重试(退避)                 → 成功 ✓✓(3143 ms 时连上,6 次尝试)
  ⑤ 健康检查 + 依赖就绪              → 成功 ✓✓(3042 ms 时连上)

2. 学习目标 ​

  • 能说清"启动竞态"为什么表现为"本地能跑、CI 偶发失败" ★★★;
  • 能对比四种等待策略的可靠性 ★★★;
  • 知道正确做法是"编排层健康检查 + 应用层重试"两条都要 ★★★;
  • 知道"服务名即主机名",以及 localhost 的坑 ★★★;
  • 能说出数据卷的三种方式与选择标准 ★★★;
  • 能写出含五个关键点的 compose 配置 ★★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看这个"最耗排查时间"的问题 ✗✗:

text
现象:CI 里偶发失败,报"连不上数据库";本地怎么试都好 ⚠️
原因:CI 机器慢,数据库初始化要 3 秒,而应用 0.1 秒就起来了 ✗
特点:**"在我这能跑"** —— 这类问题的排查成本极高 ✗✗

★ 本质是"两个容器的速度不一样" ✓✓:

容器启动耗时
应用(Spring Boot)几百毫秒 ~ 几十秒
数据库(MySQL)启动容器很快,但"能接受连接"要等初始化脚本跑完 ✗

★★ 关键认识 ✓✓:"容器启动了" ≠ "服务能用了" —— 而这正是 depends_on 的局限 ✗

5. 核心概念 ​

5.1 启动竞态与四种等待策略 ★★★ ​

实测对比 ✓✓:

策略结果说明
① 不等待(只 depends_on) ✗✗失败必挂 ✓
② 固定 sleep(2000) ⚠️成功(2001 ms 时连上)数据库慢一点就失败 ✗
③ 固定 sleep(2000),数据库要 3000 ms ✗✗失败充分说明固定等待不可靠 ✓
④ 连接重试(退避) ✓✓成功(3143 ms 时连上,6 次尝试)按需等待,不浪费 ✓✓
⑤ 健康检查 + 依赖就绪条件 ✓✓成功(3042 ms 时连上)编排层的正确答案 ✓✓

★★★ 两条必须同时做(不是二选一) ✓✓:

#层次做什么
①编排层健康检查(Compose 的 healthcheck、K8s 的 readinessProbe)✓✓
②应用层连接重试(启动时连不上就退避重试)✓✓

★ 为什么两条都要 ✓✓:

编排层把"启动顺序"变成"就绪顺序" ✓

但编排工具也可能配错、依赖方也会重启 —— 应用层的重试是最后一道防线 ✓✓

★ 一条经验值 ✓✓:

重试要设上限并记录日志 ✗✓

无限重试会让容器永远停在"启动中",看起来像卡死 ✗

有上限则快速失败 + 让编排层重启它(K8s 的 CrashLoopBackOff 就是这个机制)✓✓

5.2 服务发现:服务名即主机名 ★★★ ​

★ 同一个 Compose 网络里,服务名就是主机名 ✓✓:

配置写法等效含义
jdbc:mysql://mysql:3306/order连到服务名为 mysql 的容器 ✓✓
redis://redis:6379同上 ✓
http://order-service:8080服务间调用 ✓

★ 背后是 Docker 的内嵌 DNS ✓ ——每个容器能解析出同网络其他容器的 IP ✓✓

★ 与 K8s 的差别 ✓:Compose 没有 VIP 与内置负载均衡(粒度更粗)⚠️

★★ 一个极常见的坑 ✗✗:

配置文件里写 localhost:3306 ——

· 在非容器环境(应用与数据库同机)能跑 ✓

· 在容器里,localhost 指的是"容器自己" → 连不上 ✗✗

正确做法:用服务名,或注入环境变量(${DB_HOST}) ✓✓

5.3 数据卷:容器的数据会丢吗 ★★★ ​

★ 三种方式 ✓✓:

方式容器删除后用途
不挂卷(写在可写层)✗数据丢失 ✗✗临时数据 / 缓存 ⚠️
命名卷(named volume) ✓✓保留 ✓✓数据库数据 ✓✓
绑定挂载(bind mount) ✓✓保留 ✓✓开发时挂源码 ✓✓

★★ 一条判断标准 ✓✓:

"这个容器的数据重要吗" ✓

· 重要(数据库、上传文件)→ 必须挂卷 ✓✓

· 不重要(纯计算、无状态服务)→ 不挂卷,随删随建 ✓✓

★ 与 《微服务架构与拆分原则》 的呼应 ✓✓:

"服务要无状态" 的准确含义是:状态要么在数据库,要么在卷里 ✓✓

容器本身不承载状态 ✓

5.4 五个必须配的项 ★★★ ​

★ 一份可直接抄的 compose 配置 ✓✓:

yaml
services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}      # ★ 从 .env 注入,不写死
    volumes:
      - mysql-data:/var/lib/mysql              # ★ 数据卷(不丢数据)
    healthcheck:                               # ★★ 关键
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 30s                        # ★ 给初始化留时间

  order-service:
    build: .
    depends_on:
      mysql:
        condition: service_healthy             # ★★ 等它「健康」而不是「启动」
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/order   # ★ 用服务名

volumes:
  mysql-data:

★ 五个必须配的点 ✓✓:

#配置不配的后果
①healthcheck + condition ★★★启动竞态(本类实测)✗✗
②数据卷 ★★★重启后数据没了 ✗✗
③密码走 .env ★★★密码进 Git ✗✗
④用服务名而非 localhost ★★★容器间连不上 ✗✗
⑤start_period ★★数据库初始化慢时被判不健康 ✗

★ 三个常用命令 ✓:

bash
docker compose up -d            # 后台启动全部
docker compose ps               # ★ 看状态(含健康状态)
docker compose logs -f app      # ★ 看某个服务的日志

5.5 Compose 的定位 ★★ ​

★★ 一句话 ✓✓:

它是"单机多容器"的编排工具 ——适合本地开发与测试环境 ✓✓

生产环境要的是"跨主机调度、自愈、滚动更新" → 那是 K8s(《Kubernetes 入门》) ✓✓

★ 但它的"启动竞态"问题是通用的 ✓✓ —— 同样的坑在 K8s 里一样存在(只是用 readinessProbe 解决)✓

6. 递进示例 ​

6.1 最小示例:两个容器 ​

yaml
services:
  redis:
    image: redis:7
  app:
    build: .
    depends_on: [redis]
    environment:
      REDIS_HOST: redis        # ★ 用服务名

6.2 业务示例:应用层的连接重试 ​

java
private DataSource buildDataSourceWithRetry() {
    int attempt = 0;
    long interval = 500;
    while (true) {
        try {
            return createDataSource();                 // 尝试连接
        } catch (Exception e) {
            if (++attempt > 10) { throw e; }            // ★ 有上限,别无限等
            log.warn("数据库未就绪,第 {} 次重试", attempt);
            sleep(interval);
            interval = Math.min(interval * 2, 5000);    // ★ 退避
        }
    }
}

6.3 反例:用 sleep 在编排里"等一等" ​

yaml
# ✗ 常见但不可靠
command: sh -c "sleep 10 && java -jar app.jar"

★ 问题 ✓:它把"启动时间"写死了 —— 数据库偶尔需要 15 秒时照样失败,而正常时白等 10 秒 ✗✗

7. 完整可运行代码与实测 ​

代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/container/ComposeOrchestrationLesson.java

运行:

powershell
cd labs
.\mvnw.cmd -pl distributed-lab compile
java "-Dstdout.encoding=UTF-8" -cp distributed-lab/target/classes cn.kb.learn.dist.container.ComposeOrchestrationLesson

实测输出(关键部分) ✓:

text
===== 1. 启动竞态 =====
  app 在 150 ms 时启动完毕,立刻连接 mysql(1500 ms 时就绪?否)
  连接结果:**Connection refused** ✗✗(mysql 还差约 1339 ms 才就绪)

===== 2. 四种等待策略 =====
  ① 不等待(只有 depends_on)                → **失败:Connection refused** ✗✗
  ② 固定 sleep(2000 ms)                     → 成功 ✓(2001 ms 时连上,1 次尝试)
  ③ 固定 sleep(2000 ms),但数据库需要 3000 ms → **失败:Connection refused** ✗✗
  ④ 连接重试(最多 10 次,间隔 200→800 ms)    → 成功 ✓(3143 ms 时连上,6 次尝试)
  ⑤ 健康检查 + 依赖就绪条件(最可靠)           → 成功 ✓(3042 ms 时连上,28 次检查)

★ 三条可落地的结论 ✓:

#结论依据
①depends_on 只保证启动顺序,不保证就绪策略 ①失败 ✗✗
②固定 sleep 不可靠策略 ③失败 ✗✗
③健康检查 + 应用重试,两条都要策略 ④⑤都成功 ✓✓

8. Debug 观察点 ​

  1. docker compose ps:看服务的健康状态(不是只看"是否运行")✓✓;
  2. docker compose logs mysql:看数据库初始化要多久 ✓;
  3. 把 condition: service_healthy 去掉:观察偶发失败 ✗✗;
  4. 把数据卷去掉,重启 compose:观察数据丢失 ✗✗;
  5. 把配置里的服务名改成 localhost:观察连不上 ✗✓。

9. 常见错误 ​

#错误说明
1以为 depends_on 能保证"能用" ✗✗只保证启动顺序 ✓✓
2用固定 sleep 等依赖 ✗✗不可靠(策略 ③)✓
3只配健康检查,应用不重试 ✗编排配错时无兜底 ✓
4配置里写 localhost ✗✗容器里指向自己 ✓✓
5数据库不挂卷 ✗✗数据丢失 ✓
6密码写在 compose 文件里 ✗✗进 Git ✓
7用 Compose 做生产编排 ✗它没有跨主机与自愈能力 ✓

10. 动手实验 ​

目标:亲手复现"启动竞态",并用两种方式修好它。

步骤:

  1. 跑 ComposeOrchestrationLesson,确认策略 ① 与 ③ 失败 ✓✓;
  2. 给你的项目加健康检查 + condition: service_healthy ★★★;
  3. 同时给应用加连接重试(有上限 + 退避)★★★;
  4. 人为让数据库启动变慢(加一个 init 脚本),验证两种方式都能兜住 ✓✓;
  5. 把数据库容器删了重建,验证数据卷还在 ✓。

验收方式:能说出"你的 compose 里哪些配置缺了会导致偶发失败"。

扩展任务:设计一个"测试环境一键启动"的 compose:

  • 被测应用 + MySQL + Redis + 消息队列 ✓;
  • 所有依赖都有健康检查 ✓✓;
  • 数据卷与测试数据初始化脚本 ✓;
  • 说明:为什么"测试环境的初始化数据要幂等"(提示:重复执行不能出错)★★

11. 练习题 ​

  1. 判断:depends_on 能保证被依赖的服务已经就绪。(对 / 错)
  2. 填空:解决启动竞态要做两件事:编排层 ______、应用层 ______。
  3. 简答:为什么配置里写 localhost 在容器里会出错?
  4. (配置阅读)
yaml
services:
  db:
    image: mysql:8.0
  app:
    depends_on: [db]
    command: sh -c "sleep 5 && java -jar app.jar"

这段配置有什么问题? 5. (设计)给"本地开发环境"设计 compose:要求一条命令启动全部依赖。

参考答案(要点)
  1. 错。只保证容器启动了,不保证服务能用了 ✗✗★
  2. 配健康检查(healthcheck + condition);配连接重试(有上限 + 退避) ✓✓
  3. 容器里的 localhost 指的是容器自己 ✗ —— 数据库在另一个容器里,要用服务名(或注入的环境变量)✓✓★
  4. 两个问题:① 只 depends_on 不等就绪 → 竞态 ✗✗; ② sleep 5 硬编码等待时间 → 数据库慢就失败、正常时白等 ✗ —— 正确做法:healthcheck + condition: service_healthy,并给应用配重试 ✓✓
  5. 要点:只用 docker compose up -d 一条命令 ✓✓; 依赖配健康检查 ✓;数据卷保存数据库数据 ✓; 初始化脚本幂等(可重复执行)✓;端口映射与 .env 配置 ✓

12. 掌握标准 ​

  • [ ] 能说清"启动竞态"及它为什么表现为"偶发失败" ★★★;
  • [ ] 能对比四种等待策略的可靠性 ★★★;
  • [ ] 知道健康检查与应用重试两条都要 ★★★;
  • [ ] 知道"服务名即主机名"与 localhost 的坑 ★★★;
  • [ ] 能说出数据卷的三种方式与选择标准 ★★★;
  • [ ] 能写出含五个关键点的 compose 配置 ★★★;
  • [ ] 知道 Compose 的定位是单机开发/测试 ★★。

13. 扩展资源 ​

14. 下一步学习 ​

《Kubernetes 入门》:从"手动编排"到"声明式自愈"。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引