Docker Compose微服务编排:从单机到集群实战
发布日期: 2026/07/18 阅读总量: 0
Docker Compose微服务编排实战

真实场景:一个订单系统引发的血案

去年我接手一个电商订单系统,6个微服务(用户、商品、订单、支付、通知、日志),用Docker Compose跑在单机。上线第一天,用户量冲到500并发,订单服务直接OOM,MySQL连接池爆满,Redis缓存穿透导致数据库CPU 100%。

问题根源:Compose默认所有服务共享网络和资源,没有隔离和限流。订单服务吃光内存,其他服务跟着崩。这不是Compose的错,是我没做编排优化。

方案对比:原生Compose vs 扩展配置

我试了两种方案:

  • 方案A:原生Docker Compose(v2.24.0)—— 直接写docker-compose.yml,所有服务放一个文件,靠depends_on控制启动顺序。
  • 方案B:扩展配置 + 资源限制 —— 用extends和profiles拆分环境,加deploy.resources限制CPU/内存,用healthcheck做健康检查。

压测环境:阿里云ECS 4核8G,CentOS 7.9,Docker 24.0.7,Compose 2.24.0。压测工具:wrk 4.2.0,每个服务单独压5分钟。

指标方案A方案B提升
订单服务QPS320890178%
内存峰值(订单服务)2.1GB512MB75%
MySQL连接数200(爆满)50(稳定)75%
整体响应时间P992.3s0.8s65%

完整代码实现

1. 项目结构

order-system/
├── docker-compose.yml          # 主编排文件
├── docker-compose.override.yml # 开发环境覆盖
├── docker-compose.prod.yml     # 生产环境扩展
├── services/
│   ├── user-service/
│   │   ├── Dockerfile
│   │   └── src/
│   ├── order-service/
│   │   ├── Dockerfile
│   │   └── src/
│   ├── product-service/
│   │   ├── Dockerfile
│   │   └── src/
│   ├── payment-service/
│   │   ├── Dockerfile
│   │   └── src/
│   ├── notification-service/
│   │   ├── Dockerfile
│   │   └── src/
│   └── log-service/
│       ├── Dockerfile
│       └── src/
├── config/
│   ├── mysql/
│   │   └── my.cnf
│   ├── redis/
│   │   └── redis.conf
│   └── nginx/
│       └── default.conf
└── .env

2. 主编排文件 docker-compose.yml

version: '3.8'

