2.1 本地起一个最小集群:docker-compose 起步

场景:三个服务加一堆中间件,本地怎么跑

拆分方案敲定后,订单、商品、用户三个服务各自独立仓库,公共依赖却一个不少:MySQL 8.0、Redis 7、RocketMQ NameServer 加 Broker,偶尔还要个 etcd 做注册中心。第一周就有新人提issue:“我本地 MySQL 是 5.7,跑 produce 服务报语法错误。”另一位同事更直接:“我电脑上没装 RocketMQ,能不能先跳过?”

这种环境漂移问题,在单体时代只有一个大应用,还能靠一份 README 勉强压住;服务一多,每个服务一套依赖矩阵,组合数直接爆炸。我们的目标很务实:任何人 clone 代码后,一条命令把整个最小集群跑起来,和线上同一套中间件版本。

方案:一份 docker-compose.yml 兜底

选 docker-compose 而不是 K8s,理由很现实——团队五个人,没人有精力维护本地 K8s 集群;compose 的认知成本最低,而且 上一章技术选型表 里我们已经约定,线上是 CVM 直跑,本地编排只解决“跑通”问题,不追求与线上完全同构。

先定版本矩阵,写进团队 wiki:

组件 本地版本 线上对应 备注
Go 1.22 1.22 统一大版本
MySQL 8.0.36 云数据库 8.0 字符集统一 utf8mb4
Redis 7.2 自建 7.2 暂不引云缓存
RocketMQ 5.3.1 RocketMQ 5.x NameServer 加 Broker
etcd 3.5 3.5 注册中心

版本一定写死,别用 latest。我们最早图省事用 latest,某天 Redis 升到 7.4,本地某款 Lua 脚本行为变了,排查了大半天才发现是版本漂移。这种蠢坑一旦混进迭代节奏,代价远大于锁版本的那点麻烦。

除了版本,还有一件事要提前想好:三个服务的启动顺序。容器世界没有天生的先后,MySQL 没起来 order-service 就抢着连,只会留下一堆半启动的容器。我们把启动顺序的约束显式写进 compose,而不是靠大家脑补,这也是后面所有部署文档的行文风格——能写进配置的,绝不写进聊天记录。

然后是 compose 文件,只贴关键部分:

services:
  mysql:
    image: mysql:8.0.36
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: orders
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      retries: 10

  redis:
    image: redis:7.2
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]

  rmqnamesrv:
    image: apache/rocketmq:5.3.1
    command: sh mqnamesrv
    ports:
      - "9876:9876"

  rmqbroker:
    image: apache/rocketmq:5.3.1
    command: sh mqbroker -n rmqnamesrv:9876
    depends_on:
      - rmqnamesrv
    environment:
      NAMESRV_ADDR: rmqnamesrv:9876

  order-service:
    build: ../order
    environment:
      DSN: root:root123@tcp(mysql:3306)/orders?parseTime=true
      REDIS_ADDR: redis:6379
      NAMESRV_ADDR: rmqnamesrv:9876
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    ports:
      - "8081:8081"

volumes:
  mysql_data:

服务间用容器名互相访问,比如 order-service 里连的是 mysql:3306 而不是 localhost:3306。为了让本地配置和线上配置尽量一致,我们做了一个约定:服务读到的地址一定来自环境变量,代码里不写任何 IP 或主机名。线上把同样的环境变量换成 VPC 内网地址即可,这个习惯在后面 服务注册发现 一节还会用到。

启动命令封进 Makefile,免得每人背一堆参数:

up:
    docker compose up -d
    docker compose ps
logs:
    docker compose logs -f --tail=200 order-service
down:
    docker compose down
reset: down
    docker volume rm go-micro_mysql_data

踩坑与排查

坑一:depends_on 不等于就绪。 最早 order-service 直接依赖 mysql,起来就崩,日志里满屏连接拒绝。depends_on 只保证容器启动,不保证服务可连接。改成 healthcheck 加 condition: service_healthy 后才消停。如果中间件镜像没带健康检查指令,可以自己写一个 ping 脚本封进去。

坑二:Volume 挂载权限。 有同事在 WSL2 下跑,MySQL 容器起来就重启,日志提示 data 目录不可写,原因是宿主机目录权限与容器内 mysql 用户不一致。临时解法是退回命名卷(如上文的 mysql_data),需要改配置时再挂文件。

坑三:端口冲突。 3306 和 6379 是重灾区,财务那台老机器自己在跑本机 MySQL。我们的做法是统一改映射,比如 3326:3306,并把映射规则记在一份 env 文件里,避免每人各自为政又互相不知道。这一点在后面上云时也有对照:CVM 选型 时你会发现,云上的端口冲突问题其实变成了安全组问题。

常用命令我们也整理过一版,贴在团队群里随取随用:

命令 作用 使用时机
docker compose up -d 后台起全栈 每天开工
docker compose logs -f 服务名 跟踪某服务日志 排查启动失败
docker compose restart 服务名 单独重启一个服务 改完配置热验证
docker compose down 停止并删容器 下班清理
docker volume ls 查看持久卷 确认数据没丢

坑四:资源占用。 16G 内存的开发机跑这一套卯吃奶劲全用上。RocketMQ Broker 默认 JVM 堆 8G,直接 OOM。加了 JAVA_OPT=-Xms1g -Xmx1g 才压下去。本地编排一定要给中间件加资源限制,别让一台机器为中间件打工。

最后补一个习惯:compose 起来后第一件事跑一遍冒烟脚本(注册一个用户、下一单、查一次商品),三十秒内能判断环境是否真的通,比肉眼看 ps 强得多。这套冒烟脚本后来顺手挪进了 CI,成了 后续 CI 实践 的一部分。

还有一个收尾动作值得强调:每次 compose 文件有改动,务必在群里同步一句改了什么。我们吃过亏,有人把 MySQL 端口从 3306 换到 3326,自己本地改完没吱声,结果第二天另一位同事拉代码后死活连不上库,两个人对着各自的机器互相怀疑了半小时。环境配置的同步成本看似不起眼,积累起来比写代码本身还磨人。

小结

本地集群的核心不是 compose 写得多花哨,而是三个约定:版本锁死、地址走环境变量、健康检查先行。这三条做到了,新成员第一天就能投入开发,而不是和中间件搏斗。等到这套本地集群稳定跑了一周,我们就开始把它往云上搬——下一节先解决最现实的问题:CVM 到底买什么型号。

延伸阅读