Docker从0到1再到 Kubernetes 实战:第15篇Compose 中的服务依赖、健康检查与启动顺序

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 是否足够长,应用启动慢会导致早期检查失败。

  • 增加 intervaltimeout,避免误报。

问题 3:容器退出码为 0 但服务未就绪

  • 典型情况:数据库启动后立即接受连接,但内部表还未创建。这时需要在应用代码中实现重试逻辑,或使用 service_completed_successfully 等待初始化脚本完成。

七、命令速查表

八、本篇总结

  • 三种启动条件service_startedservice_healthyservice_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 思维 !

相关推荐
长栎6 分钟前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode10 分钟前
Redis 在生产项目的使用
前端·后端
用户5598224812214 分钟前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode15 分钟前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战16 分钟前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha35 分钟前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn36 分钟前
Docker 容器管理入门 — 从镜像到容器编排
后端
用户7623524259140 分钟前
ShardingJDBC
后端
行者全栈架构师41 分钟前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端
Colin草率地做慢慢地改1 小时前
关于QuickStore这个项目的重构(2)- 数据库建表文件
后端·面试·架构