IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。
在第 14 篇中,我们通过 Bind Mount 和覆盖文件实现了开发环境的热重载,极大提升了开发效率。但还有一个深水区我们没有涉足:多服务应用的启动顺序。
如果你的 Flask 应用启动得比 Redis 快,会发生什么?Flask 连不上 Redis,直接崩溃退出。即使你配置了 restart: unless-stopped,容器反复重启的这几分钟,服务是不可用的。开发环境或许能容忍,但在生产环境中,这就是一次启动失败的事故。
今天我们就来彻底解决这个问题。学会了这套机制,你不仅能写出更健壮的 Compose 配置,还会发现 Kubernetes 的 Pod 探针(liveness/readiness/startup probe)以及 Init Container 的设计思想,都源于同样的需求------在依赖就绪之前,不要启动关键服务。
一、问题的根源:启动顺序不当引发的故障
回顾一下我们之前的 Compose 配置(以第 12 篇为基准),flask-app 使用了 depends_on: redis,但没有加条件。这意味着 Docker 只保证 redis 容器先被创建(Created 或 Running 状态),而不等待 Redis 服务真正接受连接。
来看看实际会发生什么。假设我们有一个不带重试机制的 Flask 应用:
bash
# 脆弱的 app.py(无重试)
import redis
from flask import Flask
app = Flask(__name__)
cache = redis.Redis(host='redis', port=6379)
@app.route('/')
def hello():
count = cache.incr('hits')
return f'Hello! {count} times.\n'
Redis 容器虽然启动了,但 Redis 进程还需要几秒钟加载数据、监听端口。Flask 容器可能在 Redis 就绪之前就开始执行 cache.incr,导致抛出 redis.exceptions.ConnectionError,容器退出。
bash
docker compose up -d
docker compose ps
你可能看到:
bash
NAME COMMAND SERVICE STATUS
flask-app "python app.py" flask-app exited (1)
redis "docker-entrypoint.s..." redis running
这就是典型的"启动竞态条件"(race condition)。解决它需要两个武器:健康检查 (确认服务"真正就绪")和条件化启动顺序(等就绪后再启动依赖方)。
二、depends_on 的条件化:三种启动条件
在第 11 篇中我们简单介绍过 depends_on,现在来深挖。Compose 支持三种条件:
bash
services:
app:
depends_on:
db:
condition: service_started # 仅容器启动(默认)
redis:
condition: service_healthy # 健康检查通过
migration:
condition: service_completed_successfully # 一次性任务成功完成
2.1 service_started(默认)
这是最简单的条件:只要依赖的容器处于 running 状态(无论内部服务是否就绪),就认为条件满足。它无法避免竞态条件,只适合那些对启动时序不敏感的服务。
2.2 service_healthy
这个条件要求依赖服务的健康检查必须返回"healthy"。也就是说,Docker 不仅等待容器运行,还要等待健康检查命令在指定时间内返回成功。这是解决启动竞态的最佳方式。
2.3 service_completed_successfully
适用于一次性任务 (如数据库初始化、数据迁移脚本)。这个容器运行完成并退出,且退出码为 0,才算条件满足。典型的例子是 Django 的 manage.py migrate,在应用启动前必须执行完成。
bash
services:
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
...
migrate:
image: django-app:latest
command: python manage.py migrate
depends_on:
db:
condition: service_healthy
# 这个容器执行完就会退出
web:
image: django-app:latest
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
migrate:
condition: service_completed_successfully
web 服务会等待 migrate 服务成功退出后,才会启动。
三、健康检查配置实战
要让 service_healthy 生效,必须先给依赖服务配置健康检查。我们以 Redis 和 Flask 为例。
3.1 Redis 健康检查
Redis 官方镜像自带 redis-cli ping 命令,返回 PONG 即表示服务就绪。
bash
services:
redis:
image: redis:alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
-
test:执行的命令,redis-cli ping。 -
interval: 10s:每 10 秒检查一次。 -
timeout: 3s:单次检查超过 3 秒算失败。 -
retries: 3:连续失败 3 次标记为unhealthy。 -
start_period: 5s:容器启动后等 5 秒再开始检查(给 Redis 启动的时间)。
3.2 Flask 应用健康检查
Flask 应用我们预留了 /health 端点,可以用 curl 检查。
bash
services:
flask-app:
image: flask-redis-counter:2.0
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
start_period: 10s 给 Flask 启动留足时间,避免启动期间的误报。
3.3 查看健康状态
输出:
bash
NAME IMAGE COMMAND SERVICE STATUS
flask-app flask-redis-counter:2.0 "python app.py" flask-app running (healthy)
redis redis:alpine "docker-entrypoint.s..." redis running (healthy)
如果某服务不健康,状态会显示 unhealthy。你可以用 docker inspect 查看详细的健康检查日志:
bash
docker inspect redis --format='{{json .State.Health}}' | python3 -m json.tool
四、完整实战:为 Flask + Redis 配置启动顺序
现在我们把前面学到的知识整合进贯穿案例的 Compose 文件中。
4.1 改进后的 docker-compose.yml
bash
services:
redis:
image: redis:alpine
restart: unless-stopped
command: redis-server --appendonly yes --maxmemory 256mb
volumes:
- redis-data:/data
networks:
- app-net
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
flask-app:
image: flask-redis-counter:2.0
restart: unless-stopped
ports:
- "5000:5000"
environment:
- FLASK_ENV=production
- REDIS_HOST=redis
- LOG_LEVEL=info
volumes:
- flask-logs:/app/logs
networks:
- app-net
depends_on:
redis:
condition: service_healthy # 等待 Redis 健康检查通过
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
volumes:
redis-data:
flask-logs:
networks:
app-net:
driver: bridge
4.2 观察启动顺序
此时你会发现,flask-app 不会立刻启动。Compose 会先启动 redis,然后反复执行 redis-cli ping 直到返回 PONG,才启动 flask-app。
用 docker compose logs 可以看到时间线:
bash
redis | Ready to accept connections tcp
flask-app | * Running on http://0.0.0.0:5000
不会再出现 Flask 因为连不上 Redis 而崩溃的情况。
4.3 测试健康检查失败后的行为
模拟一下健康检查失败的情况。比如 Redis 挂了:
bash
docker compose exec redis redis-cli shutdown
稍等片刻(取决于 interval 和 retries),docker compose ps 会显示 Redis 为 unhealthy,但 Flask 仍然运行。如果你配置了 restart: unless-stopped,Docker 会自动重启 Redis;如果 Redis 持续不健康,flask-app 并不会因此终止,它只是在启动时依赖健康检查,运行时健康检查仅用于状态报告。
如果想在运行时自动重启不健康的容器,可以结合使用 restart 策略或外部监控工具,但这超出了 Compose 本身的能力范围(Kubernetes 的 liveness probe 则是专门解决这个问题的)。
五、高级话题:等待任意条件
在极少数情况下,你可能需要等待某个服务完成更复杂的初始化,比如"数据库已创建某张表"或"某个 API 可访问"。虽然 Compose 的健康检查可以自定义命令来满足这类需求,但它本质上是一个简单的命令探测,过于复杂的判断逻辑最好在应用代码内部处理(比如我们在 app.py 中用重试机制连接 Redis)。
对于数据库迁移这种一次性任务,service_completed_successfully 是标准解法。如果还需要更细粒度的条件(例如等待另一个容器创建某个文件),可以考虑使用 Init Container 模式(Kubernetes 原生支持)或编写自定义的入口脚本。
六、常见问题排查
问题 1:depends_on 不生效
-
检查是否使用了
docker compose(V2),V1 在某些情况下不支持条件语法。 -
确保依赖服务确实配置了
healthcheck,否则service_healthy条件永远不会满足,依赖方永远不会启动。 -
使用
docker compose config查看最终配置,确认depends_on块被正确解析。
问题 2:健康检查频繁失败
-
检查命令是否在容器中可执行(例如 Alpine 容器可能没有 curl)。
-
检查
start_period是否足够长,应用启动慢会导致早期检查失败。 -
增加
interval和timeout,避免误报。
问题 3:容器退出码为 0 但服务未就绪
- 典型情况:数据库启动后立即接受连接,但内部表还未创建。这时需要在应用代码中实现重试逻辑,或使用
service_completed_successfully等待初始化脚本完成。
七、命令速查表
八、本篇总结
-
三种启动条件 :
service_started、service_healthy、service_completed_successfully,解决竞态条件必须用后两种。 -
健康检查 :是
service_healthy的基础,Redis 用redis-cli ping,Web 应用用 HTTP 端点。 -
启动顺序实战 :通过
depends_on+healthcheck,确保 Flask 在 Redis 完全就绪后才启动,彻底告别"启动即崩溃"。 -
演进视角:Kubernetes 中的 Init Container 和 Pod 探针,正是 Compose 这套思想的集群级延伸。你在 Compose 里练熟的启动顺序控制,在 K8s 中将无缝迁移。
下一篇------第 16 篇:实战:用 Compose 编排 WordPress 与 MySQL,我们将跳出计数器的例子,编排一个更复杂的应用栈,让你在真实业务场景中巩固 Compose 的技能。
想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !