Docker 教程

Docker Compose 依赖管理与编排

Docker Compose 在定义和管理多容器应用方面非常强大,它提供了声明服务间依赖关系、并编排它们启动和关闭的机制。本章将深入探讨 Docker Compose 如何处理应用依赖,以及它提供了哪些丰富的编排功能,来确保各项服务按正确的顺序启动并高效地协同工作。

1. 定义服务依赖关系

Docker Compose 允许你显式地定义服务之间的依赖关系,这能确保服务按照特定的顺序被创建和启动。这对于那些必须等某些组件运行后才能正常工作的应用来说至关重要,比如一个必须等待数据库就绪的 Web 应用。

docker-compose.yml 文件中,主要有两种方式来定义依赖关系:单纯使用 depends_on,以及将 healthcheck(健康检查)与 depends_on 结合使用。

1.1 使用 depends_on 控制启动顺序

depends_on 键建立了一种依赖关系,用于决定服务的启动顺序。当使用 depends_on 时,Compose 会确保被依赖的服务声明依赖的服务之前启动。同时,它也能保证在停止或移除服务时,按相反的顺序进行处理。

想象一个简单的 Web 应用,包含一个前端 Web 服务器(如 Nginx)、一个后端 API(如 Python Flask 应用)和一个数据库(如 PostgreSQL)。后端 API 需要数据库运行才能存取数据,而 Nginx 前端需要后端 API 来代理请求。

下面是一个演示 depends_ondocker-compose.yml 示例:

version: '3.8'
services:
  db:
    image: postgres:13
    environment:
      POSTGRES_DB: mydatabase
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    volumes:
      - db_data:/var/lib/postgresql/data
    networks:
      - app_network
      
  backend:
    build: ./backend # 假设在 'backend' 目录中有一个 Dockerfile
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgres://user:password@db:5432/mydatabase
    depends_on:
      - db # backend 服务依赖于 db 服务
    networks:
      - app_network
      
  frontend:
    image: nginx:latest
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - backend # frontend 服务依赖于 backend 服务
    networks:
      - app_network

networks:
  app_network:
    driver: bridge
volumes:
  db_data:

在这个配置中:

  • backend 会一直等待,直到 db 启动后才开始启动。
  • frontend 会一直等待,直到 backend 启动后才开始启动。

重要注意事项: 单纯的 depends_on 只能保证被依赖服务的容器已经启动。它不会等待容器内部的应用程序准备就绪或变为健康状态。 例如,一旦 db 容器运行起来,backend 就会立刻启动,即使 db 内部的 PostgreSQL 服务器还在初始化、根本无法接受连接。这会导致“竞争条件 (Race Conditions)”,即后端在数据库准备好之前就尝试连接,最终引发连接错误或应用崩溃。

1.2 结合 condition 与 healthcheck 确保服务健康就绪

为了解决上述的竞争条件问题,Compose 从 3.4 版本开始为 depends_on 引入了 condition(条件)选项。这允许你指定一个条件,被依赖的服务必须满足该条件,当前服务才会启动。可用的条件包括 service_started(已启动,默认值)、service_healthy(已健康)和 service_completed_successfully(已成功完成)。

service_healthy 条件与 healthcheck(健康检查)定义结合使用时尤其有效。healthcheck 允许你定义一条命令,Docker 会定期在容器内部运行该命令,以判断服务是否仍在正常运行。

让我们利用 healthcheckdepends_on: condition: service_healthy 来优化之前的例子:

version: '3.8'
services:
  db:
    image: postgres:13
    environment:
      POSTGRES_DB: mydatabase
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    volumes:
      - db_data:/var/lib/postgresql/data
    networks:
      - app_network
    healthcheck: # 为数据库服务定义健康检查
      test: ["CMD-SHELL", "pg_isready -U user -d mydatabase"]
      interval: 5s
      timeout: 5s
      retries: 5
      
  backend:
    build: ./backend
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgres://user:password@db:5432/mydatabase
    depends_on:
      db:
        condition: service_healthy # 后端将等待,直到 db 报告为健康状态
    networks:
      - app_network
    healthcheck: # 为后端服务定义健康检查
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"] # 假设有一个健康检查 API 节点
      interval: 10s
      timeout: 5s
      retries: 3
      
  frontend:
    image: nginx:latest
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      backend:
        condition: service_healthy # 前端将等待,直到后端报告为健康状态
    networks:
      - app_network