services:
  # 基础服务
  mysql:
    image: mysql:8.0.35
    container_name: order-mysql
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: order_db
      MYSQL_USER: order_user
      MYSQL_PASSWORD: ${DB_USER_PASSWORD}
    volumes:
      - mysql_data:/var/lib/mysql
      - ./config/mysql/my.cnf:/etc/mysql/conf.d/my.cnf:ro
    ports:
      - "3306:3306"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1G
        reservations:
          cpus: '0.5'
          memory: 512M

  redis:
    image: redis:7.2-alpine
    container_name: order-redis
    restart: unless-stopped
    command: redis-server /usr/local/etc/redis/redis.conf --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
      - ./config/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

  # 微服务
  user-service:
    build:
      context: ./services/user-service
      dockerfile: Dockerfile
    container_name: order-user
    restart: unless-stopped
    environment:
      APP_ENV: ${APP_ENV}
      DB_HOST: mysql
      DB_PORT: 3306
      DB_DATABASE: order_db
      DB_USERNAME: order_user
      DB_PASSWORD: ${DB_USER_PASSWORD}
      REDIS_HOST: redis
      REDIS_PORT: 6379
      REDIS_PASSWORD: ${REDIS_PASSWORD}
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    ports:
      - "8081:80"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
        reservations:
          cpus: '0.25'
          memory: 128M

  order-service:
    build:
      context: ./services/order-service
      dockerfile: Dockerfile
    container_name: order-order
    restart: unless-stopped
    environment:
      APP_ENV: ${APP_ENV}
      DB_HOST: mysql
      DB_PORT: 3306
      DB_DATABASE: order_db
      DB_USERNAME: order_user
      DB_PASSWORD: ${DB_USER_PASSWORD}
      REDIS_HOST: redis
      REDIS_PORT: 6379
      REDIS_PASSWORD: ${REDIS_PASSWORD}
      USER_SERVICE_URL: http://user-service:80
      PRODUCT_SERVICE_URL: http://product-service:80
    depends_on:
      user-service:
        condition: service_healthy
      product-service:
        condition: service_healthy
    ports:
      - "8082:80"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M

  product-service:
    build:
      context: ./services/product-service
      dockerfile: Dockerfile
    container_name: order-product
    restart: unless-stopped
    environment:
      APP_ENV: ${APP_ENV}
      DB_HOST: mysql
      DB_PORT: 3306
      DB_DATABASE: order_db
      DB_USERNAME: order_user
      DB_PASSWORD: ${DB_USER_PASSWORD}
      REDIS_HOST: redis
      REDIS_PORT: 6379
      REDIS_PASSWORD: ${REDIS_PASSWORD}
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    ports:
      - "8083:80"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
        reservations:
          cpus: '0.25'
          memory: 128M

  payment-service:
    build:
      context: ./services/payment-service
      dockerfile: Dockerfile
    container_name: order-payment
    restart: unless-stopped
    environment:
      APP_ENV: ${APP_ENV}
      DB_HOST: mysql
      DB_PORT: 3306
      DB_DATABASE: order_db
      DB_USERNAME: order_user
      DB_PASSWORD: ${DB_USER_PASSWORD}
      REDIS_HOST: redis
      REDIS_PORT: 6379
      REDIS_PASSWORD: ${REDIS_PASSWORD}
      ORDER_SERVICE_URL: http://order-service:80
    depends_on:
      order-service:
        condition: service_healthy
    ports:
      - "8084:80"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
        reservations:
          cpus: '0.25'
          memory: 128M

  notification-service:
    build:
      context: ./services/notification-service
      dockerfile: Dockerfile
    container_name: order-notification
    restart: unless-stopped
    environment:
      APP_ENV: ${APP_ENV}
      REDIS_HOST: redis
      REDIS_PORT: 6379
      REDIS_PASSWORD: ${REDIS_PASSWORD}
    depends_on:
      redis:
        condition: service_healthy
    ports:
      - "8085:80"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    deploy:
      resources:
        limits:
          cpus: '0.25'
          memory: 128M
        reservations:
          cpus: '0.1'
          memory: 64M

  log-service:
    build:
      context: ./services/log-service
      dockerfile: Dockerfile
    container_name: order-log
    restart: unless-stopped
    environment:
      APP_ENV: ${APP_ENV}
      DB_HOST: mysql
      DB_PORT: 3306
      DB_DATABASE: order_db
      DB_USERNAME: order_user
      DB_PASSWORD: ${DB_USER_PASSWORD}
    depends_on:
      mysql:
        condition: service_healthy
    ports:
      - "8086:80"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    deploy:
      resources:
        limits:
          cpus: '0.25'
          memory: 128M
        reservations:
          cpus: '0.1'
          memory: 64M

volumes:
  mysql_data:
  redis_data:

3. 生产环境扩展 docker-compose.prod.yml

version: '3.8'

services:
  mysql:
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.role == manager
      resources:
        limits:
          cpus: '2.0'
          memory: 2G
        reservations:
          cpus: '1.0'
          memory: 1G

  redis:
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.role == manager
      resources:
        limits:
          cpus: '1.0'
          memory: 1G
        reservations:
          cpus: '0.5'
          memory: 512M

  user-service:
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first
      restart_policy:
        condition: on-failure
        max_attempts: 3
        window: 120s
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
        reservations:
          cpus: '0.25'
          memory: 128M

  order-service:
    deploy:
      replicas: 5
      update_config:
        parallelism: 2
        delay: 5s
        order: start-first
      restart_policy:
        condition: on-failure
        max_attempts: 3
        window: 120s
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M

  product-service:
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first
      restart_policy:
        condition: on-failure
        max_attempts: 3
        window: 120s
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
        reservations:
          cpus: '0.25'
          memory: 128M

  payment-service:
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first
      restart_policy:
        condition: on-failure
        max_attempts: 3
        window: 120s
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
        reservations:
          cpus: '0.25'
          memory: 128M

  notification-service:
    deploy:
      replicas: 2
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first
      restart_policy:
        condition: on-failure
        max_attempts: 3
        window: 120s
      resources:
        limits:
          cpus: '0.25'
          memory: 128M
        reservations:
          cpus: '0.1'
          memory: 64M

  log-service:
    deploy:
      replicas: 2
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first
      restart_policy:
        condition: on-failure
        max_attempts: 3
        window: 120s
      resources:
        limits:
          cpus: '0.25'
          memory: 128M
        reservations:
          cpus: '0.1'
          memory: 64M

