3. Docker Compose编排:Compose文件结构、定义多服务(网关、登录、战斗、数据库)、服务依赖与启动顺序控制

好,咱们进入正题。Docker Compose 这东西,说白了就是帮你把多个容器「串」起来的一个工具。你想想看,一个游戏服务器,不可能只有一个进程吧?网关、登录、战斗、数据库,少说四五个服务。手动一个个 docker run,那不得累死。而且每次重启,顺序还得自己记,烦不烦?

Compose 就是来解决这个问题的。它用一个 YAML 文件,把你整个服务拓扑描述清楚。然后一行 docker-compose up,全部搞定。我在项目中遇到过好几次,新同事上来就手动启动,结果数据库还没起来,战斗服先连了,报错一堆。嗯,用 Compose 就能避免这种低级错误。

3.1 Compose 文件结构:从零开始写一个 docker-compose.yml

先看一个最基础的结构。我个人习惯,每个项目根目录下放一个 docker-compose.yml,所有服务定义都在里面。版本我用 3.8,稳定,功能也够用。

version: '3.8'

services:
  # 服务定义写在这里

networks:
  game-net:
    driver: bridge

volumes:
  db-data:

这个骨架,你记住三个核心块:servicesnetworksvolumes。services 定义你的容器,networks 定义它们怎么通信,volumes 定义数据怎么持久化。缺一不可。

小提示: 我建议所有服务都放在同一个自定义网络里。默认的 bridge 网络虽然也能用,但服务名解析会出问题。自定义网络下,你可以直接用服务名当主机名来访问,方便很多。

3.2 定义多服务:网关、登录、战斗、数据库

好,现在我们来定义四个核心服务。这是游戏服务器最典型的架构了。我直接上代码,然后逐行解释。

version: '3.8'

services:
  # 1. 网关服务
  gateway:
    image: game-gateway:latest
    container_name: game-gateway
    ports:
      - "8080:8080"
    environment:
      - LOGIN_SERVICE_HOST=login
      - LOGIN_SERVICE_PORT=8081
    networks:
      - game-net
    depends_on:
      - login

  # 2. 登录服务
  login:
    image: game-login:latest
    container_name: game-login
    ports:
      - "8081:8081"
    environment:
      - DB_HOST=db
      - DB_PORT=3306
      - DB_USER=game
      - DB_PASSWORD=game123
    networks:
      - game-net
    depends_on:
      db:
        condition: service_healthy

  # 3. 战斗服务
  battle:
    image: game-battle:latest
    container_name: game-battle
    ports:
      - "8082:8082"
    environment:
      - DB_HOST=db
      - DB_PORT=3306
    networks:
      - game-net
    depends_on:
      - db

  # 4. 数据库
  db:
    image: mysql:8.0
    container_name: game-db
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: game_db
      MYSQL_USER: game
      MYSQL_PASSWORD: game123
    volumes:
      - db-data:/var/lib/mysql
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - game-net
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

networks:
  game-net:
    driver: bridge

volumes:
  db-data:

你看,每个服务都有几个关键字段:

  • image:指定镜像。我习惯用 latest 标签开发,但生产环境一定要用具体版本号。
  • container_name:给容器起个名字,方便 docker logs 查看。
  • ports:端口映射。注意,网关暴露 8080 给外部,内部服务之间用自定义网络通信,不需要暴露端口。
  • environment:环境变量。这是配置服务最常用的方式。我建议把数据库密码这类敏感信息放到 .env 文件里,别硬编码。
  • volumes:数据持久化。数据库的数据必须挂载,否则容器重启数据就丢了。这个坑我踩过,血的教训。
  • networks:指定网络。所有服务都在 game-net 里,互相能通过服务名访问。
重点: 网关服务通过环境变量 LOGIN_SERVICE_HOST=login 来知道登录服务的地址。这里的 login 就是 Compose 里定义的服务名,Docker 内置 DNS 会自动解析成容器 IP。你想想看,这比写死 IP 地址灵活多少倍?

3.3 服务依赖与启动顺序控制

这是最容易出问题的地方。很多新手以为 depends_on 能保证服务完全就绪。其实不是的。depends_on 只保证启动顺序,不保证服务已经可以接受请求。

举个例子,数据库容器启动了,但 MySQL 进程可能还在初始化。这时候战斗服去连数据库,肯定报错。怎么办?

我一般用两种方式解决:

  1. 健康检查(healthcheck):在数据库服务里加上 healthcheck 配置,然后依赖方用 condition: service_healthy。就像上面登录服务依赖数据库那样。
  2. 应用层重试:在代码里加连接重试逻辑。比如战斗服启动时,如果连不上数据库,等 3 秒再试,最多重试 5 次。这是兜底方案。

我曾经在一个项目里,只用了 depends_on 没加健康检查。结果每次部署,战斗服都有 30% 的概率启动失败。后来加上 healthcheck,问题彻底解决。嗯,这个坑我替你们踩过了。

注意: depends_oncondition 选项在 Compose V2 中已经支持 service_startedservice_healthyservice_completed_successfully。但要注意,service_healthy 要求被依赖的服务必须定义了 healthcheck。否则会报错。

3.4 启动顺序的完整控制策略

来,我总结一下实际项目中怎么控制启动顺序。这是我从多个项目里提炼出来的经验。

依赖关系 控制方式 说明
网关 → 登录 depends_on(默认) 网关只需要登录容器启动即可,不需要等待就绪
登录 → 数据库 depends_on + healthcheck 登录必须等数据库完全就绪才能连接
战斗 → 数据库 depends_on + 应用层重试 战斗服可以容忍数据库短暂不可用
战斗 → 登录 无直接依赖 战斗服不依赖登录服务,可以独立启动

你看,不是所有依赖都需要健康检查。过度使用 healthcheck 会增加启动时间。我的原则是:关键依赖用健康检查,非关键依赖用重试

实战技巧: 如果你用 docker-compose up -d 后台启动,可以用 docker-compose logs -f 实时查看日志。我经常开两个终端,一个看数据库日志,一个看应用日志,这样能快速定位启动问题。

3.5 完整启动流程演示

好,我们把上面的配置跑起来看看。执行命令:

# 启动所有服务
docker-compose up -d

# 查看启动状态
docker-compose ps

# 查看日志(实时)
docker-compose logs -f

# 只启动某个服务
docker-compose up -d battle

# 停止所有服务
docker-compose down

启动顺序是这样的:

  1. 先启动 db,因为其他服务都依赖它。
  2. 数据库启动后,健康检查开始执行。每隔 10 秒检查一次,直到 MySQL 就绪。
  3. 登录服务检测到数据库健康后,开始启动。
  4. 网关服务等登录服务启动后,再启动。
  5. 战斗服务与登录服务并行启动,因为它只依赖数据库。

整个过程大概 30 秒到 1 分钟。如果数据库初始化脚本比较大,可能会更久。我建议在 init.sql 里只放必要的表结构,数据迁移放到应用启动时做。

核心要点: Docker Compose 不是万能的。它解决的是「容器编排」问题,不是「服务编排」问题。真正的服务发现、负载均衡、弹性伸缩,那是 Kubernetes 的领域。但作为入门,Compose 已经足够让你把游戏服务器跑起来了。

好了,这一章的内容就到这里。下一章我们会深入聊聊 Docker 网络模型,以及如何让多个 Compose 项目之间互相通信。到时候我会分享一个我在微服务架构中踩过的网络坑,保证让你印象深刻。