前段时间接了个活儿:要在测试环境快速验证一套分库分表合并同步方案,数据要从几个业务库实时汇聚到一个分析库。选型阶段没纠结,直接用了阿里的开源数据同步中间件 otter——它基于 MySQL binlog 增量捕获,manager 负责配置调度、node 负责干活,生态比较成熟。真正让我头疼的是环境搭建,官方文档默认你要自己准备 MySQL、Zookeeper、manager、node 四类进程,手工来一遍至少小半天,而且换台机器就全部重来。
后来我花了一天时间把这套环境全部容器化,用 docker-compose 编排好,配上一个初始化脚本,在任何一台装了 Docker 的机器上执行一条命令,几分钟就能得到一个可用的 otter 测试环境,自带源库、目标库、Web 控制台,连 manager 的元数据库都自动建好。这篇就把这套“alibaba otter docker 测试环境一键生成”方案完整记录下来,包括架构拆解、compose 编排思路、关键配置、初始化动作和搭建过程中踩过的坑。如果你正准备搭 otter 做数据同步验证、做灾备演练,或者单纯想学习它的内部运作机制,这份记录应该能帮你省下不少时间。
1. 先摸清otter测试环境里有哪些角色
1.1 otter的数据同步模型:manager和node各管什么
otter 的核心思想是“基于 binlog 的增量同步”。业务在源库产生的每次 insert、update、delete,都会变成 binlog 事件,otter 的 node 通过内嵌的 canal client 伪装成 MySQL 从库去拉取这些事件,解析成结构化数据,再按照你在控制台里配置的管道同步到目标库。所以源库必须开启 binlog,而且binlog_format必须设为row,否则拿到的不是字段级变化,没法准确同步。
一个最简可用的 otter 环境,至少包含这几类角色:
- Zookeeper:负责集群协调和元数据存储,manager 和 node 启动后都会注册到这里。
- manager:提供 Web 控制台,维护数据源、同步任务(Channel/Pipeline)、表映射关系,并把任务下发给 node。
- node:真正干活的工作节点,内嵌 canal client,执行数据抽取和写入。
- manager 元数据库:存放 manager 自身的配置数据,比如任务定义、机器信息、映射规则,用 MySQL 承载。
- 源库和目标库:业务层面的 MySQL 实例,一个负责产生 binlog,一个负责接收同步结果。
理清这几个角色之后,你会发现用 Docker 来承载它们非常自然:它们就是一组可以独立启动、通过配置互相指向对方的进程。容器化之后,反而比裸机部署更干净,每个组件都只有一个职责,日志、配置、数据卷都分开。
1.2 为什么测试环境不选手工安装而选Docker
手工搭 otter 测试环境是最不划算的做法。组件太多,要分别装 JDK、MySQL、Zookeeper 和 otter 发布包;版本之间还互相纠缠,otter 不同版本对 canal、Zookeeper 的兼容要求不一样;最后是隔离问题,在一台机器上配好了,换台机器或者顺手清理下目录,整套环境就废了。
容器化或者说“基础设施即代码”的价值,在于把环境当成可复现的资产。我有两次重建这套环境的需求,一次是为了验证同步性能,一次是给新同事演示完整流程。两次都是找一台干净机器跑同一个启动脚本,环境一致性没有任何偏差。而且容器可以随时销毁重建,数据卷独立存放,搞坏了就 reset,不需要担心“删了会不会影响别的东西”。
所以这套方案的目标很明确:任何一台装了 Docker 的机器,拉下项目目录,执行一键脚本,得到一套带 Web 控制台、自带源库和目标库的完整 otter 环境。后续做任何破坏性验证都不可惜,重建成本几乎为零。
2. 一键生成前的准备:镜像、目录与启动脚本
2.1 基础镜像版本怎么选更少踩坑
镜像版本这事不用纠结,但也不能乱配。我的做法是参考 otter 官方 release 对应的依赖版本,然后固定镜像 tag,避免每次拉取到不一致的版本。
具体到这次测试环境,我用了 MySQL 5.7 同时充当源库、目标库和 manager 元数据库。说句实话,5.7 是最省心的选择:otter 的 canal 插件对 5.7 的 binlog 格式支持最稳,公开案例也最多。如果你坚持 MySQL 8.0,也不是不行,但 8.0 在认证插件、binlog 权限和事务机制上都有变化,需要额外调,不建议第一次就跑 8.0。
Zookeeper 直接用 3.5 的镜像,稳得很。manager 和 node 启动时需要连接 ZK 做注册与协调,端口就是经典的 2181。
otter 自身的 manager 和 node 镜像,你可以从镜像仓库拉现成的 tag,也可以用官方构建目录本地 build。判断标准很简单:镜像里得有 JDK 1.8 以上、otter 发布包解压后的完整目录、以及对外暴露可覆盖的配置入口。测试环境不需要追求镜像精简,稳定可复现才是第一位的。
2.2 项目目录与初始化资产要提前备好
为了让执行命令的人无脑操作,项目目录我建议这样组织:
otter-docker-demo/ ├── docker-compose.yml ├── .env.example ├── script/ │ ├── init-manager.sql │ └── docker-entrypoint.d/ │ └── 01_create_otter_db.sh ├── conf/ │ ├── manager/ │ │ └── system.properties │ ├── node/ │ │ └── node.properties │ └── otter-node/ │ └── nid ├── data/ │ ├── mysql-source/ │ ├── mysql-target/ │ └── mysql-manager/ └── README.mdmanager 的初始化 SQL 要从 otter 发布包里拿,不要自己写。它会在 manager 元数据库中建好全部表结构和初始数据,是 manager 启动的前提。版本不对会导致 manager 起不来或者页面各种报错,这是我们第 5 节要说的第一个坑。
docker-entrypoint.d/01_create_otter_db.sh这个脚本是配合 MySQL 官方镜像的初始化机制用的。MySQL 容器首次启动、数据卷为空时,会自动执行该目录下的.sql或.sh文件。脚本内容很简单:
#!/bin/bash mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "CREATE DATABASE IF NOT EXISTS otter DEFAULT CHARACTER SET utf8mb4;"这样 manager 的元数据库就不需要人工介入,容器一启动就自动建好。
2.3 一键启动脚本的设计思路
一键生成不是真的只有一个命令,而是要把重复劳动都收敛到一个入口脚本里。我写了一个init.sh,按顺序做四件事:
- 检查本地 Docker 和 docker compose 插件是否存在,缺了就中断并提示安装。
- 解析
.env,如果没有就基于模板生成随机密码,避免把密码写死在仓库里。 - 执行
docker compose up -d拉起全部容器。 - 轮询 manager 的 HTTP 接口,确认 Web 页面可访问后,输出访问地址、默认账号密码和后续需要在控制台里手动操作的提示。
脚本里最容易被忽略的是等待逻辑。docker compose up -d返回得很快,但 manager 和 node 内部启动可能需要几十秒。不等待就直接告诉用户“环境已就绪”,用户打开页面什么都看不到,体验会很糟。我这里用循环探测 manager 端口 8080 的 TCP 连通性,超时给到 120 秒,连续失败再提示用户去看容器日志。
3. 逐项拆解docker-compose与关键配置
3.1 compose服务编排:6个容器各司其职
这个 compose 文件是整个方案的骨架。我按职责拆成了六个服务:zk、managerdb、manager、node、source-mysql、target-mysql。下面是一份简化但可用的写法,关键点都用注释标出来了:
version: '3.8' services: zk: image: zookeeper:3.5 networks: [otter-net] managerdb: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: ${MYSQL_MANAGER_PASSWORD} volumes: - ./data/mysql-manager:/var/lib/mysql - ./script/docker-entrypoint.d:/docker-entrypoint-initdb.d:ro networks: [otter-net] manager: image: otter-manager:4.2.x depends_on: - zk - managerdb environment: OTTER_MANAGER_PORT: 8080 OTTER_ADMIN_PORT: 1099 ZK_SERVERS: zk:2181 volumes: - ./conf/manager/system.properties:/otter/conf/system.properties ports: - "8080:8080" - "1099:1099" networks: [otter-net] node: image: otter-node:4.2.x depends_on: - manager - zk volumes: - ./conf/otter-node/nid:/otter/nid - ./conf/node/node.properties:/otter/conf/node.properties networks: [otter-net] source-mysql: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: ${MYSQL_SOURCE_PASSWORD} command: --server-id=100 --log-bin=mysql-bin --binlog-format=row --binlog-row-image=full volumes: - ./data/mysql-source:/var/lib/mysql networks: [otter-net] target-mysql: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: ${MYSQL_TARGET_PASSWORD} networks: [otter-net] networks: otter-net:这里有几个点值得展开说。source-mysql的command里,server-id=100必须显式指定,而且不能和后面 node 侧 canal 伪装实例的 server-id 相同,否则 MySQL 主从复制协议会认为是同一个实例,连接直接断开。binlog-format=row是 otter 正确解析数据的必要条件,statement 或 mixed 模式下拿不到字段级变化,同步出来的数据必然对不上。binlog-row-image=full让 binlog 记录完整的前后镜像,otter 的某些 update 操作需要旧值做对比,必须开。
managerdb 挂载docker-entrypoint.d目录,这是把初始化动作变成自动执行的关键。注意这个过程只在数据卷为空时触发,如果之前初始化过,后面再怎么改脚本也不会执行。需要重建时,把data/mysql-manager目录一起清掉就行。
3.2 manager配置项的核心要点
manager 镜像启动后会读取挂载进去的system.properties。这里面最重要的是两个信息:manager 元数据库的连接,以及 Zookeeper 地址。示例配置大概长这样:
otter.manager.database.driver=com.mysql.jdbc.Driver otter.manager.database.url=jdbc:mysql://managerdb:3306/otter?useSSL=false otter.manager.database.username=root otter.manager.database.password=你的密码 otter.manager.zookeeper.cluster.default=zk:2181有三个易错点。第一,url里的 host 不能写localhost,容器内要写 compose 的 service 名managerdb,否则连不上数据库。第二,如果配置里出现特殊字符,比如密码带@或#,配置文件解析很容易出问题,建议生成随机密码时只保留字母数字和短横线。第三,这里还有个隐藏细节:如果system.properties是通过卷挂载进去的静态文件,${}环境变量不会被自动替换。测试环境图省事可以直接把密码明文写进去,如果你想让脚本更通用,就得在启动入口用envsubst渲染一次。我把这个逻辑放到了init.sh里,避免镜像内配置模板和实际环境脱节。
3.3 node注册为什么是配置里最隐晦的一环
node 的配置涉及两个文件:node.properties和nid。前者存放 manager 连接地址和通信参数,后者是节点在 manager 里的唯一标识。示例:
otter.node.manager.url=http://manager:8080 otter.node.manager.nid=1 otter.node.communication.port=2088nid文件里的值要和node.properties中的otter.node.manager.nid保持一致。测试环境我直接把 nid 固定为 1,省得每次重建都在控制台里重新确认节点编号。node 启动后会拿着 nid 向 manager 注册自己,但 manager 的管理员还没有把机器状态置为“启用”,所以首次启动后需要到 Web 控制台的“机器管理”页面,把 node 状态改成“启用”。这一步我目前还没有完全自动化,会在脚本最后的操作提示里打印出来。
想把这步也自动化的话,可以研究 manager 对外开放的 OpenAPI,通过 HTTP 调登录接口和机器管理接口,但登录 token 和接口签名的处理比较繁琐。我实际用下来,在测试环境点一下就完成的事,专门写自动化反而增加维护成本。
4. 从启动到跑通第一条同步数据
4.1 启动后的自检清单
执行一键脚本后,先别急着配置同步任务。对照下面几条检查一遍,任何一条不满足都可能导致后面数据同步一直挂在某个奇怪的状态:
- 源库 binlog 是否开启:执行
show variables like 'log_bin',结果要为 ON。 - 源库
server-id是否和 canal 伪装实例冲突:我上面手动指定为 100,node 侧的 canal serverId 要避开这个值。 - manager 是否完成初始化:浏览器打开
http://ip:8080,能出现登录页而不是白屏。 - node 是否启用:机器管理页面里 node 状态为“启用”。
- ZK 是否正常:manager 日志里不该有一大堆连接 ZK 失败的重试。
这套检查做完,才算进入业务配置阶段。第一次搭的时候,最耗时的恰恰是这些前置条件,很多人一上来就去配管道,结果同步任务一直启动失败,回过来才发现源库 binlog 都没开。
4.2 控制台配置:数据源、Canal与管道映射
otter 里配置同步的常规顺序是:数据源 → Canal → Channel/Pipeline → 表映射 → 启动。测试环境可以一切从简,但思路值得说清楚。
数据源配置:在控制台新增数据源,一个指向source-mysql,一个指向target-mysql。地址可以写 compose 里的 service 名,也可以写宿主机 IP 加映射端口。manager 只负责把配置存进元数据库并下发给 node,真正建连接的是 node,所以数据源配置中填的地址必须是 node 能访问到的。
Canal 配置:源库新增数据源之后,还要配置对应的 Canal 实例。这一步会要求填写源库地址、serverId、binlog 文件或偏移量。测试环境不需要手动指定偏移量,选择“从最新位点同步”就行,启动后会自动定位。
管道与映射:先创建 Channel,再在 Channel 里添加 Pipeline。Pipeline 需要选择机器(刚激活的 node)、源数据源、目标数据源,并选择同步模式。otter 支持单向同步、双向同步、文件同步等,测试常用单向。最后在映射管理中把源表映射到目标表,表名相同、字段一致的话可以自动建立映射,省掉字段级调整的麻烦。
4.3 用一条SQL验证同步是否打通
配置完成后,在source-mysql里执行:
CREATE TABLE IF NOT EXISTS sync_demo (id INT PRIMARY KEY, payload VARCHAR(64)); INSERT INTO sync_demo VALUES (1, 'hello otter');然后到target-mysql里查询:
SELECT * FROM sync_demo;正常情况下,几秒内就能看到这条数据。我建议先建表再插数,是为了把“表结构映射问题”和“增量数据同步问题”分开排查。如果目标表不存在,INSERT 也会同步失败,但你看到的是 SQL 层面的报错,容易误判成同步链路问题。表结构一致但数据没到,才应该往 Canal 和 node 日志方向排查。
还有一点:第一次同步的延迟可能不止几秒,因为 node 要新创建 Canal 连接、完成一次 binlog 位点定位,然后再启动管道。超过 10 秒没看到数据,直接看 node 日志,通常会有明确报错,别在那傻等。
5. 搭建中最容易踩的坑与排查经验
5.1 高频问题速查表
这一节我把搭建过程中真实踩过的坑整理成速查表,从高概率到低概率排序。有些问题看起来很低级,但越低级越容易被忽略,比如“忘记给源 MySQL 开 binlog”,我第一次搭建时就栽过。
| 问题表现 | 常见原因 | 处理方式 |
|---|---|---|
| manager 启动后一直连不上数据库 | system.properties 中 host 写成 localhost,或容器不在同一 network | url 改成 managerdb:3306,检查 compose 网络配置 |
| 页面能开但功能报 JSON 解析错 | manager 版本和初始化 SQL 版本不匹配 | 从同一套发布包内取 SQL,不要混用不同版本 |
| node 一直注册不上,manager 日志提示机器不存在 | nid 文件与 node.properties 的 nid 值不一致 | 统一两个地方的 nid,重建前清空旧 nid 文件 |
| 同步任务停掉,日志提示 Canal 连接被断开 | 源库 server-id 与 canal 的 serverId 冲突 | 修改 source-mysql 的 server-id,重启容器,同时调整 node 侧 canal serverId |
| 同步延迟很大,数据积压严重 | canal 偏移量定位到了很久之前 | 在 Canal 配置里重置到当前位点,或者删除重建 Canal |
| 目标库表结构缺失,任务一直报 SQL 异常 | 只配了映射,没有建目标表 | 先手工建好目标表,或用 otter 的建表能力 |
| 容器重启后源库里新写的数据不同步 | 端口映射、防火墙问题 | 检查对外访问端口是否暴露,容器内用 service 名访问不受影响 |
| node 日志里有大量 Connection refused | 依赖服务还没 ready 就启动了 node | compose 里加 depends_on 条件,脚本里加健康检查等待 |
5.2 代码量之外的细节经验
再补充几条运维层面的经验。每次重建前,养成习惯把data目录和conf目录里的旧文件清掉,尤其nid和 MySQL 数据卷。否则容器虽然换了,数据还是旧的,问题非常隐蔽。
docker compose down不会删除卷,如果你想要真正全新环境,需要手动删掉对应的 named volume 或者data子目录。
还有一点很容易让人迷惑:otter 和 canal 的关系。otter 依赖 canal 抓 binlog,node 内嵌 canal client,并不是让你再去独立部署一套 canal。所以当你看到报错里出现 canal 字样,别急着单独部署,问题大概率出在源库 binlog 配置或 server-id 上。这一点我在文档里翻了好久才彻底明白。
6. 一键脚本后续还能怎么扩展
6.1 团队内推广可以加的扩展点
这套方案继续往下扩展的空间挺大的。如果要在团队内推广,可以加上healthcheck让 compose 严格按依赖顺序启动,manager 和 node 都等依赖就绪再拉起;还可以加上监控采集,在 manager 里开启 JMX 端口,把同步延迟和任务状态做成看板。
对需要持续集成验证的场景,可以在init.sh后面接一个自动化用例阶段,直接对目标库做数据断言,跑完再销毁整套环境。这样每次改动代码后都能自动验证一遍同步链路有没有被破坏。
6.2 更贴近业务的场景化扩展
我目前最常用的扩展,是把source-mysql变成一个预置结构生成器,启动时自动建好几张典型的业务表,比如订单表、用户表、账务流水表,再配好分库分表规则。这样新人在几分钟内就能看到一个有真实感的同步 demo,比 PPT 上讲半天都直观。
最后说点实在的。这套环境我在三台完全不同的机器上跑过,分别是旧笔记本、云上测试机和办公电脑,运行结果完全一致,这也是容器化最让我踏实的一点。但也要承认,一键生成不等于零维护,otter 版本一旦更新,镜像内部目录结构可能调整,新版本对 JDK、ZK 的依赖也可能变化,脚本要跟着升级。所以建议在项目里固定版本号,升级时单独走一次全量验证。如果你也在搭类似的数据同步测试环境,希望这篇能帮你省下半天时间。