ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于日志捕获的实时数据同步实践:MiroFish 从部署到调优

基于日志捕获的实时数据同步实践:MiroFish 从部署到调优 这阵子又把一套数据同步链路翻出来重做起因是业务同事抱怨报表库的数据老是慢半拍凌晨跑批又总跟源库打架。折腾了一圈最后落到一个叫 MiroFish 的开源工具上。名字有点意思Miro 取镜像之意Fish 则是捕获、钓鱼的动作——把源端的每一次变更像钓鱼一样精准地钓出来再原封不动地镜像到目标端。这思路听着简单真要把链路做稳、做透里面的门道不少。这篇就把我这几周从选型、部署到调优的全过程拆开讲重点聊聊 MiroFish 的核心设计、实操步骤以及那些文档里不会写的坑。如果你是做数据同步、数据异构、或者正在被定时任务式同步方案折磨的开发者这篇应该对你有用。就算你暂时用不上了解一套完整的 CDC 数据镜像方案是怎么一步步落地的也不是坏事。1. MiroFish 是什么先搞清楚它解决了什么问题1.1 传统同步方案的三座大山先说说我为什么会盯上 MiroFish。以前的同步方案大概是两条路一条是业务代码里双写主库写一份目标库再写一份另一条是定时任务每天凌晨把变更数据批量拉过去。这两条路在实际运维中都挺难受的。双写的问题在于侵入性太强。业务代码里到处都是同步逻辑一旦某个接口漏写了数据就开始悄悄不一致。等到发现的时候往往已经过去几天对账对到怀疑人生。而且双写逻辑一多事务边界就变得模糊主库提交成功了目标库可能还卡在某个网络超时里两边事务又不能天然联动最后只能靠人工补偿脚本去抹平。定时任务那条路更不用提延迟是硬伤。凌晨跑批意味着白天产生的数据变更要到第二天才能到目标库报表、搜索索引、数仓分层全都跟着延迟一整天。还有更隐蔽的问题跑批时间窗口跟业务高峰期重叠时源库的 IO 被批量查询拉高正常业务查询跟着遭殃。我见过生产环境里凌晨一点的大表全量更新把主库慢查询数拉高一倍DBA 半夜爬起来救火。这三座大山本质上是同一个问题我们没有去监听变化本身而是反反复复在做全量对比和全量搬运。数据量小的时候勉强能忍一旦单表过千万、变更频率上来这套思路就撑不住了。1.2 MiroFish 的解题思路日志即数据流MiroFish 的核心思路跟传统方案完全不同它不碰业务代码也不用定时任务去扫表而是直接盯住数据库的日志文件。主库上每一次 insert、update、delete 都会按顺序写进日志MiroFish 做的就是把这个日志流读出来解析成结构化的变更事件再按需投递到下游。这样做的好处非常明显。首先它对业务代码完全透明不需要改任何一行业务逻辑引入成本低。其次它拿到的是流式的变更记录天然就是实时的秒级甚至毫秒级延迟都能做到。最关键的是它拿到的是日志里真实发生的变更而不是通过对比两张表快照猜出来的差异从源头上避免了漏数据的问题。我最初看到 MiroFish 的第一反应是这不就是又一个 CDC 轮子吗Debezium、Canal、Flink CDC 这些我都用过为什么还要多看一个但真把文档翻完发现它在几个细节上做了文章一是部署形态非常轻量单体二进制没有一堆依赖组件二是对断点续传的处理比较细不是简单记个 offset 就完了三是自带了镜像一致性校验能力能自动对比源端和目标端的数据。这几个点恰好是我在之前方案里要花很多额外精力去补的。1.3 主要应用场景与适用读者从我这段时间的实际使用来看MiroFish 适合的场景大概有这几类异构数据库之间的实时同步比如 MySQL 同步到 ClickHouse、PostgreSQL、Elasticsearch多活或读写分离架构下把变更数据广播给多个下游消费方数据湖和数仓的实时入湖入仓替代小时级的批量抽取任务缓存失效、搜索引擎索引更新这类下游强依赖数据变更的业务联动。如果你属于以下情况MiroFish 会比较对胃口团队不大没有专职的数据平台组但又想快速上一条实时同步链路或者现有的同步方案老是出幺蛾子想找一个更可控、更容易排查问题的替代品。2. 核心设计拆解CDC、镜像链路与断点恢复2.1 一切从日志捕获开始要理解 MiroFish 的工作原理得先明确一个基础概念数据库日志是变更的唯一真相来源。主库上任何一个已提交事务都会留下对应的日志记录里面包含了事务ID、操作类型、涉及的字段新旧值、操作时间等关键信息。MiroFish 做的事情本质上就是一个日志消费者。具体来说它会伪装成一个普通的备库或从库跟源库建立复制协议连接然后持续请求日志数据流。源库把日志一段一段发给它它负责解析、过滤、转换最后推送给下游。整个过程跟 MySQL 主从复制的机制高度一致所以对源库的侵入几乎可以忽略不计。这里有个关键设计值得展开日志解析的可靠性和顺序性。数据库日志本身是有序的事务提交的顺序就是日志产生的顺序。MiroFish 必须严格遵循这个顺序来处理每一条变更否则下游数据就会乱。实际操作中它会为每个数据流维护一个单调递增的位置标记每处理完一条记录就更新一次这样即使中途崩溃重启后也能从上一次确认的位置继续。有人可能会问为什么不直接从业务表里按时间字段增量捞数据这也是我踩过坑之后才彻底想明白的。业务表里未必有统一的、可信的更新时间字段就算有高频更新集中在同一行数据时你很难判断到底发生了多少次变更。基于日志捕获则不同每一次变更都是一条独立记录不多不少语义清晰。2.2 镜像一致性全量 增量 水位线听上去只要盯着日志就行了但现实比这复杂得多。第一次接入 MiroFish 的时候目标库通常是空的或者只有一份旧数据你总不能只同步从今天开始的新变更吧这时候必须解决一个经典问题存量数据怎么同步同步期间产生的新增量怎么衔接两边不会重叠或遗漏吗MiroFish 的做法是分阶段处理。第一阶段是全量镜像阶段它会把源库当前的数据快照读取一份写入目标端。第二阶段才是增量监听阶段它从全量开始时刻对应的日志位置继续往后消费。这里就涉及一个非常关键的概念水位线也就是全量快照到底截止到哪个日志位置。实际操作中全量读取和增量监听是并行推进的。MiroFish 在全量开始前先记录一个日志位点作为基准然后一边导数据一边从那个位点开始缓存新产生的变更。等全量数据倒完再把缓存的增量按顺序应用上去。这样做的好处是全程无锁不需要停业务而且因为增量变更都被缓存着不会出现全量倒完了中间变更已经丢了的尴尬局面。这个设计我见过很多次但真正做得好的不多。有些工具的全量阶段和增量阶段是严格串行的中间要手动停写对在线业务很不友好有些工具虽然并行但水位线记录得不够精确全量导完之后总是有少量数据对不上。MiroFish 在这一块处理得比较干净全量快照和增量起点严格对齐到同一个全局位点源端表结构和数据在同步过程中能保持自洽。2.3 断点续传与重启恢复的设计同步任务跑着跑着突然挂了是分布式环境里的常态不用慌关键是谁能把现场恢复得干净利落。MiroFish 的断点恢复机制是我觉得它最值得拿出来讲的亮点之一。它把所有已处理记录的确认位点做成了持久化状态每隔一小段时间或者每处理一批记录就刷一次盘。重启之后它会加载最近的确认位点从那里重新读取日志而不是从最开始重来一遍。这里有个容易踩坑的细节确认位点刷得太频繁会拖累性能刷得太慢崩溃后重放的日志会变多。MiroFish 默认的策略是每隔固定时间批量确认一次既保证了性能又把重放范围控制在一个很小的窗口内。这个参数官方建议按业务容忍度来调后面我会在实操部分演示怎么改。另一个贴心设计是幂等写入保障。日志重放往往意味着同一条变更可能被投递不止一次这是分布式系统的经典问题——at-least-once 语义。MiroFish 在目标端做了一层去重或幂等处理比如写入时携带主键和版本信息重复投递时自动覆盖或忽略。这样一来重放不再是噩梦最多是重复执行同样的操作不会造成数据错乱。3. 实操从零部署一个 MiroFish 同步任务3.1 环境准备与安装下面进入实操环节。我用一套常见的架构来做演示源端是 MySQL 8.0目标端是 ClickHouse目标是把我库里几张核心业务表实时同步到 ClickHouse供数据分析和报表查询用。首先得确认源库开启了必要的配置。基于日志捕获的前提是数据库本身得把日志打开并且对从库连接开放权限。MySQL 里需要执行以下配置-- 确认 log_bin 已开启 SHOW VARIABLES LIKE log_bin; -- 如果没开启需要在 my.cnf 中配置并重启 MySQL -- [mysqld] -- server-id 1 -- log_bin mysql-bin -- binlog_format ROW -- binlog_row_image FULL -- 创建 MiroFish 使用的复制账号 CREATE USER mirofish% IDENTIFIED BY your_password; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO mirofish%; FLUSH PRIVILEGES;有几个细节我得重点强调一下。binlog_format必须设置成ROW因为只有行级日志才会记录每一行变更前后的完整字段值MiroFish 才能解析出结构化的变更事件。如果数据库用的是默认的STATEMENT格式日志里记的是 SQL 语句本身对于带NOW()、UUID()这类非确定性函数的语句重放的结果可能跟原库不一致。binlog_row_image设为FULL则是确保更新操作的前后镜像都是完整的后面做数据对比时会省很多事。另外要注意如果源库上已经挂着一个主从复制链路新加的 MiroFish 实际上会成为源库的另一个从库server_id 必须跟现有从库区分开不然源库会认为是同一条复制链路日志位点管理会乱掉。接下来是安装 MiroFish 本体。它提供了官方的预编译二进制包下载解压后只有一个可执行文件依赖极少这点在容器化部署时非常友好。我这边用的是 Docker 方式docker pull mirofish/mirofish:latest docker run -d \ --name mirofish \ -p 8080:8080 \ -v /etc/mirofish:/etc/mirofish \ -v /var/lib/mirofish:/var/lib/mirofish \ mirofish/mirofish:latest挂载到/var/lib/mirofish的目录用来存断点位点状态文件这个目录很重要一定不要放在容器临时文件系统里否则容器一重建之前所有确认位点就全丢了任务只能从头开始扫日志IO 开销会非常大。3.2 配置源端与目标端安装完成后需要创建一个 MiroFish 的 Pipeline 配置文件也就是我们常说的管道文件。一个 Pipeline 描述了一条完整的同步任务从哪里读日志、解析哪些表、转换什么格式、写到哪去。我贴一份实际能跑通的示例name: mysql-to-clickhouse source: type: mysql host: 192.168.1.101 port: 3306 username: mirofish password: your_password serverId: 1001 startPosition: LATEST tables: - mydb.users - mydb.orders sink: type: clickhouse host: 192.168.1.102 port: 8123 database: ods username: default password: mapping: mode: auto_create fullSyncBatchSize: 5000 incrementalBatchSize: 1000 commitIntervalMs: 2000 monitor: endpoint: 0.0.0.0:8080 metricsEnabled: true逐个说说关键参数。serverId是 MiroFish 作为源库的从库时使用的复制 ID如前面提到的必须唯一。startPosition配置了首次启动时的起点LATEST表示从当前最新日志位点开始只同步增量数据如果是首次接入且目标库为空建议改成一个历史时间点或配成全量同步MiroFish 会先做全量镜像再做增量衔接。sink部分配置了目标端这里用的是 ClickHouseMiroFish 会自动建库建表不需要提前手动准备表结构。mapping里的auto_create模式会读取源表的结构定义并做一次数据类型映射之后在目标端建出对应表结构。fullSyncBatchSize控制全量阶段每批读取的行数incrementalBatchSize控制增量阶段每批写入的行数这俩参数直接影响同步吞吐后面调优时还要细讲。配置写好之后把它提交给 MiroFish# 通过 admin API 创建 pipeline curl -X POST http://localhost:8080/pipelines \ -H Content-Type: application/yaml \ --data-binary mysql-to-clickhouse.yaml # 启动 pipeline curl -X POST http://localhost:8080/pipelines/mysql-to-clickhouse/start启动之后可以用curl http://localhost:8080/pipelines/mysql-to-clickhouse/status查看当前任务状态正常情况会显示RUNNING同时能看到当前解析到的日志位点、已处理记录数、延迟水位等指标。3.3 启动任务与验证数据一致性Pipeline 跑起来只是第一步验证数据是不是真的一致才是重头戏。我建议在启动后做三个层面的验证。第一层是计数验证。分别查询源表和目标表的行数先看数量级对不对-- 源端 SELECT COUNT(*) FROM mydb.orders; -- 目标端 SELECT COUNT(*) FROM ods.orders;不过行数一致不能说明数据没问题因为可能同一条记录被删掉又插入另一条数量没变但内容错了。所以还得做第二层校验。第二层是抽样校验。挑几行关键数据比对所有字段的值。更严谨的做法是算哈希-- 源端计算一行数据的校验值 SELECT id, MD5(CONCAT_WS(|, id, user_id, amount, status, created_at)) FROM mydb.orders WHERE id BETWEEN 1000 AND 2000 ORDER BY id; -- 目标端同样算一遍对比两侧的 MD5 结果 SELECT id, MD5(CONCAT_WS(|, id, user_id, amount, status, created_at)) FROM ods.orders WHERE id BETWEEN 1000 AND 2000 ORDER BY id;两边结果一致说明这一段数据已经准确镜像了。实际数据量很大的时候可以按 ID 范围分段抽样覆盖开头、中间、结尾几个区间。第三层是实时变更验证。在源库执行一组增删改操作看目标端能不能在几秒内同步过去-- 源端插入一条新订单 INSERT INTO mydb.orders(id, user_id, amount, status, created_at) VALUES (1000001, 8888, 199.00, PAID, NOW()); -- 等一两秒然后到目标端查询 SELECT * FROM ods.orders WHERE id 1000001;如果这条记录在 ClickHouse 中出现了说明整个增量链路已经打通。我习惯把这个验证脚本写成一个简单的 shell 循环连续插入多条记录观察目标端的行数变化和延迟指标确认几天内不会有问题。4. MiroFish 常见问题与排查经验4.1 数据不一致先怀疑映射再怀疑过滤数据同步过程中最怕的就是静默不一致两边都显示同步成功但数据对不上。遇到这种情况我的排查顺序是固定的。先看类型映射。源库的datetime转到 ClickHouse 会不会时差偏移Decimal(10,2)转到 Float 会不会精度丢失varchar的字符集编码转换会不会产生乱码MiroFish 默认的类型映射表在大部分场景下是够用的但总有一些边缘类型需要手动调整。我遇到的真实案例是源库一个datetime(3)字段带毫秒默认映射到目标端后毫秒部分被截断了导致按时间排序的数据乱序。解决办法是在 mapping 配置里显式指定字段类型mapping: columnOverrides: mydb.orders.created_at: targetType: DateTime64(3)再查过滤规则。如果你配置了表过滤或字段过滤一定要确认过滤条件真的符合预期。我见过一个坑是过滤条件里用了源库字段名的驼峰大小写而 MiroFish 解析出来的字段名默认是全小写条件永远匹配不上导致一批数据被悄悄丢掉。这类问题不报错非常隐蔽只能在配置审查阶段仔细盯。如果映射和过滤都没问题就要考虑是不是源库的日志本身就不完整。比如源库开启了日志的临时清理策略或者有人手动执行了PURGE BINARY LOGS把还没被 MiroFish 消费完的日志删掉了。这种情况 MiroFish 会报位点丢失错误但更麻烦的是如果 sink 端做了部分写入目标端会残留半截数据。这时候我通常的做法是重建 pipeline目标表清空重来一遍确保两边从同一个基准点出发。4.2 断点失效确认位点没刷盘MiroFish 的断点恢复依赖持久化的确认位点但有些用户会发现任务重启之后还是会从很远的地方重新开始同步甚至重新做全量。排查这个问题第一件事就是确认状态文件所在的磁盘目录。/var/lib/mirofish如果挂载在容器临时层或者被系统定期清理的临时目录下容器一重启位点文件就没了。还有一种情况是配置里把状态持久化关闭了这在一些测试场景下为了省 IO 可能会这么干但不小心带到生产环境就麻烦了。生产环境一定要让状态目录持久化并且做好备份。我一般会定期把这个状态目录打包存一份到对象存储虽然用到的机会不多但真出问题的时候能救命。另一个比较容易忽略的点是源库的日志位点跟 MiroFish 记录的位置对不上。比如源库发生了主从切换新的主库日志文件名和位点体系跟旧主库不是一个序列MiroFish 按旧的位置去新主库拉日志必然会失败。这种情况无法完全靠程序自动解决需要人工介入告诉 MiroFish 从新的起点开始。MiroFish 提供了一条 API 来手动调整初始位点操作前务必先确认新主库的数据跟旧主库是完整一致的否则断点恢复就没有意义了。4.3 性能瓶颈批量参数不是越大越好我最初调优的时候有个错误直觉以为批量参数设得越大同步速度一定越快。实际测下来批量参数太大反而会出现两个问题。一是单批次处理时间变长内存占用飙升二是目标端单次写入的数据量过大容易触发锁竞争或超出单条查询的长度限制失败重试反而拖慢了整体进度。在我那套配置里fullSyncBatchSize从默认的 5000 调到 10000 时全量导出的速度确实提升了但 ClickHouse 侧的 CPU 和内存指标明显走高偶尔出现写入超时。后来调回 5000然后把多线程写入的并发数从 1 提到 4吞吐反而上去了资源消耗也更平稳。增量阶段的incrementalBatchSize和commitIntervalMs也要配套调整。commitIntervalMs决定了确认位点刷盘的频率刷得越频繁崩溃后的重放窗口越小但磁盘 IO 压力也越大。如果源库的变更量不大可以适当把批量调小、提交间隔调大如果是高吞吐场景就要反过来找到延迟和持久化开销之间的平衡点。这方面没有银弹只能靠监控数据说话。4.4 运维监控这些指标你最好没事就盯一眼MiroFish 内置了 metrics 接口Prometheus 格式拿来接 Grafana 很方便。我最关心的指标就几个当前处理的日志位点相对于源库最新位点的延迟这个直接反应同步链路是不是健康的目标端写入失败的重试次数这个通常预示着目标端有问题已处理记录数和吞吐速率用来观察链路是否在正常工作。另外有个小技巧给 pipeline 配置一个简单的存活探活。MiroFish 有健康检查接口返回 200 就说明主进程还活着。但进程活着不代表任务在正常推进——有时候网络分区会导致它连着源库但读不到新日志。所以我还会定期检查延迟指标如果延迟持续走高超过阈值就触发告警。这套组合拳打下来同步链路基本不会出那种挂了几个小时没人发现的事故。5. MiroFish 的横向对比与选型建议5.1 它和 Debezium、Canal、Flink CDC 的差异在哪聊到这里肯定有人会想拿 MiroFish 跟市面已有的 CDC 工具做对比。我的看法是它跟这些工具并不是纯粹的替代关系更像是各有侧重的竞品。Canal 专注的是 MySQL 的日志解析在 MySQL 生态里很成熟但部署依赖 Alibaba 的生态环境独立使用时要自己搭不少配套组件。Debezium 功能更强支持的数据源也多但部署形态基于 Kafka Connect对没有 Kafka 基础设施的团队来说略重。Flink CDC 则适合跟 Flink 计算引擎深度集成的场景如果你本来就在用 Flink 做流式计算选它顺手如果只是想把 MySQL 数据实时搬到 ClickHouse 里引入一套 Flink 集群可能有点过度设计。MiroFish 的定位正好卡在中间单进程、轻量级、开箱即用不强制依赖消息队列和计算引擎。对中小团队来说部署和维护成本低出现问题也好排查。它的表达能力不如 Debezium 和 Flink CDC 那么通用但在数据库到数据库这个最常见的同步需求上胜在简单直接。5.2 什么时候该用什么时候不该用如果你需要的是多源异构、复杂 ETL、流式计算老老实实用 Flink CDC 或 Debezium它们是更通用的大杀器。如果只是把几张核心业务表实时同步到数仓、搜索或下游业务库且团队没有专职的数据平台工程师MiroFish 是很合适的选择。也要注意它的边界。MiroFish 目前对源端的支持以 MySQL 为主流其他数据库的适配还在逐渐补齐如果你的源端是 Oracle 或 SQL Server 且没有太多现成经验可能需要多踩一些坑。另外它的目标端连接器虽然覆盖了不少常用组件但某些小众存储的适配度不高选型前最好用真实数据做一轮小规模 POC别光看文档列表。在我这边的项目里MiroFish 最终落地的方式是MySQL 作为源库输出到 ClickHouse 建立实时 ODS 层再被下游的报表和临时查询消费。这个链路跑了一个多月每天处理上百万条变更手工对账倒是彻底省了。6. 实操中的几个小技巧与最后的扩展想法最后分享几个我在配置和运维 MiroFish 时积累的小技巧这些在官方文档里提得不多但实战中非常有用。第一目标表如果已经存在并且你已经知道它的表结构是合理的一定要设置sink.createPolicy skip不要启动auto_create否则 MiroFish 可能会因为类型映射不符合预期反复尝试重建表造成大量无用操作。第二如果同步的源表数量很多强烈建议按业务模块拆分成多个 pipeline而不是一个 pipeline 管所有表。这样某个模块链路出问题时不会连带影响其他模块出问题后的排查范围也缩小很多。第三源库大字段特别多的时候全量阶段的网络带宽消耗非常惊人建议在生产窗口外启动首次全量或者限一下带宽。另外可以聊一点后续的扩展思路。MiroFish 的日志捕获产物本质上是标准化的数据变更事件你可以把它接进自己的消息队列让下游不仅是数据库还能触发缓存刷新、搜索引擎索引更新、事件总线广播等。坦白说我这个项目目前还只用了它最核心的同步能力但它的这套设计已经让我觉得后续想在这个链路上增加更多实时消费者是有充分扩展空间的。接手这个项目之前我对这类工具是抱持怀疑态度的毕竟数据同步这事听着简单做起来全是细节。但用顺手之后我最大的体会是一个工具能不能解决实际问题关键要看它有没有把那些脏活累活提前想明白。MiroFish 至少在日志位点管理、全量增量衔接、幂等写入这几个最闹心的点上给了我足够的信心。现在这套链路跑得很稳我也有精力去操心别的真正值得操心的业务问题了。
返回列表