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:
这个骨架,你记住三个核心块:services、networks、volumes。services 定义你的容器,networks 定义它们怎么通信,volumes 定义数据怎么持久化。缺一不可。
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 进程可能还在初始化。这时候战斗服去连数据库,肯定报错。怎么办?
我一般用两种方式解决:
- 健康检查(healthcheck):在数据库服务里加上
healthcheck配置,然后依赖方用condition: service_healthy。就像上面登录服务依赖数据库那样。 - 应用层重试:在代码里加连接重试逻辑。比如战斗服启动时,如果连不上数据库,等 3 秒再试,最多重试 5 次。这是兜底方案。
我曾经在一个项目里,只用了 depends_on 没加健康检查。结果每次部署,战斗服都有 30% 的概率启动失败。后来加上 healthcheck,问题彻底解决。嗯,这个坑我替你们踩过了。
depends_on 的 condition 选项在 Compose V2 中已经支持 service_started、service_healthy 和 service_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
启动顺序是这样的:
- 先启动
db,因为其他服务都依赖它。 - 数据库启动后,健康检查开始执行。每隔 10 秒检查一次,直到 MySQL 就绪。
- 登录服务检测到数据库健康后,开始启动。
- 网关服务等登录服务启动后,再启动。
- 战斗服务与登录服务并行启动,因为它只依赖数据库。
整个过程大概 30 秒到 1 分钟。如果数据库初始化脚本比较大,可能会更久。我建议在 init.sql 里只放必要的表结构,数据迁移放到应用启动时做。
好了,这一章的内容就到这里。下一章我们会深入聊聊 Docker 网络模型,以及如何让多个 Compose 项目之间互相通信。到时候我会分享一个我在微服务架构中踩过的网络坑,保证让你印象深刻。