networks:
  app_network:
    driver: bridge
volumes:
  db_data:

在这个更新后的示例中:

  • db 服务有一个 healthcheck,它使用 pg_isready 命令来检查 PostgreSQL 是否准备好接受连接。
  • backend 服务将不会启动,直到 db 服务的健康检查报告其状态为 healthy
  • backend 服务也有自己的 healthcheck,frontend 会等待它。
  • frontend 服务将不会启动,直到 backend 服务报告为 healthy。

这种方法确保了启动过程极其稳健,防止了服务去连接还不可用的依赖项。

1.3 假设场景:微服务支付系统

想象一个用于支付处理系统的微服务架构:

  • payment-gateway (支付网关): 处理外部支付请求,需要存储交易详情。
  • transaction-database (交易数据库): 存储所有交易记录。
  • fraud-detection-service (欺诈检测服务): 分析交易是否存在可疑活动,需要 transaction-database 的数据。
  • notification-service (通知服务): 发送成功或失败支付的邮件/短信提醒,依赖 payment-gateway 来触发通知。
version: '3.8'
services:
  transaction-database:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: transactions
    volumes:
      - transaction_db_data:/var/lib/mysql
    networks:
      - payment_network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot_password"]
      interval: 10s
      timeout: 5s
      retries: 5
      
  fraud-detection-service:
    build: ./fraud-detection
    environment:
      DB_HOST: transaction-database
      DB_NAME: transactions
      DB_USER: root
      DB_PASSWORD: root_password
    depends_on:
      transaction-database:
        condition: service_healthy
    networks:
      - payment_network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5001/health"] # 假设欺诈服务有一个健康节点
      interval: 15s
      timeout: 5s
      retries: 3
      
  payment-gateway:
    build: ./payment-gateway
    ports:
      - "8080:8080"
    environment:
      TRANSACTION_SERVICE_URL: http://fraud-detection-service:5001 # 网关与欺诈服务通信
    depends_on:
      fraud-detection-service:
        condition: service_healthy # 支付网关等待欺诈服务变为健康状态
    networks:
      - payment_network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/status"] # 假设网关有一个状态节点
      interval: 10s
      timeout: 5s
      retries: 3
      
  notification-service:
    build: ./notification-service
    environment:
      GATEWAY_URL: http://payment-gateway:8080 # 通知服务可能会订阅网关的事件
    depends_on:
      payment-gateway:
        condition: service_healthy # 通知服务需要网关运行才能接收事件
    networks:
      - payment_network

networks:
  payment_network:
    driver: bridge
volumes:
  transaction_db_data:

这确保了一个健壮的启动序列:数据库健康后欺诈检测才启动;欺诈检测健康后支付网关才接受请求;支付网关正常运作后通知服务才准备就绪。

2. 编排功能扩展

除了简单的启动顺序,Docker Compose 还提供了其他几种编排功能,帮助你有效地管理多容器应用。

2.1 容器重启策略 (Restart Policies)

重启策略规定了当容器停止时 Docker 应该作何反应。这对于保持应用的可用性和自动从故障中恢复来说必不可少。你可以使用 restart 键为每个服务定义重启策略。

常见的 restart 策略值:

  • no: 不自动重启容器(默认值)。
  • on-failure: 只有当容器以非零退出代码(表示发生错误)退出时,才重启容器。
  • always: 无论退出代码是什么,只要容器停止了就总是重启。当 Docker 守护进程启动时,它也会重启那些配置为 always 且在守护进程停止时正在运行的容器。
  • unless-stopped: 总是重启容器,除非它被显式地停止了(例如通过 docker stop <容器名>)。如果 Docker 守护进程重启,该容器也会跟着重启。

带有重启策略的示例:

version: '3.8'
services:
  db:
    image: postgres:13
    # ... (省略环境变量和数据卷等配置)
    restart: unless-stopped # 数据库应该一直运行,除非被手动停止
    
  backend:
    build: ./backend
    # ...
    depends_on:
      db:
        condition: service_healthy
    restart: on-failure # 如果后端崩溃了,应该重启
    
  frontend:
    image: nginx:latest
    # ...
    depends_on:
      backend:
        condition: service_healthy
    restart: always # 前端 Web 服务器应该总是尝试保持可用状态

在这个例子中:

  • db 服务将自动重启,除非它被 docker stop 手动停止。
  • backend 服务只有在遇到错误并以非零状态码退出时才会重启。
  • frontend 服务如果停止了会永远尝试重启,确保 Web 界面持续可用。

2.2 服务扩展 (Service Scaling)

Docker Compose 允许你“扩展 (Scale)”服务,这意味着你可以运行特定服务的多个相同实例(副本),以应对增加的负载或提供数据冗余。虽然 Compose 本身不像 Docker Swarm 或 Kubernetes 那样提供动态、自动的扩展,但你可以使用 docker compose up --scale 命令来手动扩展服务。

例如,要运行 3 个 backend 服务实例:

docker compose up -d --scale backend=3

Compose 会为 backend 服务创建三个独立的容器,每个容器都有唯一的名字(例如 myapp-backend-1, myapp-backend-2, myapp-backend-3)。这分散了负载并提供了一定的容错能力。如果一个后端实例挂了,其他实例可以继续处理请求(前提是在我们这个例子中,像 Nginx 这样的负载均衡器配置了将流量分发到它们身上)。

2.3 实战应用:电商平台架构

考虑一个包含几个关键服务的电商平台:

  • Product Catalog Service (商品目录服务): 提供商品信息、图片、价格。(读取数据库)
  • Order Processing Service (订单处理服务): 处理用户订单、更新库存。(写入数据库,与支付网关交互)
  • User Authentication Service (用户认证服务): 管理用户登录、注册。(读/写数据库)
  • Redis Cache (Redis 缓存): 被多个服务用于会话管理和快速数据检索。
  • PostgreSQL Database (PostgreSQL 数据库): 所有服务的持久化存储。
  • Load Balancer (Nginx 负载均衡器): 将流量分发到商品目录、订单处理和用户认证服务。

一个健壮的 Compose 配置会充分利用依赖和编排功能:

version: '3.8'
services:
  database:
    image: postgres:13
    environment:
      POSTGRES_DB: ecommerce_db
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    volumes:
      - db_data:/var/lib/postgresql/data
    networks:
      - ecommerce_network
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d ecommerce_db"]
      interval: 5s
      timeout: 5s
      retries: 5
      
  redis:
    image: redis:6-alpine
    networks:
      - ecommerce_network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 1s
      timeout: 3s
      retries: 5
      
  product-catalog:
    build: ./product-catalog
    environment:
      DB_HOST: database
      REDIS_HOST: redis
    depends_on:
      database:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - ecommerce_network
    restart: on-failure
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8001/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      
  user-auth:
    build: ./user-auth
    environment:
      DB_HOST: database
      REDIS_HOST: redis
    depends_on:
      database:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - ecommerce_network
    restart: on-failure
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8002/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      
  order-processing:
    build: ./order-processing
    environment:
      DB_HOST: database
      PRODUCT_CATALOG_URL: http://product-catalog:8001
    depends_on:
      database:
        condition: service_healthy
      product-catalog:
        condition: service_healthy
    networks:
      - ecommerce_network
    restart: on-failure
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8003/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      
  nginx:
    image: nginx:latest
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      product-catalog:
        condition: service_healthy
      user-auth:
        condition: service_healthy
      order-processing:
        condition: service_healthy
    networks:
      - ecommerce_network
    restart: always

networks:
  ecommerce_network:
    driver: bridge
volumes:
  db_data:

此配置确保了:

  1. 在任何应用服务尝试连接之前,Database 和 Redis 必须是健康的。
  2. 在 Nginx 负载均衡器开始路由流量之前,所有应用服务(商品、认证、订单)必须是健康的。
  3. 所有核心基础设施(database, redis, nginx)都配置了 restart: unless-stoppedalways 以保证高可用性。
  4. 应用服务配置为 restart: on-failure,以便从内部错误中恢复。