Administrator
发布于 2020-02-25 / 3391 阅读
85

Docker Compose 编排一整套开发环境

新人装环境装了两天,我写了份 docker-compose.yml

组里 2 月来了个新同事。第一天拉代码,第二天还在拉代码——不是网速问题,是在装环境:MySQL 8.0、Redis 5.0、RocketMQ 4.6.1、Nacos 1.1.4、Elasticsearch 7.5,一个一个装,中间还把本地 MySQL 5.7 覆盖了,折腾掉两天。

这事我两年前也干过。当时我就想,这套中间件组合其实每个项目都一样,为什么不固化下来。于是花了一下午写了份 docker-compose.yml,现在新人入职只需要三步:git clonedocker-compose up -d、启动应用。

用的是 Docker 19.03 和 docker-compose 1.25.4,文件格式 version: '3.7'

完整的文件

version: '3.7'

services:
  mysql:
    image: mysql:8.0.19
    container_name: dev-mysql
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: shop
      TZ: Asia/Shanghai
    command:
      --default-authentication-plugin=mysql_native_password
      --character-set-server=utf8mb4
      --collation-server=utf8mb4_unicode_ci
      --max_connections=500
      --innodb_buffer_pool_size=512M
    volumes:
      - mysql-data:/var/lib/mysql
      - ./initdb:/docker-entrypoint-initdb.d
    networks:
      - dev-net
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot123"]
      interval: 5s
      timeout: 3s
      retries: 20

  redis:
    image: redis:5.0.7
    container_name: dev-redis
    ports:
      - "6379:6379"
    command: redis-server --requirepass redis123 --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis-data:/data
    networks:
      - dev-net

  rocketmq-namesrv:
    image: rocketmqinc/rocketmq:4.6.1
    container_name: dev-mq-namesrv
    ports:
      - "9876:9876"
    environment:
      JAVA_OPT_EXT: "-server -Xms256m -Xmx256m -Xmn128m"
    command: sh mqnamesrv
    networks:
      - dev-net

  rocketmq-broker:
    image: rocketmqinc/rocketmq:4.6.1
    container_name: dev-mq-broker
    ports:
      - "10909:10909"
      - "10911:10911"
    environment:
      JAVA_OPT_EXT: "-server -Xms512m -Xmx512m -Xmn256m"
    command: sh mqbroker -n rocketmq-namesrv:9876 -c /opt/rocketmq-4.6.1/conf/broker.conf
    volumes:
      - ./broker.conf:/opt/rocketmq-4.6.1/conf/broker.conf
      - mq-data:/root/store
    depends_on:
      - rocketmq-namesrv
    networks:
      - dev-net

  nacos:
    image: nacos/nacos-server:1.1.4
    container_name: dev-nacos
    ports:
      - "8848:8848"
    environment:
      MODE: standalone
      SPRING_DATASOURCE_PLATFORM: mysql
      MYSQL_SERVICE_HOST: mysql
      MYSQL_SERVICE_PORT: 3306
      MYSQL_SERVICE_DB_NAME: nacos_config
      MYSQL_SERVICE_USER: root
      MYSQL_SERVICE_PASSWORD: root123
    depends_on:
      mysql:
        condition: service_healthy
    networks:
      - dev-net

networks:
  dev-net:
    driver: bridge

volumes:
  mysql-data:
  redis-data:
  mq-data:

几个必须讲清楚的坑

一、depends_on 不等于"已经就绪"

这是最坑的一个。depends_on 只保证容器被创建并启动,不保证里面的服务能对外提供服务。MySQL 第一次启动要初始化数据目录、执行 /docker-entrypoint-initdb.d 下的 SQL,我的初始化脚本有 6 张表加 2 万条测试数据,要跑 40 多秒。

结果就是:Nacos 容器起来了,连 MySQL 连不上,直接退出。日志:

Caused by: java.sql.SQLNonTransientConnectionException: Could not create connection to database server.
Attempted reconnect 3 times. Giving up.

compose 文件格式 3.x 里 depends_on 支持 condition: service_healthy(需要 2.1 以上,3.x 的 long syntax 在某些版本被移除了又加回来,1.25.4 是支持的)。配合 healthcheck 用,上面的配置里 Nacos 就是这么写的。

如果你用的是不支持的旧版本,用 wait-for-it.sh 兜底:

  nacos:
    # ...
    command: ["/bin/sh", "-c", "chmod +x /wait-for-it.sh && \
              /wait-for-it.sh mysql:3306 -t 90 -- sh /home/nacos/start.sh -m standalone"]
    volumes:
      - ./wait-for-it.sh:/wait-for-it.sh

二、RocketMQ 的 broker 会注册错 IP

