简介:面向部署数据库迁移工具的开发与运维人员,这份容器镜像集合将迁移引擎、代理服务、监控告警、可视化界面等组件打包为可一键编排的部署方案。包内共十一个文件,包含十个gz格式的容器镜像包和一个YAML编排文件,整体大小约七百三十八兆。镜像覆盖数据库、消息代理、监控、可视化等核心服务,配合编排文件执行单条命令即可批量启动所有依赖组件,省去手工配置网络、卷挂载与环境变量的繁琐步骤。对于需要快速搭建数据库同步或迁移环境的技术团队,该资源能显著缩短环境准备时间,并借助监控与可视化组件获得清晰的运行状态视图。目前已有四十八人学习下载,适合具备基础容器使用经验、希望直接复用现成编排方案的读者。
1. 为什么说 DBmotion 的全量迁移,先从一份容器集合开始
做数据迁移的人应该都有这种体会:真正耗时间的往往不是迁移任务本身,而是把源端、采集端、传输通道、目标端这一整套依赖环境凑齐。DBmotion 这类迁移工具虽然把同步逻辑封装得很好,但它的全量迁移链路里,ZooKeeper、Kafka、etcd、采集与写入服务一个都少不了。手动一个个装、一个个调,半天过去了还没开始迁移数据。
所谓“DBmotion 全量所需要容器集合包含可执行的 docker-compose.yaml”,就是把这条链路里的每个环节都做成容器,用一份 docker-compose 编排文件一次性拉起来。适合两种人:一是想快速评估 DBmotion 迁移效果、不想在环境搭建上花太多时间的开发者;二是要给客户或内部环境交付迁移方案,需要一套可复现、可移植的部署模板。这篇文章按我实际搭建这套环境的顺序,把容器集合怎么设计、compose 文件怎么写、参数怎么调、哪些坑必踩,一次讲清楚。
2. 拆开 DBmotion 的容器集合:每个容器解决迁移链路里的哪个环节
2.1 一条全量迁移链路里,哪些组件是不能省的
DBmotion 做全量迁移,简单说就是“读源库 → 传输 → 写目标库”。但中间为了保证数据不丢、顺序不乱、断点能续,引入了一组支撑组件。我最早做模拟项目X的时候就想着精简,只起源端和目标端两个容器,结果根本跑不通。后来把链路理清楚才发现,下面这些角色各有各的活,谁也替不了谁。
源端数据库容器承担“数据从哪来”的角色。全量迁移要验证数据一致性,没有真实的源库实例,后面所有环节都是空转。我一般用 MySQL 或 PostgreSQL 的官方镜像,直接挂初始化脚本造数据。
采集端容器是 DBmotion 里干累活的。它连上源库,把全量数据按表读出来,转成统一的消息格式,发到 Kafka。这个角色通常对应 DBmotion 的采集服务,如果只是做功能验证,可以用轻量模拟程序代替,但正式评估时必须用真实的采集节点。
Kafka 是核心传输通道。全量数据量大,采集端不可能直接往目标库灌,那样源库和目标库会互相拖垮。Kafka 在这里相当于一个高速缓冲带,采集端只管往里面写,写入端只管从里面读,两者解耦。Kafka 本身依赖 ZooKeeper 做元数据管理,所以 ZooKeeper 容器成了隐藏的必需项。
etcd 管的是迁移任务的元数据。哪些表迁移完了、迁移到哪个偏移量了、任务当前什么状态,这些都存在 etcd 里。没有它,断点续传就是空话。最后是写入端容器,它消费 Kafka 里的数据,把全量数据按目标库的方言写入,同时往 etcd 上报进度。
这么一拆就很清楚了:源端、采集端、Kafka、ZooKeeper、etcd、写入端、目标端,七个角色缺一个,链路就断。我见过有人想把 ZooKeeper 省掉直接连 Kafka,只能说那是给自己挖坑。
2.2 为什么用 ZooKeeper + Kafka + etcd 三件套,而不是单机版
很多人第一次看到这套组合会问:都是存元数据,为什么 Kafka 要配 ZooKeeper,DBmotion 自己又要一个 etcd?这俩不能合并吗?
答案是职责不同。Kafka 用 ZooKeeper 存的是 broker 列表、分区分配、消费者组偏移量这些“消息系统自己的状态”。Kafka 的历史版本把 ZooKeeper 当成硬依赖,虽然新版本在做 KRaft 模式去 ZooKeeper,但 DBmotion 官方链路默认还是走 ZooKeeper 的方式,实盘验证时扯皮最少。我自己试过 KRaft 模式,不是说不行,而是排查问题时社区资料少,一旦出状况很难判断是 DBmotion 的问题还是 Kafka 自身的问题。
etcd 管的是迁移任务级别的元数据,比如哪个表迁移到哪条记录了。这不是 Kafka 的分区偏移量,是业务数据在迁移管道里的进度。如果硬塞进 Kafka 的 topic 里,任务一旦重建,历史偏移量会污染新任务。
所以这套“三件套”不是重复造轮子,而是各自守住各自的边界。ZooKeeper 守消息系统,etcd 守迁移任务。明白了这一点,编排容器时就能理解为什么每个服务都要独立部署,而不是图省事塞进同一个容器里。进程混在一起,日志、重启、扩容全都互相干扰,全量迁移跑到一半某个组件挂了,排查起来想死的心都有。
2.3 容器集合的拓扑与端口规划
我把整套容器集合的拓扑固定成了一张表,每次搭新环境直接照着抄。拓扑上分为三层:数据层、传输层、任务层。数据层是源端和目标端数据库;传输层是 Kafka 加 ZooKeeper;任务层是采集端、写入端和 etcd。DBmotion 本身的管理端(如果有)放在任务层最上面,负责下发任务和查看状态。
| 服务 | 镜像类型 | 默认端口 | 数据持久化 | 职责 |
|---|---|---|---|---|
| source-db | MySQL/PostgreSQL 官方镜像 | 3306/5432 | 是(初始化脚本挂载) | 全量数据来源 |
| target-db | 目标数据库官方镜像 | 3306/5432 | 是 | 全量数据写入目标 |
| zookeeper | ZooKeeper 官方镜像 | 2181 | 是(dataDir 挂载) | Kafka 元数据存储 |
| kafka | Kafka 官方镜像 | 9092 | 是(log.dirs 挂载) | 全量数据传输通道 |
| etcd | etcd 官方镜像 | 2379, 2380 | 是(data 目录挂载) | 迁移任务进度/状态 |
| dbmotion-collector | DBmotion 采集端镜像 | 无外部端口 | 否 | 读源库,写 Kafka |
| dbmotion-writer | DBmotion 写入端镜像 | 无外部端口 | 否 | 读 Kafka,写目标库 |
端口规划有个原则:只有需要被外部访问的服务才映射到宿主机,服务间通信用 Docker 内部网络。Kafka 比较特殊,因为客户端(采集端、写入端)跑在别的容器里,Kafka 自己的 advertised.listeners 必须配成容器网络内的地址,否则会踩“容器里连不上 Kafka”的经典大坑。这一点后面专门讲。etcd 的 2380 是集群内部 peer 通信用的,单节点部署不需要映射到宿主机,2379 看需要再决定要不要暴露。
3. 写一份可执行的 docker-compose.yaml:部署模型与关键参数
3.1 先从 compose 文件结构入手:网络、卷、服务三段式
一份可执行的 compose 文件,结构上分三块:networks 定义容器间通信网络,volumes 定义持久化存储,services 定义每个容器。全量迁移的场景里,容器之间都是“服务发现”的关系——采集端通过 etcd 找任务,通过 Kafka 发数据,写入端从 Kafka 拉数据。所以网络必须用自定义 bridge 网络,而不是默认的 default 网络,这样才能用服务名互相访问。
下面是我整理出来的精简版文件框架。注意 source-db 和 target-db 的初始化脚本,我用的是挂载目录而不是镜像内嵌,这样换表结构、换数据都不用重新 build 镜像。
version: "3.8" networks: dbmotion-net: driver: bridge ipam: config: - subnet: 172.28.0.0/16 volumes: zk-data: kafka-data: etcd-data: source-db-data: target-db-data: services: zookeeper: image: zookeeper:3.8 container_name: dbmotion-zookeeper networks: dbmotion-net: ipv4_address: 172.28.0.10 ports: - "2181:2181" volumes: - zk-data:/data - ./zk-conf:/conf environment: ZOO_MY_ID: 1 ZOO_SERVERS: server.1=zookeeper:2888:3888;2181 healthcheck: test: ["CMD", "zkServer.sh", "status"] interval: 10s timeout: 5s retries: 5network 指定了固定子网和固定 IP。这个做法在 compose 里不是必须的,但配合 healthcheck 和 depends_on 的 condition 时,服务名解析稳定,排查网络问题也少一层干扰。volumes 全部用 named volume,而不是 bind mount 到宿主机目录,原因是迁移环境经常要在不同机器之间搬,named volume 随 compose 项目走,不会被宿主机路径差异绊倒。
zookeeper 这里指定了 ZOO_MY_ID 和 ZOO_SERVERS。单节点时 ZOO_MY_ID 写 1 就行,ZOO_SERVERS 的格式是 server.1=主机名:2888:3888;2181,2888 是 Leader 选举端口,3888 是集群内通信端口。单节点不涉及选举,但配全了以后想扩成三节点集群,只需再追加 server.2 和 server.3,不用改其他配置。healthcheck 用的是 zkServer.sh status,这条命令在容器里返回 0 表示服务健康。
3.2 中间件服务的参数陷阱:Kafka 的 advertised.listeners 与健康检查
Kafka 是这套容器集合里最容易翻车的服务,没有之一。官方镜像默认的 listener 配置只对宿主机访问友好,容器内其他服务通过服务名访问时,Kafka 返回的 broker 地址是容器 ID,客户端拿着这个地址去连,必然超时。这就是业界说的“Kafka 容器网络黑匣子”问题。
解决办法是在 environment 里显式指定三组 listener:PLAINTEXT 负责容器内通信,EXTERNAL 负责宿主机访问,CONTROLLER 是给新版本 Kafka 用的。advertised.listeners 必须写成服务名 kafka:9092,而不是 localhost 或宿主机 IP。如果写 127.0.0.1,采集容器里的客户端能连上 broker,但 broker 返回的元数据里写的地址还是 127.0.0.1,客户端拿这个地址根本路由不到 Kafka 容器。
kafka: image: bitnami/kafka:3.4 container_name: dbmotion-kafka networks: dbmotion-net: ipv4_address: 172.28.0.11 ports: - "9092:9092" volumes: - kafka-data:/bitnami/kafka environment: KAFKA_CFG_NODE_ID: 1 KAFKA_CFG_PROCESS_ROLES: broker,controller KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093 KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092 KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: "true" KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR: 1 ALLOW_PLAINTEXT_LISTENER: "yes" KAFKA_HEAP_OPTS: "-Xmx512m -Xms256m" healthcheck: test: ["CMD", "kafka-topics.sh", "--bootstrap-server", "localhost:9092", "--list"] interval: 15s timeout: 10s retries: 5 start_period: 30s这里用的是 bitnami 镜像,环境变量前缀 KAFKA_CFG_ 是 bitnami 的封装规范,和 Apache 官方镜像的 KAFKA_ 前缀不一样。选 bitnami 的原因是其健康检查脚本更完整,kafka-topics.sh --list 能真实反映 broker 是否对外可用,而 Apache 镜像默认的 healthcheck 经常只是检查进程在不在。
KAFKA_CFG_PROCESS_ROLES 写成 broker,controller 是让单节点同时承担 broker 和 controller 职责。全量迁移的最小环境不需要独立的 KRaft controller 节点。副本因子全部设成 1,因为单节点 Kafka 上不允许配大于 1 的副本因子,这个参数不影响迁移正确性,但能避免一堆 “Replication factor is larger than available brokers” 的报错刷屏。KAFKA_HEAP_OPTS 限制 JVM 内存,后面避坑章节专门讲内存问题。
etcd 的配置相对清爽,主要注意 client 端口和 peer 端口的区分。
etcd: image: quay.io/coreos/etcd:v3.5.5 container_name: dbmotion-etcd networks: dbmotion-net: ipv4_address: 172.28.0.12 ports: - "2379:2379" volumes: - etcd-data:/etcd-data command: - /usr/local/bin/etcd - --name=etcd0 - --data-dir=/etcd-data - --advertise-client-urls=http://etcd:2379 - --listen-client-urls=http://0.0.0.0:2379 - --listen-peer-urls=http://0.0.0.0:2380 - --initial-advertise-peer-urls=http://etcd:2380 - --initial-cluster=etcd0=http://etcd:2380 - --initial-cluster-state=new healthcheck: test: ["CMD", "etcdctl", "endpoint", "health"] interval: 10s timeout: 5s retries: 5advertise-client-urls 写容器服务名 etcd 而不是 IP,这样 DBmotion 写入端的容器不管调度到哪个网络位置,都能通过服务名稳定访问。initial-cluster-state 必须写 new,如果写 existing,etcd 会去找旧集群状态,单节点场景下会一直卡在等待集群就绪。
3.3 DBmotion 相关服务的启动顺序与 depends_on 的正确用法
容器编排里启动顺序是个老生常谈但永远有人踩坑的问题。DBmotion 采集端启动时必须能连上 etcd 和 Kafka,写入端必须能连上 Kafka 和 etcd,而 Kafka 又要等 ZooKeeper 就绪。这些依赖如果用默认的 depends_on,docker compose 只保证“先启动”不保证“真就绪”,采集端大概率在 Kafka 还在初始化时就尝试连接,然后崩溃退出。容器退出后 compose 不会自动重启,整套环境就瘫在那里。
正确做法是 depends_on 配 condition: service_healthy,再配合每个中间件的 healthcheck。compose 会先启动依赖服务,等 healthcheck 通过后才启动下游服务。
dbmotion-collector: image: dbmotion-collector:latest container_name: dbmotion-collector networks: dbmotion-net: ipv4_address: 172.28.0.20 depends_on: kafka: condition: service_healthy etcd: condition: service_healthy environment: DBMOTION_ROLE: collector DBMOTION_ETCD_ENDPOINTS: http://etcd:2379 DBMOTION_KAFKA_BOOTSTRAP: kafka:9092 DBMOTION_SOURCE_DSN: root:sourcepass@tcp(source-db:3306)/migration_db?charset=utf8mb4 command: ["/app/dbmotion-collector", "--config", "/etc/dbmotion/collector.yaml"] dbmotion-writer: image: dbmotion-writer:latest container_name: dbmotion-writer networks: dbmotion-net: ipv4_address: 172.28.0.21 depends_on: kafka: condition: service_healthy etcd: condition: service_healthy environment: DBMOTION_ROLE: writer DBMOTION_ETCD_ENDPOINTS: http://etcd:2379 DBMOTION_KAFKA_BOOTSTRAP: kafka:9092 DBMOTION_TARGET_DSN: root:targetpass@tcp(target-db:3306)/migration_target?charset=utf8mb4 command: ["/app/dbmotion-writer", "--config", "/etc/dbmotion/writer.yaml"]DBMOTION_SOURCE_DSN 和 DBMOTION_TARGET_DSN 里的主机名全部写服务名,tcp(source-db:3306) 这种格式是 Go 语言数据库驱动的标准写法。这里有个容易忽略的点:source-db 和 target-db 这两个容器本身也要配 healthcheck,否则 depends_on 里如果加了它们,condition 会检查不到健康状态而一直等待。MySQL 镜像的 healthcheck 建议用 mysqladmin ping,PostgreSQL 用 pg_isready。
启动顺序变长了以后,整套环境冷启动可能要等 1 到 2 分钟,这不是卡死,是中间件在做初始化。后面加了一个 start_period 参数专门解决“启动慢被 healthcheck 误杀”的问题。采集端和写入端的镜像这里用 dbmotion-collector:latest 指代,实际使用时换成你自己的镜像名。
4. 把容器集合跑起来:从 docker compose up 到第一个迁移任务
4.1 启动前检查:端口冲突、内存预算、镜像源
启动之前有三次检查,能帮你省掉后面至少两个小时的排查时间。第一次检查端口冲突。2181、9092、2379 这三个端口是中间件默认端口,本机如果已经跑着别的 ZooKeeper、Kafka 或 etcd,compose 起服务时会直接报 bind: address already in use。检查命令很简单:
lsof -i :2181 -i :9092 -i :2379有输出说明端口被占了。解决方式有两个:关掉宿主机的旧服务,或者在 compose 文件里改 ports 映射的宿主机侧端口,比如把 Kafka 改成 "19092:9092"。要注意,改宿主机侧端口不影响容器间通过服务名访问,因为容器间走的是 9092 这个容器内端口。
第二次检查内存预算。Kafka 默认的 JVM 堆上限是 1G,加上 ZooKeeper、etcd、两个数据库实例,整套环境冷启动大概需要 3 到 4G 可用内存。如果机器只有 2G,Kafka 会频繁 Full GC,然后被 healthcheck 判死,compose 就会反复重启它,日志里全是内存溢出。建议在 compose 里给 Kafka 显式限制 KAFKA_HEAP_OPTS,给 MySQL/PostgreSQL 用 shm_size 控制共享内存。
第三次检查镜像源。DBmotion 相关镜像如果是私有仓库,docker compose 不会自动登录,需要先手动 docker login。ZooKeeper、Kafka、etcd 这几个中间件镜像建议提前 docker pull 到本地,免得 compose up 时才开始拉,一旦网络不稳定,整个环境起得断断续续。
4.2 启动整套环境与验证每个容器状态
检查做完就可以启动了。我习惯用后台模式启动,然后把日志输出到文件,方便后面排查。
docker compose up -d这个命令会按依赖顺序依次启动所有容器。等第一次执行完,别急着配迁移任务,先花 30 秒确认每个容器都进入了 healthy 状态:
docker compose ps输出里每个服务对应一行,STATUS 列应该有 healthy 字样。这一步非常重要,因为采集端和写入端的启动依赖于 Kafka 和 etcd 健康,如果中间件还处于 starting,下游容器会卡在等待状态。看到 unhealthy 的容器,立即看它的日志:
docker compose logs -f kafka docker compose logs -f etcd日志出现的常见关键字也有套路。ZooKeeper 日志里出现 bind to port 2181 说明监听成功;Kafka 日志里出现 [KafkaRaftServer nodeId=1] Started 说明 KRaft 模式启动完成;etcd 日志出现 serving insecure client requests on 0.0.0.0:2379 说明客户端接口可用。如果等了两分钟这些关键字还没出现,基本可以断定配置有问题而不是启动慢。
所有容器都 healthy 之后,再验证一下容器间的网络连通性。我一般会进入采集端容器里,用内置工具测试到 Kafka 和 etcd 的连通性:
docker exec -it dbmotion-collector bash # 在容器内执行 curl http://etcd:2379/health # 期望输出 {"health":"true"} kafka-topics.sh --bootstrap-server kafka:9092 --list # 期望输出不报错,哪怕没有 topic这一步验证的是服务名解析和端口连通。curl 输出 health true 说明 etcd 正常,kafka-topics.sh 能连上说明 Kafka 的 advertised.listeners 配置生效了。如果这两步有一处失败,不要继续往下配任务,先回头改配置。
4.3 跑通一个最小全量迁移任务
环境就绪后,迁移任务本身怎么下发?DBmotion 的做法是通过 etcd 写入任务配置,采集端和写入端监听 etcd 的变化然后开始干活。所以我需要往 etcd 里写入一个最小的迁移任务配置。任务配置的核心内容包括:源库连接信息、目标库连接信息、要迁移的表列表、Kafka topic 名称。
docker exec -it dbmotion-etcd etcdctl put /dbmotion/tasks/task001 '{ "name": "full_migration_demo", "source": "root:sourcepass@tcp(source-db:3306)/migration_db", "target": "root:targetpass@tcp(target-db:3306)/migration_target", "tables": ["users", "orders"], "kafka_topic": "dbmotion-full-001", "mode": "full", "batch_size": 1000 }'这段配置我一般在源库和目标库里都先建好同名表,再填到 etcd。batch_size 控制每个批次写入的记录数,设 1000 是保守值,主要考虑目标库写入性能和 Kafka 消息大小。模式写成 full,表示只做全量迁移,不做增量同步。字段名和结构按实际 DBmotion 版本可能略有差异,但核心信息就是这四类:两端连接、表清单、通道、批大小。
任务写进去之后,采集端监听到配置变化,会去源库读数据,然后发到 Kafka;写入端从 Kafka 拉数据,写入目标库。整个过程中可以通过 etcd 查看任务进度:
docker exec -it dbmotion-etcd etcdctl get /dbmotion/tasks/task001/status看到 status 字段变成 finished 或 done,说明全量迁移跑完了。这时进目标库查一下数据量:
docker exec -it dbmotion-target-db mysql -uroot -ptargetpass -e "SELECT COUNT(*) FROM migration_target.users;"如果和目标库源表行数一致,第一个全量迁移任务就算通了。第一次跑通的意义不在于迁移了多大数据量,而在于验证了从容器集合到任务下发的整条链路是通的,后续换真实库、加表、调并发都在这条链路上迭代。
5. 容器集合落地避坑:五个最容易翻车的点
5.1 Kafka 容器起来了,客户端却连不上
现象:docker compose ps 显示 Kafka 是 healthy,但采集端容器日志里反复报 connection refused 或者 timeout,Kafka 的 topic 列表也拉取失败。
原因:绝大多数情况是 advertised.listeners 配置不对。Kafka 容器对外广播的地址写成了 localhost 或 127.0.0.1,容器内的采集端拿到这个地址后尝试连自己,自然连不上。我见过有人排查了一下午,最后发现是 KAFKA_CFG_ADVERTISED_LISTENERS 漏了 KAFKA_CFG_ 前缀,导致配置根本没生效。 解决:把 advertised.listeners 显式写成 PLAINTEXT://kafka:9092,注意主机名必须和 compose 服务名一致。改完之后重启 Kafka 容器,然后进入采集端容器里执行 kafka-topics.sh --bootstrap-server kafka:9092 --list 验证。改完配置不重启、只看 docker inspect 里的 Environment 字段,是不会生效的。
5.2 depends_on 直接写服务名,下游容器还是崩溃退出
现象:dbmotion-collector 容器启动后立即退出,日志显示连接 etcd 超时或 Kafka 未就绪,但 Kafka 容器明明是 running。
原因:depends_on 只保证服务启动先后顺序,不保证服务内部就绪。Kafka 容器进程起来了,但 broker 还在加载磁盘里的日志、注册 controller,这个窗口可能持续几十秒。采集端这个时候去连,必然失败。容器退出之后 compose 不会自动拉起它,所以整个环境就静默地“半死”了。 解决:depends_on 里加上 condition: service_healthy,然后给 Kafka 和 etcd 都配上真正的 healthcheck。注意 Kafka 的 healthcheck 不要用 telnet 端口,要用 kafka-topics.sh --list,因为端口通了不代表 broker 完成注册。这是一个很典型的“玄学问题”,其实不是玄学,是就绪语义没搞清。
5.3 etcd 容器启动失败,名叫“permission denied”
现象:docker compose up 时 etcd 容器反复重启,日志里出现 mkdir /etcd-data: permission denied。
原因:etcd 官方镜像里的进程默认以非 root 用户运行,而挂载进来的 named volume 初始所有权是 root。容器用户没有写权限,etcd 建不了数据目录。这个坑在大部分 Linux 发行版上都会踩,macOS 上因为 Docker Desktop 的特殊权限模型可能不触发,反而增加了迷惑性。 解决:最简单的方式是在 compose 的 etcd 服务里加 user: "0:0" 强制以 root 运行,或者用初始化容器预先 chown 数据目录。生产环境建议用初始化容器,本地评估可以直接加 user。改完记得 docker compose down -v 把旧的卷删掉重建,否则权限修复不会套用到已创建的卷上。
5.4 整套环境跑着跑着,某个容器被系统杀掉
现象:迁移任务跑到一半,Kafka 或两个数据库容器突然消失,docker compose ps 里看不到,系统日志里出现 oom-kill 记录。
原因:宿主机内存被占满。Kafka 默认的 JVM 堆内存上限是 1G,加上 ZooKeeper、etcd、两个数据库实例,再算上 Docker 本身的开销,2G 内存的机器根本扛不住。系统 OOM killer 会优先杀掉内存占用最大的进程,Kafka 首当其冲。 解决:在 compose 里给每个服务加 mem_limit,Kafka 设 512m,ZooKeeper 设 256m,etcd 设 256m,数据库各自设 512m。同时给 Kafka 设置 KAFKA_HEAP_OPTS 为 -Xmx512m -Xms256m,让 JVM 在容器内存限制之内运行。压内存之前,先用 docker stats 看实时占用,确定是谁在吃内存。
5.5 数据“丢了”:重启后源库或目标库是空的
现象:整套环境重启之后,迁移任务的任务还在,但目标库里的数据没了,有时候源库的初始化数据也没了。
原因:数据库容器虽然挂了 volumes,但挂的是匿名卷或没挂卷。compose down 的时候如果没加 -v,匿名卷会保留,但 docker compose down -v 会把匿名卷一起删掉。更隐蔽的场景是:镜像自带的数据目录路径和你在 volumes 里挂载的容器内路径不一致,比如 MySQL 镜像的数据目录是 /var/lib/mysql,你挂成了 /var/lib/mysql/data,卷里存了文件但 MySQL 根本不认识,启动时生成一个空的数据目录,看起来就是“数据丢了”。 解决:先确认镜像文档里的数据目录路径。MySQL 官方镜像是 /var/lib/mysql,PostgreSQL 是 /var/lib/postgresql/data,etcd 的路径通过 --data-dir 参数自己指定。compose 里 volumes 的容器侧路径必须和实际数据目录完全一致。养成一个好习惯:改动环境之前先 docker compose down,不要直接 docker compose down -v,-v 是删除数据卷的操作,没有后悔药。
6. 让这套容器集合更顺手:参数化模板与多环境复用
容器集合能跑通只是第一步,第二步是让它能复用。我最常用的是 .env 配合 compose 变量替换。把端口、密码、数据目录这些环境相关的值抽到 .env 文件里,compose 文件本身保持一份不动。
# .env 示例 SOURCE_DB_PORT=3306 TARGET_DB_PORT=3307 MYSQL_ROOT_PASSWORD=sourcepass TARGET_MYSQL_ROOT_PASSWORD=targetpass KAFKA_PORT=9092compose 文件里用 ${MYSQL_ROOT_PASSWORD} 引用这些变量。这样同一份 compose 文件,开发环境、测试环境、客户现场,只要改 .env 就能迁移,不用动 YAML 本身。变量替换是 compose 原生的能力,不需要额外工具。
数据校验是另一个值得养成的习惯。全量迁移结束,只对比行数远远不够。行数一样但内容对不上,比如字符集编码问题、时区转换问题、浮点数精度问题,都会让迁移“成功但错误”。我一般用一个简单的校验脚本,把源表和目标表按主键排序后做哈希对比:
docker exec source-db mysql -e "SELECT CRC32(GROUP_CONCAT(col1, col2, col3 ORDER BY id)) FROM users;" docker exec target-db mysql -e "SELECT CRC32(GROUP_CONCAT(col1, col2, col3 ORDER BY id)) FROM users;"两边的 CRC32 值一致,说明这批数据内容层面也对上了。不一致时,把排序后的主键列表导出做 diff,就能定位到具体哪一行差异。这个校验方法简单直接,不用引入额外的工具链。
这套容器集合做到现在这个程度,我最大的教训是:不要因为追求“环境小”而阉割依赖组件。最早我为了省内存,把 ZooKeeper 换成嵌入式模式,把 etcd 塞进采集端进程里,结果每次排查问题都要先怀疑自己改过的地方,反而浪费更多时间。按标准链路把容器集合完整拉起来,虽然占资源多一点,但每一步行为和线上行为一致,验证出来的结论才作数。希望这篇笔记能帮你把环境搭建这一步真正省下来,把精力留给迁移任务本身。
本文还有配套的精品资源,点击获取