4. .env 环境变量文件

# 应用环境
APP_ENV=production

# 数据库
DB_ROOT_PASSWORD=root_secret_2024
DB_USER_PASSWORD=user_secret_2024

# Redis
REDIS_PASSWORD=redis_secret_2024

# 服务端口
USER_SERVICE_PORT=8081
ORDER_SERVICE_PORT=8082
PRODUCT_SERVICE_PORT=8083
PAYMENT_SERVICE_PORT=8084
NOTIFICATION_SERVICE_PORT=8085
LOG_SERVICE_PORT=8086

5. 服务Dockerfile示例(order-service)

FROM php:8.3-fpm-alpine AS base

# 安装依赖
RUN apk add --no-cache \
    curl \
    git \
    unzip \
    libzip-dev \
    oniguruma-dev \
    && docker-php-ext-install pdo_mysql mbstring zip opcache

# 安装Composer
COPY --from=composer:2.7 /usr/bin/composer /usr/bin/composer

# 复制应用代码
WORKDIR /var/www/html
COPY . .

# 安装PHP依赖
RUN composer install --no-dev --optimize-autoloader --no-interaction

# 配置Opcache
RUN echo "opcache.enable=1\nopcache.memory_consumption=128\nopcache.interned_strings_buffer=8\nopcache.max_accelerated_files=10000\nopcache.revalidate_freq=2\nopcache.fast_shutdown=1" > /usr/local/etc/php/conf.d/opcache.ini

# 生产阶段
FROM base AS production
COPY --from=base /var/www/html /var/www/html
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache

EXPOSE 80

CMD ["php-fpm"]

6. 启动脚本 deploy.sh

#!/bin/bash
set -euo pipefail

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

log_info() {
    echo -e "${GREEN}[INFO]${NC} $1"
}

log_warn() {
    echo -e "${YELLOW}[WARN]${NC} $1"
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $1"
}

# 检查Docker和Compose
check_dependencies() {
    if ! command -v docker &> /dev/null; then
        log_error "Docker未安装,请先安装Docker 24.0+"
        exit 1
    fi

    if ! command -v docker-compose &> /dev/null; then
        log_error "Docker Compose未安装,请先安装Compose 2.24+"
        exit 1
    fi

    log_info "Docker版本: $(docker --version)"
    log_info "Compose版本: $(docker-compose --version)"
}

# 清理旧容器和镜像
cleanup() {
    log_warn "清理旧容器..."
    docker-compose down --remove-orphans 2>/dev/null || true
    docker system prune -f 2>/dev/null || true
}

# 构建镜像
build_images() {
    log_info "构建服务镜像..."
    docker-compose build --parallel --no-cache
}

# 启动服务
start_services() {
    local env=${1:-production}

    if [ "$env" == "production" ]; then
        log_info "启动生产环境服务..."
        docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
    else
        log_info "启动开发环境服务..."
        docker-compose up -d
    fi
}

# 健康检查
health_check() {
    log_info "等待服务启动..."
    sleep 30

    local services=("user-service" "order-service" "product-service" "payment-service" "notification-service" "log-service")
    local all_healthy=true

    for service in "${services[@]}"; do
        local container_name="order-${service%-service}"
        local status=$(docker inspect --format='{{.State.Health.Status}}' "$container_name" 2>/dev/null || echo "unknown")

        if [ "$status" == "healthy" ]; then
            log_info "$service: 健康"
        else
            log_error "$service: 不健康 (状态: $status)"
            all_healthy=false
        fi
    done

    if [ "$all_healthy" == false ]; then
        log_error "部分服务不健康,请检查日志"
        exit 1
    fi

    log_info "所有服务健康!"
}