容器里 broker 启动时,会把自己的容器内网 IP(比如 172.20.0.5)注册给 nameserver。宿主机上的应用拿到这个 IP 之后连不上,因为那 IP 只在 Docker 网桥里可达。

报错是这个:

org.apache.rocketmq.remoting.exception.RemotingConnectException:
  connect to <172.20.0.5:10911> failed

解决办法是显式指定 brokerIP1。单独的 broker.conf

# broker.conf
brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
# 关键:填宿主机的 IP,不是容器 IP
brokerIP1 = 192.168.1.47

然后 volumes 挂载进去并在 command 里用 -c 指定。填宿主机内网 IP 之后,本机应用和局域网内其他同事都能连。

如果觉得写死 IP 太脏,也可以用 extra_hosts 加环境变量拼,但开发环境没必要搞那么复杂。

三、MySQL 8 的认证插件

MySQL 8.0 默认用 caching_sha2_password,而很多老客户端(包括我们项目里 Druid 1.1.20 之前的版本、还有 Navicat 11)不支持。连上去报:

Unable to load authentication plugin 'caching_sha2_password'.

--default-authentication-plugin=mysql_native_password 就退回旧认证方式。这个参数只在第一次初始化时生效,如果数据目录已经有数据了,得先 docker-compose down -v 删掉卷重建,或者手动执行:

ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root123';
FLUSH PRIVILEGES;

四、数据卷:named volume 还是挂载目录

我选了 named volume(mysql-data:redis-data:),理由是跨平台。团队里有 Windows 同事,bind mount 在 Windows 上要处理路径和文件权限,麻烦。

代价是看数据没那么直接。要导数据时用临时容器:

$ docker run --rm -v dev_mysql-data:/data -v $(pwd):/backup \
    busybox tar czf /backup/mysql-backup.tar.gz /data

注意 volume 名在 docker 里会被加上项目名前缀,dev 是目录名。用 docker volume ls 确认。

另外 docker-compose down 不会删除 named volume,数据还在。要连数据一起删必须 docker-compose down -v。这个行为有好有坏,我第一次想重建 MySQL 时 down 了三次都没清掉,后来才发现。

五、初始化 SQL 怎么塞进去

MySQL 官方镜像会自动执行 /docker-entrypoint-initdb.d 目录下的 .sh.sql.sql.gz,按文件名排序。我把建表脚本放进去:

initdb/
  01-schema.sql      # 建库建表
  02-base-data.sql   # 字典表、测试数据

只在数据目录为空时执行一次。已经初始化过之后改这些 SQL 不会重跑,得手动执行。

常用命令

# 后台启动全部
$ docker-compose up -d

# 只启动某个,比如先起 MySQL 和 Redis
$ docker-compose up -d mysql redis

# 看日志,追着看
$ docker-compose logs -f nacos

# 看状态
$ docker-compose ps

# 停掉但保留数据
$ docker-compose stop

# 销毁容器(数据卷保留)
$ docker-compose down

# 销毁容器并且删数据卷,彻底重来
$ docker-compose down -v

# 改了 yml 之后重建单个服务
$ docker-compose up -d --force-recreate redis

进容器里排查:

$ docker-compose exec mysql mysql -uroot -proot123
$ docker-compose exec redis redis-cli -a redis123
$ docker-compose exec nacos sh

资源占用

全起之后我看了一眼:

$ docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
NAME                 CPU %     MEM USAGE / LIMIT
dev-mysql            0.71%     412MiB / 512MiB
dev-redis            0.15%     8.2MiB / 256MiB
dev-mq-namesrv       0.33%     248MiB / 300MiB
dev-mq-broker        1.02%     596MiB / 700MiB
dev-nacos            0.88%     724MiB / 800MiB

加起来不到 2 GB。我给每个服务都加了 mem_limit(在 deploy 之外 compose 3.x 非 swarm 模式下要用 mem_limit 字段),免得 Nacos 吃太多把笔记本拖垮。8G 内存的机器跑完全没问题。

小结

  • depends_on 只等容器启动,不等服务就绪。要么用 condition: service_healthyhealthcheck,要么用 wait-for-it.sh
  • RocketMQ broker 在容器里会注册错 IP,必须配 brokerIP1 指向宿主机。
  • MySQL 8 要加 --default-authentication-plugin=mysql_native_password,否则老客户端连不上,且只在首次初始化时生效。
  • named volume 跨平台友好,但 docker-compose down 不删它,重装要 -v
  • 给每个中间件加内存限制,不然开发机上跑一天就卡了。

写完之后新同事的环境花了 18 分钟搭好,其中 12 分钟在拉镜像。内网如果有镜像仓库的话,把这几个镜像推上去会更快。ES 我没放进这份文件,它单独占 1 GB 内存,需要的人再单独 up

参考