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 思维 !

相关推荐
Waay2 小时前
K8s Deployment 滚动更新与回滚深度详解(含踩坑实录+生产选型原理)
云原生·容器·kubernetes
“码”力全开2 小时前
深度解析:基于 Docker 与边缘计算的 AI 视频管理平台架构——打通 GB28181/RTSP 协议与“源码交付”的高效集成方案
人工智能·docker·边缘计算
顾默@2 小时前
双系统Ubuntu18.04升级22.04,安装docker进行openclaw安装
运维·docker·容器
木卫二号Coding2 小时前
打包容器有两种方式
docker
蜀道山老天师3 小时前
Docker Compose 多容器编排实战:LNMP、Tomcat 集群、云桌面、Portainer、Zabbix 一键部署
运维·docker·容器·tomcat·zabbix
“码”力全开4 小时前
解构企业级安防中台:基于Docker容器化与GB28181/RTSP多协议汇聚的边缘计算AI视频管理平台(全量源码交付)
人工智能·docker·边缘计算
见牛羊4 小时前
docker理解
java·docker·容器
tellmewhoisi4 小时前
Docker Compose最巧妙的设计之一——内置的服务发现机制
docker·服务发现
AI服务老曹5 小时前
解耦异构算力:基于 Docker 与 GB28181/RTSP 的边缘计算 AI 视频管理平台架构设计(支持源码交付)
人工智能·docker·边缘计算