# 主流程
main() {
    local env=${1:-production}

    log_info "开始部署订单系统(环境: $env)"

    check_dependencies
    cleanup
    build_images
    start_services "$env"
    health_check

    log_info "部署完成!"
    log_info "服务列表:"
    echo "  - 用户服务: http://localhost:8081"
    echo "  - 订单服务: http://localhost:8082"
    echo "  - 商品服务: http://localhost:8083"
    echo "  - 支付服务: http://localhost:8084"
    echo "  - 通知服务: http://localhost:8085"
    echo "  - 日志服务: http://localhost:8086"
}

main "$@"

效果数据

压测环境:阿里云ECS 4核8G,CentOS 7.9,Docker 24.0.7,Compose 2.24.0。压测工具:wrk 4.2.0,每个服务压5分钟,并发100。

服务方案A QPS方案B QPS提升方案A P99(ms)方案B P99(ms)
user-service4501200166%1800600
order-service320890178%2300800
product-service5001300160%1500500
payment-service280750167%2500900
notification-service6001500150%1200400
log-service7001800157%1000350

内存占用对比(docker stats采集10分钟平均值):

服务方案A内存(MB)方案B内存(MB)降低
mysql102451250%
redis25612850%
user-service38419250%
order-service204851275%
product-service51225650%
payment-service76838450%
notification-service25612850%
log-service1929650%

避坑指南

我踩过的坑,列出来你们直接避:

  • 坑1:depends_on不是万能的。depends_on只控制启动顺序,不等待服务就绪。MySQL启动需要30秒,但Compose会立即启动依赖服务,导致连接失败。必须用healthcheck + condition: service_healthy。
  • 坑2:资源限制不设等于没限制。默认Compose不限制资源,一个服务OOM会拖垮整个宿主机。必须给每个服务设deploy.resources.limits,特别是内存。
  • 坑3:端口冲突。多个服务映射同一宿主机端口会报错。用不同端口映射,或者用内部网络通信(推荐)。内部网络用服务名访问,不需要端口映射。
  • 坑4:.env文件变量覆盖。Compose会加载.env文件,但docker-compose.yml里直接写变量名(如${DB_PASSWORD})时,如果.env里没定义,Compose会报错。所有变量必须在.env里定义默认值。
  • 坑5:镜像构建缓存。docker-compose build默认用缓存,如果Dockerfile改了但依赖没变,缓存可能导致旧代码。用--no-cache强制重建,或者用--pull拉取最新基础镜像。
  • 坑6:日志爆盘。默认Docker日志驱动是json-file,不限制大小。一个服务一天能写几GB日志,把磁盘撑爆。加logging配置:logging: driver: "json-file" options: max-size: "10m" max-file: "3"
  • 坑7:网络性能。默认bridge网络性能一般,高并发场景用host网络模式(Linux only)或overlay网络(Swarm模式)。我测试过,bridge网络下QPS比host低15%。
  • 坑8:版本兼容。Compose v2和v3版本差异大,v3支持Swarm模式但有些特性(如extends)在v3里被移除。我统一用v3.8,兼容性最好。

扩展:多节点集群

单机Compose扛不住时,上Docker Swarm。把docker-compose.prod.yml里的deploy配置直接用在Swarm里,用docker stack deploy -c docker-compose.prod.yml order-system部署。Swarm自动做服务发现和负载均衡,不需要额外配置。

Swarm模式下,服务间通信用overlay网络,性能比bridge好20%。我测试过,3节点Swarm集群,订单服务QPS从890提升到2500。

总结

Docker Compose不是玩具,做好资源限制、健康检查、日志管理,能稳定跑生产。别偷懒,每个服务都配好healthcheck和deploy.resources。单机扛不住就上Swarm,配置几乎不用改。