实时数据同步实战:从日志捕获到一致性保障的工程指南
1. 为什么实时两个字值钱从一次数据延迟事故说起很多团队第一次认真对待实时数据同步都是被线上问题逼出来的。我印象很深的一次某业务系统的报表数据总是比实际业务慢半拍运营同学上午看到的订单量到下午才对得上。排查下来不是数据库慢也不是查询写错了而是同步链路用的是定时批量拉取——每15分钟跑一次跑一次要3分钟中间还经常因为锁表被拖到5分钟。业务方要的是现在这一刻的数据链路给的却是15分钟前的快照这个gap在业务高峰期会被放大到让人抓狂。这就是实时数据同步要解决的核心问题让源端的数据变化在秒级甚至亚秒级内出现在目标端。它和传统的ETL批量同步最大的区别在于批量同步是我按我的节奏去拿实时同步是数据一变就主动推过来。前者适合T1的离线数仓后者适合实时大屏、风控、在线推荐、异构系统双写这些场景。Tapdata Real Time DaaS 这套东西本质上就是围绕实时这个目标搭起来的一整套数据即服务能力。Part I 聚焦在实时数据同步这一块我把它拆成几个必须搞明白的问题它靠什么机制做到实时、支持哪些源和目标、同步过程中怎么保证不丢不重、遇到大事务和DDL怎么办、实际部署时哪些参数会坑人。下面按我自己的理解顺序展开不按官方文档的目录走。提示本文涉及的所有配置示例和参数值都是基于常见生产实践的合理补充具体以你实际使用的版本和官方文档为准。不同版本之间行为可能有差异动手前先确认版本号。2. 实时同步的底层抓手日志捕获到底在捕什么2.1 从查表到读日志的思维切换批量同步的思路是不断去源库执行SELECT靠时间戳或者自增ID捞增量。这个做法在数据量小、并发低的时候能用但一旦源库压力大频繁查询本身就是负担而且时间戳字段如果没有索引或者存在时钟回拨增量就会漏。实时同步换了一条路不去问数据库有什么新数据而是去读数据库自己记的账本——日志。关系型数据库为了保证事务的持久性和主从复制都会把每一次数据变更写进日志里。MySQL 有 binlogPostgreSQL 有 WAL逻辑复制槽Oracle 有 Redo Log 和 LogMinerSQL Server 有 CDC。这些日志原本是给数据库自己用的但它们的格式是结构化的、有序的、包含完整变更前后镜像的天然适合做数据同步的数据源。Tapdata 这类工具的核心能力之一就是把这些不同格式的日志解析成统一的变更事件流。这个切换带来的好处很直接对源库的侵入极小基本就是多了一个从库在消费日志延迟可以做到毫秒级因为日志是实时写的变更顺序和事务边界都能保留不会出现先看到更新后看到插入这种乱序。2.2 不同源库的日志读取姿势差异虽然思路都是读日志但每种数据库的脾气不一样配置起来差别很大。我整理了一张对照表是实际部署时最常打交道的几个源源库类型日志机制关键前置条件常见坑MySQLbinlogbinlog_formatROWbinlog_row_imageFULL用 STATEMENT 格式拿不到完整行镜像PostgreSQL逻辑复制槽wal_levellogical需要复制权限槽不消费会撑爆 WAL 磁盘OracleLogMiner / Redo需要补充日志supplemental logging不开补充日志拿不到完整列值SQL ServerCDC需要启用数据库级和表级 CDCCDC 清理任务会删掉未消费的变更MongoDBOplog / Change Stream副本集或分片集群单机模式没有 oplog拿 MySQL 来说binlog_format必须是 ROW这个几乎是硬性要求。原因在于 STATEMENT 格式只记录 SQL 语句本身比如UPDATE t SET status1 WHERE id100同步端拿到这条语句根本不知道具体改了哪些行、改成了什么值。ROW 格式记录的是每一行的前后镜像同步端才能精确重放。binlog_row_image设成 FULL 是为了拿到更新前的完整旧值做冲突检测和回滚的时候用得上。PostgreSQL 的逻辑复制槽有个特别需要注意的地方槽位会记住消费者读到哪儿了如果消费者挂了不消费WAL 日志会一直堆积不释放最后把磁盘撑满。我见过一次因为同步任务停了三天没管源库磁盘直接告警的事故。所以生产环境一定要给复制槽配监控或者设置max_slot_wal_keep_size做兜底。2.3 日志解析之后发生了什么日志被读出来只是第一步后面还有一串处理解析成统一的变更事件INSERT/UPDATE/DELETE、按事务分组保证原子性、做 schema 映射源表字段对应到目标表字段、类型转换比如 MySQL 的datetime到目标端的timestamp、最后写入目标端。Tapdata 把这套流程做成了可视化的管道但底层逻辑跑不出这几步。这里有个容易被忽略的点事务边界。一个事务里改了5张表同步端必须保证这5张表的变更要么都成功、要么都失败不能出现改了3张卡住的情况。这就要求同步引擎在处理日志时能识别事务的开始和提交把中间的事件先缓存起来等提交信号到了再一起下发。这也是为什么实时同步对内存有一定要求——大事务的缓存会占用不少堆内存。3. 把数据搬过去之后写入端的策略选择3.1 目标端不只是写进去那么简单很多人以为同步就是源端读一条、目标端写一条实际上写入端的策略决定了整个链路的吞吐和一致性。最朴素的逐条 INSERT 在低并发下没问题但一旦源端每秒几千条变更逐条写会把目标库打爆。所以实际产品里都会做批量攒批把短时间内的一批变更攒成一个批次用批量写入的方式提交。攒批的代价是延迟。攒批窗口设成100毫秒延迟就至少100毫秒设成1秒延迟就上去了。这是个典型的吞吐和延迟的权衡。我的经验是对延迟敏感的场景比如实时风控把批次调小、并发调高对吞吐敏感的场景比如历史数据迁移把批次调大。Tapdata 里一般能配 batch size 和 flush interval 这两个参数具体名字看版本。3.2 幂等写入解决至少一次带来的重复实时同步链路为了保证不丢数据通常采用至少一次at-least-once的投递语义——网络抖动、进程重启都可能导致同一条变更被重复投递。如果目标端只是无脑 INSERT重复数据就进去了。解决办法是让写入具备幂等性。幂等的实现方式取决于目标端类型。关系型数据库一般用主键做 UPSERTINSERT ... ON DUPLICATE KEY UPDATE或者MERGE这样同一条变更重放多少次结果都一样。对于没有主键的表就得靠额外的去重机制比如记录已处理的位点offset或者用变更的唯一标识做判重。这里要提醒一句源表没有主键的同步一定要提前评估很多同步工具对无主键表的支持是有限制的要么性能差要么干脆不支持。3.3 目标端的多样性带来的映射问题实时同步的目标端可能是另一个关系库、可能是消息队列Kafka、可能是搜索引擎Elasticsearch、也可能是数据湖。不同目标端的写入模型差别很大。写 Kafka 是追加消息天然幂等靠分区和 offset写 ES 是文档覆盖靠_id做幂等写关系库是行级 UPSERT。类型映射也是个细活。源端 MySQL 的tinyint(1)通常被当作布尔值但目标端如果是 PostgreSQL得映射成boolean源端的decimal(10,2)到目标端如果精度不够会丢小数。这些映射规则在配置同步任务时都要逐字段确认别指望默认映射能覆盖所有情况。我一般会拿一张有代表性的表先跑通把类型映射调对了再批量铺开。4. 一致性这道坎全量加增量怎么衔接才不丢不重4.1 全量同步和增量同步的接缝实时同步通常不是一上来就只做增量因为目标端一开始是空的得先把存量数据搬过去这叫全量初始化。全量搬完之后再切到增量中间这个切换点是最容易出问题的地方。假设全量同步在 T 时刻开始扫了100万行数据扫到 T10 分钟才扫完。这10分钟里源端又产生了新的变更如果增量同步是从 T10 分钟才开始读日志那 T 到 T10 之间的变更就丢了。正确的做法是增量日志的读取起点要早于全量扫描的起点先把日志缓冲起来等全量搬完再把缓冲的增量重放上去。这样全量增量才能无缝衔接。Tapdata 这类工具一般会自动处理这个衔接但作为使用者要理解它的原理才能在出问题时判断是哪里断了。判断方法很简单全量结束后对比源和目标的行数、校验几个关键字段的 checksum如果对不上大概率是衔接点没处理好。4.2 一致性校验别等业务发现数据不对才查同步链路跑起来之后怎么知道数据是一致的靠业务方反馈数据不对是最被动的。主动做一致性校验才是正道。校验分几个层次行数校验最粗粒度源和目标各COUNT(*)对比。快但不够行数一样内容可能不一样。分块 checksum把表按主键分成若干块每块算一个校验和对比。能定位到具体哪块不一致。逐行比对最细但代价最大一般只对关键表做。我的做法是核心表每天凌晨做一次分块 checksum非核心表每周一次行数校验。发现不一致之后不要急着全量重刷先定位是哪条记录、哪个字段的问题很多时候是某次大事务处理异常导致的局部问题补一下就行。4.3 冲突处理当源和目标都能写有些场景下目标端不是只读的业务可能也会往目标端写数据这就产生了冲突。比如双写架构里两个库互为源和目标。这时候同步工具需要一套冲突解决策略以源端为准、以时间戳新的为准、或者自定义规则。大多数实时同步场景其实是单向的源端写、目标端读冲突问题不突出。但一旦涉及双向同步或者多源汇聚冲突策略就必须提前设计好。我见过因为没定义冲突规则两个方向的同步互相覆盖最后数据来回震荡的案例。双向同步一定要有明确的权威源概念或者用版本号/时间戳做仲裁。5. 大事务、DDL 和那些让同步卡住的瞬间5.1 大事务内存和延迟的双重考验一个事务改了50万行这在业务上可能是批量更新或者数据修复。对同步链路来说这意味着要缓存50万条变更事件直到事务提交内存压力陡增。如果缓存扛不住要么丢数据要么同步进程 OOM。应对大事务有几个思路。一是调大同步进程的堆内存给它足够的缓冲空间这是最直接的。二是开启事务拆分如果工具支持把大事务拆成小批次下发牺牲一点原子性换稳定性。三是在源端规避业务上尽量把大事务拆小比如分批提交。第三种其实是最治本的但往往改不动业务代码。我个人的经验是先监控再优化。给同步任务加上事务大小的监控指标超过阈值就告警这样能在问题爆发前介入。Tapdata 的任务监控里一般能看到处理的事务数和延迟定期看一眼没坏处。5.2 DDL 变更schema 漂移怎么跟源表加了一列、改了一个字段类型、删了一个索引这些 DDL 变更同步端能不能感知如果感知不到后续的变更事件里多出来的字段就会解析失败。好的同步工具会解析 DDL 事件并自动更新目标端的 schema但这个过程不是所有场景都能自动完成。比如源端 MySQL 加了一列目标端是 ElasticsearchES 的 mapping 一旦建立就不能随便改字段类型这时候自动同步 DDL 就会失败。实际做法通常是DDL 变更走变更流程同步任务暂停、手动调整目标端 schema、再恢复同步。听起来笨但可控。自动 DDL 同步适合源和目标同构的场景异构场景还是人工介入稳妥。5.3 位点管理重启之后从哪儿继续同步任务重启后从哪个位点继续直接决定了会不会丢数据或重复数据。位点信息一般包括日志文件名和偏移量MySQL、LSNPostgreSQL、SCNOracle。这些位点需要持久化存储重启后读取。这里有个细节位点提交的频率。如果每处理一条就提交一次位点性能差但丢数据风险低如果攒一批提交一次性能好但进程崩溃时可能丢最后一批。折中方案是异步提交位点配合幂等写入即使重复也不会出错。Tapdata 里位点管理是自动的但理解它的机制能帮你在排查重启后数据重复这类问题时快速定位。6. 部署与调优那些文档里不会写的参数心得6.1 源库侧的让路配置实时同步对源库是有资源消耗的虽然比批量查询小但也不是零成本。MySQL 上binlog 的写入本身有开销如果同步端消费慢binlog 文件会堆积。建议给 binlog 设置合理的过期时间expire_logs_days或binlog_expire_logs_seconds同时监控同步延迟别让消费速度长期落后于生产速度。PostgreSQL 的逻辑复制槽前面提过一定要配max_slot_wal_keep_size做兜底防止 WAL 无限堆积。Oracle 的 LogMiner 会占用额外的 UNDO 空间大事务多的时候要留意 UNDO 表空间的增长。6.2 同步进程的资源分配同步进程本身是个吃内存和网络的应用。内存主要花在事务缓存、批次缓冲和 schema 缓存上。网络则是持续的数据流。部署时建议内存至少给到 4GB 起步大事务多的场景 8GB 以上。CPU日志解析是 CPU 密集型核数多一点解析快。网络跨机房同步要评估带宽变更量大时带宽是瓶颈。磁盘位点和元数据存储需要持久化磁盘别用临时盘。6.3 监控指标盯住这几个数就够了同步任务跑起来之后不需要盯几十个指标抓住几个核心的就行指标含义告警阈值建议同步延迟源端变更到目标端可见的时间差持续超过 10 秒告警处理速率每秒处理的变更条数骤降 50% 以上关注位点落后当前位点与源端最新位点的差距持续增大告警错误计数解析或写入失败的次数大于 0 就查内存使用同步进程堆内存超过 80% 关注延迟是最直观的指标但要注意它的测量方式。有些工具算的是最后一条变更的处理时间差有些算的是位点差距换算成的时间两者含义不同。搞清楚你用的工具怎么算延迟才能正确解读。6.4 一个真实的调优案例某次同步任务延迟忽高忽低高峰期能到几十秒。排查发现源端有个定时任务每小时跑一次批量更新一次改几万行形成大事务。同步端处理这个大事务时内存吃紧触发 GCGC 停顿期间延迟飙升。解决办法是双管齐下一方面把同步进程内存从 4GB 调到 8GB减少 GC 频率另一方面和业务方沟通把那个批量更新拆成每批 2000 行、间隔提交大事务变成小事务。调整之后延迟稳定在 1 秒以内。这个案例说明实时同步的稳定性往往不只取决于同步工具本身源端的写入模式同样关键。7. 选型与落地时我踩过的几个认知误区7.1 实时不等于零延迟刚接触的时候我以为实时同步就是没有延迟实际用下来发现任何链路都有延迟只是大小问题。日志产生到被读取有延迟解析有延迟网络传输有延迟写入目标端有延迟。端到端能做到亚秒级已经很不错了追求绝对的零延迟不现实。把预期定在秒级而不是零心态会好很多架构设计也更务实。7.2 不是所有表都适合实时同步有些表变更极其频繁比如计数器表、心跳表每秒几千次更新同步过去意义不大还占资源。有些表几乎不变比如配置表、字典表实时同步纯属浪费定时全量就够了。按表分级热表实时、温表准实时、冷表批量这样资源用在刀刃上。7.3 同步链路本身也需要高可用同步进程挂了怎么办单点部署的话挂了就断了恢复期间的数据变更可能丢。生产环境建议同步进程也做高可用主备或者集群模式主挂了备能接管位点继续跑。Tapdata 支持集群部署具体怎么配看部署文档但同步链路要高可用这个意识得有。7.4 别忘了目标端的写入能力源端每秒一万条变更目标端能不能扛住每秒一万次写入如果目标端是个配置不高的库同步过去反而把它打挂了。上线前一定要做目标端的写入压测确认它的写入能力匹配源端的变更速率。必要时给目标端加索引优化、调大连接池、甚至做分库分表。8. 写在最后的一点个人体会实时数据同步这东西原理讲起来不复杂——读日志、解析、写入三步。但真正落地到生产环境坑都在细节里源库的日志配置、事务边界的处理、位点的持久化、大事务的应对、DDL 的同步、目标端的写入能力、监控告警的完善。每一个环节没处理好都可能变成线上事故。我的建议是第一次上实时同步先拿一张变更量适中的表跑通全流程把全量加增量的衔接、延迟监控、一致性校验都验证一遍再逐步扩大范围。别一上来就全库同步出了问题排查起来范围太大。等跑顺了几张表对工具的行为有了手感再铺开就从容多了。另外实时同步不是一劳永逸的事。业务在变、数据量在涨、源端 schema 在改同步链路需要持续维护。把它当成一个需要长期照看的服务而不是配好就不管的工具心态上就对了。