主题
界面
主题
《Docker 基础与镜像容器》讲单个容器;本节讲**"多个容器怎么配合"**。
★ Compose 最经典的坑只有一个 ✓✓:
`depends_on` 只保证「启动顺序」,**不保证「服务能不能用」** ✗✗★ 本节的实测会把它跑出来 ✓✓:
场景:应用启动只用 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 时连上)localhost 的坑 ★★★;先看这个"最耗排查时间"的问题 ✗✗:
现象:CI 里偶发失败,报"连不上数据库";本地怎么试都好 ⚠️
原因:CI 机器慢,数据库初始化要 3 秒,而应用 0.1 秒就起来了 ✗
特点:**"在我这能跑"** —— 这类问题的排查成本极高 ✗✗★ 本质是"两个容器的速度不一样" ✓✓:
| 容器 | 启动耗时 |
|---|---|
| 应用(Spring Boot) | 几百毫秒 ~ 几十秒 |
| 数据库(MySQL) | 启动容器很快,但"能接受连接"要等初始化脚本跑完 ✗ |
★★ 关键认识 ✓✓:"容器启动了" ≠ "服务能用了" —— 而这正是 depends_on 的局限 ✗
实测对比 ✓✓:
| 策略 | 结果 | 说明 |
|---|---|---|
① 不等待(只 depends_on) ✗✗ | 失败 | 必挂 ✓ |
② 固定 sleep(2000) ⚠️ | 成功(2001 ms 时连上) | 数据库慢一点就失败 ✗ |
③ 固定 sleep(2000),数据库要 3000 ms ✗✗ | 失败 | 充分说明固定等待不可靠 ✓ |
| ④ 连接重试(退避) ✓✓ | 成功(3143 ms 时连上,6 次尝试) | 按需等待,不浪费 ✓✓ |
| ⑤ 健康检查 + 依赖就绪条件 ✓✓ | 成功(3042 ms 时连上) | 编排层的正确答案 ✓✓ |
★★★ 两条必须同时做(不是二选一) ✓✓:
| # | 层次 | 做什么 |
|---|---|---|
| ① | 编排层 | 健康检查(Compose 的 healthcheck、K8s 的 readinessProbe)✓✓ |
| ② | 应用层 | 连接重试(启动时连不上就退避重试)✓✓ |
★ 为什么两条都要 ✓✓:
编排层把"启动顺序"变成"就绪顺序" ✓
但编排工具也可能配错、依赖方也会重启 —— 应用层的重试是最后一道防线 ✓✓
★ 一条经验值 ✓✓:
重试要设上限并记录日志 ✗✓
无限重试会让容器永远停在"启动中",看起来像卡死 ✗
有上限则快速失败 + 让编排层重启它(K8s 的
CrashLoopBackOff就是这个机制)✓✓
★ 同一个 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}) ✓✓
★ 三种方式 ✓✓:
| 方式 | 容器删除后 | 用途 |
|---|---|---|
| 不挂卷(写在可写层)✗ | 数据丢失 ✗✗ | 临时数据 / 缓存 ⚠️ |
| 命名卷(named volume) ✓✓ | 保留 ✓✓ | 数据库数据 ✓✓ |
| 绑定挂载(bind mount) ✓✓ | 保留 ✓✓ | 开发时挂源码 ✓✓ |
★★ 一条判断标准 ✓✓:
"这个容器的数据重要吗" ✓
· 重要(数据库、上传文件)→ 必须挂卷 ✓✓
· 不重要(纯计算、无状态服务)→ 不挂卷,随删随建 ✓✓
★ 与 《微服务架构与拆分原则》 的呼应 ✓✓:
"服务要无状态" 的准确含义是:状态要么在数据库,要么在卷里 ✓✓
容器本身不承载状态 ✓
★ 一份可直接抄的 compose 配置 ✓✓:
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 ★★ | 数据库初始化慢时被判不健康 ✗ |
★ 三个常用命令 ✓:
docker compose up -d # 后台启动全部
docker compose ps # ★ 看状态(含健康状态)
docker compose logs -f app # ★ 看某个服务的日志★★ 一句话 ✓✓:
它是"单机多容器"的编排工具 ——适合本地开发与测试环境 ✓✓
生产环境要的是"跨主机调度、自愈、滚动更新" → 那是 K8s(《Kubernetes 入门》) ✓✓
★ 但它的"启动竞态"问题是通用的 ✓✓ —— 同样的坑在 K8s 里一样存在(只是用 readinessProbe 解决)✓
services:
redis:
image: redis:7
app:
build: .
depends_on: [redis]
environment:
REDIS_HOST: redis # ★ 用服务名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); // ★ 退避
}
}
}# ✗ 常见但不可靠
command: sh -c "sleep 10 && java -jar app.jar"★ 问题 ✓:它把"启动时间"写死了 —— 数据库偶尔需要 15 秒时照样失败,而正常时白等 10 秒 ✗✗
代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/container/ComposeOrchestrationLesson.java
运行:
cd labs
.\mvnw.cmd -pl distributed-lab compile
java "-Dstdout.encoding=UTF-8" -cp distributed-lab/target/classes cn.kb.learn.dist.container.ComposeOrchestrationLesson实测输出(关键部分) ✓:
===== 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 不可靠 | 策略 ③失败 ✗✗ |
| ③ | 健康检查 + 应用重试,两条都要 | 策略 ④⑤都成功 ✓✓ |
docker compose ps:看服务的健康状态(不是只看"是否运行")✓✓;docker compose logs mysql:看数据库初始化要多久 ✓;condition: service_healthy 去掉:观察偶发失败 ✗✗;localhost:观察连不上 ✗✓。| # | 错误 | 说明 |
|---|---|---|
| 1 | 以为 depends_on 能保证"能用" ✗✗ | 只保证启动顺序 ✓✓ |
| 2 | 用固定 sleep 等依赖 ✗✗ | 不可靠(策略 ③)✓ |
| 3 | 只配健康检查,应用不重试 ✗ | 编排配错时无兜底 ✓ |
| 4 | 配置里写 localhost ✗✗ | 容器里指向自己 ✓✓ |
| 5 | 数据库不挂卷 ✗✗ | 数据丢失 ✓ |
| 6 | 密码写在 compose 文件里 ✗✗ | 进 Git ✓ |
| 7 | 用 Compose 做生产编排 ✗ | 它没有跨主机与自愈能力 ✓ |
目标:亲手复现"启动竞态",并用两种方式修好它。
步骤:
ComposeOrchestrationLesson,确认策略 ① 与 ③ 失败 ✓✓;condition: service_healthy ★★★;init 脚本),验证两种方式都能兜住 ✓✓;验收方式:能说出"你的 compose 里哪些配置缺了会导致偶发失败"。
扩展任务:设计一个"测试环境一键启动"的 compose:
depends_on 能保证被依赖的服务已经就绪。(对 / 错)localhost 在容器里会出错?services:
db:
image: mysql:8.0
app:
depends_on: [db]
command: sh -c "sleep 5 && java -jar app.jar"这段配置有什么问题? 5. (设计)给"本地开发环境"设计 compose:要求一条命令启动全部依赖。
localhost 指的是容器自己 ✗ —— 数据库在另一个容器里,要用服务名(或注入的环境变量)✓✓★depends_on 不等就绪 → 竞态 ✗✗; ② sleep 5 硬编码等待时间 → 数据库慢就失败、正常时白等 ✗ —— 正确做法:healthcheck + condition: service_healthy,并给应用配重试 ✓✓docker compose up -d 一条命令 ✓✓; 依赖配健康检查 ✓;数据卷保存数据库数据 ✓; 初始化脚本幂等(可重复执行)✓;端口映射与 .env 配置 ✓localhost 的坑 ★★★;《Kubernetes 入门》:从"手动编排"到"声明式自愈"。