ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Ponytail:基于Canal的MySQL到HBase增量同步插件解析

Ponytail:基于Canal的MySQL到HBase增量同步插件解析 第一次看到“ponytail”这个词我以为又是个和发型相关的项目结果在美团点评的 GitHub 组织里撞见它才反应过来这压根不是什么编发教程而是个实打实的数据同步插件。它的名字很形象马尾辫嘛一束数据从 MySQL 的 binlog 里被梳理出来再顺顺当当地绑到 HBase 上。在 Canal 解析完增量变更之后在 HBase 真正落库之前这个插件填补了中间那段“从消息到写入”的空白。如果你正在做实时数仓、想把业务库的增量数据同步到 HBase/Hive又不想自己造一套消费 Canal 的轮子这篇文章应该对你有用。我会从它解决的痛点讲起拆一遍底层的工作链路再把手里的部署配置和排障经验一并倒出来。1. ponytail不是发型这个数据同步插件到底在解决什么问题1.1 从Canal到HBase中间缺了一个“数据搬运工”很多做数据开发的团队早期同步数据的方式很直接写一个定时任务凌晨把 MySQL 里的数据全量抽一遍到 HBase白天业务系统再查 HBase。这套逻辑在数据量小、实时性要求不高的阶段确实够用但等业务到了一定规模凌晨抽数的延迟、全量导入对源库的压力都会变成实实在在的问题。于是大家开始关注增量同步而聊到增量同步Canal 是第一选择。Canal 做的事情很纯粹把自己伪装成 MySQL 的从库拉取 binlog把增删改解析成结构化消息。但它只管“告诉你有变化”不管“把变化写到哪儿”。想要把 Canal 的消息落进 HBase你至少得写一个消费者程序处理消息解析、字段映射、并发写入、失败重试、断点续传。这些事情单拎出来哪一件都不难但凑在一起就变成了一个隐形的开发量而且每个团队写出来的方案都不一样很难维护。ponytail 就是在这个夹缝里出现的。它把自己定位成 Canal 和 HBase 之间的轻量管道消费 Canal 推送过来的 binlog 变更然后按你配置的规则写入 HBase。你不需要关心消息从哪来、位点怎么记、写失败了怎么办至少大部分常规场景不需要你操心。这个东西体积不大定位清晰放在实时同步链路里就是一个标准的“数据搬运工”。1.2 和Sqoop、DataX的定位差异全量与增量是两条路线我经常看到有人把 ponytail 和 Sqoop、DataX 放一起比较然后纠结该选哪个。其实这俩根本不是同一个赛道的东西。Sqoop 是典型的全量批量工具它擅长把一张表一次性搬到 HDFS/Hive但搬完之后你再想让新增的数据自动流过去Sqoop 就没什么好办法了通常只能靠定时任务再抽一次。DataX 差不多也是这一路的思路强在异构数据源之间的离线批量同步一次性把一个表从 A 抽到 B它是专业的。ponytail 走的是另一条路线持续不断地监听 binlog每来一条变更就实时写入 HBase。它不做全量也没有批量灌库的概念它的核心优势是“实时”和“增量”。做离线数仓用 DataX、Sqoop 没问题但做实时链路、做 HBase 在线服务的数据回填那才是 ponytail 的主场。所以正确的姿势不是二选一而是分工配合。存量数据用 Sqoop 或 DataX 先抽一遍灌进去增量部分交给 Canal 加 ponytail 持续同步。一个解决“历史怎么来”一个解决“未来怎么走”两条腿走路才稳。对比维度Sqoop / DataXponytail同步方式全量批量、定时抽取增量实时、事件驱动数据源多种关系型/RDBMS依赖 Canal 解析的 MySQL binlog典型去向HDFS、Hive、HBaseHBase适用阶段存量数据初始化增量变更持续写入运维模式跑批任务、关注耗时常驻进程、关注位点与堆积2. ponytail的工作链路拆解一条binlog是怎么变成HBase里的一行记录2.1 整条链路的分工与流转想用好 ponytail最怕的就是只把它当黑盒使出了问题没法定位。所以我先花点篇幅把它的工作链路说清楚整条线其实分成四段。第一段是 MySQL 这侧。要开启 binlog而且必须用 ROW 格式因为只有 ROW 格式才记录“哪一行发生了什么变化”这是增量同步的基础。第二段是 Canal 这侧。Canal 把自己伪装成一个 MySQL 从库和主库建立复制协议拉取 binlog 并解析成一条条结构化的变更消息再通过网络推送给消费者。第三段是 ponytail 这侧。它注册成 Canal 的消费者收到消息后解析出来拿到表名、主键、列名、新旧值再根据配置做字段映射拼装成 HBase 的 Put 请求。第四段是 HBase 这侧。Put 请求通过 HBase 客户端发到 RegionServer最终落到 WAL 和 MemStore数据就可见了。用生活里的例子来打比方MySQL 是银行柜台每一笔存取款都被流水账binlog记下来Canal 是盯账本的会计把每一笔流水整理成通顺的报表ponytail 是跑腿的拿着报表去另一个储蓄所HBase把钱存到对应的户头上。如果没有跑腿的会计整理完报表就只能堆在桌上什么也干不了。2.2 几个关键机制位点、映射与并发写入理解链路之后还有三个机制值得单独拿出来说因为它们决定了同步能不能稳定跑起来。第一个是位点机制。Canal 在 Zookeeper 里记录了消费者消费到了哪个 binlog 位点ponytail 每次消费完一批消息就会把位点往前推。这个机制保证了宕机重启之后程序能从上次的位置接着消费而不是从头重放也不是跳着漏数据。位点丢了是增量同步里最要命的事故之一后面我会专门聊怎么避免。第二个是字段映射。MySQL 的表结构是二维的HBase 的表结构是“行键加列族加列限定符”的两者要对应起来必须有一份映射关系。ponytail 的配置里会写明源表的哪一列对应 HBase 的 RowKey哪一列落到哪个列族的哪个列。我见过不少团队图省事把整行数据塞进一个列结果 HBase 这张表完全没法做范围查询最后只能重建表重新刷数。映射这步省不得。第三个是并发写入。binlog 是一条一条的但写入 HBase 是可以并发的。ponytail 内部维护了一个线程池把一批消息拆开并行发 Put 请求给 HBase。并发数直接决定同步的吞吐上限。这个参数也不是盲目往大了调得看 HBase 集群的 Region 数量和写入能力调太大容易把 RegionServer 打满调太小又压不住高峰期的写入量。3. 部署前最容易被忽略的环节binlog配置、Canal实例与网络连通性3.1 开启MySQL的binlog是第一个门槛而且必须用ROW格式很多人拿着 ponytail 的代码跑不起来第一步就卡在 MySQL 的 binlog 配置上。默认情况下 MySQL 的 binlog 可能压根没开或者开了但格式是 STATEMENT这种格式只记录 SQL 语句不记录行的变更细节增量同步拿到这种日志根本没法还原数据。我推荐的配置是在 MySQL 的my.cnf里加上以下几行server_id 1 log_bin mysql-bin binlog_format ROW binlog_row_image FULL expire_logs_days 7 max_binlog_size 256Mbinlog_format ROW这个最重要它保证 binlog 里记录的是每行数据变更前后的完整字段值Canal 解析出来才准确。binlog_row_image FULL是为了把变更行的全部列都记下来避免只记主键和变更列导致下游拿不到完整的上下文。expire_logs_days建议别设太长磁盘有限而且同步链路断了太久修不回来的时候binlog 早被清了这时候只能接受现实做一次全量重刷。配置完别忘了一个动作先重启 MySQL再确认SHOW VARIABLES LIKE binlog_format返回的是 ROW。我排障时见过有人把参数写进配置文件但忘了重启折腾一整天以为是代码问题最后发现 MySQL 根本没生效。3.2 Canal实例的配置要点与常见雷区Canal 这侧主要涉及两个配置文件canal.properties里的canal.destinations决定有哪些 instance 要被启动instance.properties里则配置连接哪个 MySQL、用什么账号密码、从哪个位点开始消费。Canal 连接 MySQL 时本质上是在模拟一个从库所以它必须有一个属于自己的slaveId而且这个slaveId不能和 MySQL 主库里已经有任何从库的slaveId重复。我这边的经验是给 Canal 单独分配一个大一点的数字区间比如 100 往上的编号同时把 MySQL 现有从库的 ID 都查一遍从根上避免冲突。另外一个雷区是位点的初始位置。instance.properties里可以配置canal.instance.master.journal.name和canal.instance.master.position这决定了 Canal 从哪个 binlog 文件、哪个偏移量开始消费。如果这里配错了可能出现两种结果一种是从很早的位点开始把过期数据全量重放一遍另一种是从太新的位点开始中间一段的数据直接漏掉。最稳妥的初始位点是当前 MySQL 的 binlog 最新位置这样接入之后只消费新产生的变更。3.3 网络连通性Zookeeper、Canal、HBase三者之间的时延底线部署前还有一件容易被忽略的事网络连通性。ponytail 需要同时访问 Zookeeper、Canal 服务和 HBase 集群这三者之间如果网络不通畅同步会表现得非常诡异。Zookeeper 主要用于复用 Canal 的位点信息和 HBase 的元数据信息如果 ZK 时延高最直接的后果是位点提交变慢极端情况下会触发重复消费。Canal 和 ponytail 之间是实时推送关系网络抖动会导致消息堆积在 Canal 侧表现为同步延迟突然拉高。HBase 这侧更直接写入的 RPC 超时、重试风暴都跟网络相关。我自己的习惯是先把三台机器的互 ping 时延打出来看一眼超过 5ms 就要警惕了再用nc -vz把关键端口都扫一遍。部署之前花十分钟做这个检查能省掉后面一大半的连锁排查。4. 从代码到数据构建、核心配置与验证同步4.1 构建项目并不复杂但依赖版本要留意ponytail 的代码在 GitHub 上可以找到拿到之后第一步是本地构建。项目是一个标准的 Maven 工程装好 JDK 和 Maven 之后在根目录直接执行mvn package -DskipTests构建过程中比较容易踩的坑是依赖版本冲突。ponytail 本身要对接 HBase 客户端、Zookeeper、Netty这几个组件对 JDK 版本和彼此的版本都很敏感。如果你本地的 JDK 版本太新可能会遇到一些反射相关的告警不影响编译但运行起来会有隐患。我的建议是和当时的 HBase 版本保持同一代的 JDK比如 HBase 1.x 时代用 JDK 8 就够稳。构建完成后会打出一个可执行的 jar 包或者按项目说明把依赖目录一起整理好。这一步只要 Maven 能正常拉取依赖基本不会出大问题。4.2 核心配置参数读一遍就懂怎么改运行之前的配置环节是决定同步成败的关键。ponytail 的配置本质上就是回答几个问题数据从哪个 Canal 实例来写到哪个 HBase 集群同步哪些表列怎么映射并发度设多少我把常见需要关注的参数整理成了一个表格方便你对照着改配置项作用说明配置建议Canal 的 destination指定消费哪个 Canal 实例的消息和canal.properties中配置的实例名保持一致Zookeeper 地址用于读取 Canal 位点、HBase 元数据多个节点用逗号分隔至少两个HBase 的 znode 父路径指向 HBase 在 ZK 上注册的根节点默认通常是/hbase按集群实际配置HBase 表名数据要写入的目标表建议提前建好预分区后再同步列族名数据落到哪个列族按业务访问模式设计不必多个列族源表到 RowKey 的映射哪一列作为 HBase 的行键取业务唯一键避免单调递增线程池大小写入 HBase 的并行度从 4 到 8 起步看延迟再调这里要特别提醒不同版本的配置文件字段命名可能有差异具体以你拉下来的项目源码里示例配置文件为准。但不管字段名怎么变背后回答的问题就是表格里那几件事。想明白之后再对着示例文件填基本不会跑偏。4.3 启动与验证从日志到HBase Shell的完整确认配置写完之后启动命令其实就是一个带 classpath 的 Java 进程java -cp ponytail.jar:./conf com.meituan.ponytail.PonyTailMain启动之后先别急着看 HBase盯着日志确认三件事。第一件有没有报“connected to canal”之类的日志有就说明 Canal 这侧接上了。第二件有没有持续打印消费到的消息条数或是 Put 的耗时有就说明数据开始流过来了。第三件有没有打印位点提交成功的日志有就说明 ZK 这侧的位点在正常推进。日志确认没问题之后再去 HBase 侧做最终验证。用 HBase Shell 查看目标表scan your_table, {LIMIT 10}再对你刚同步过去的那条数据做精确查询比如用 get 指定 RowKey 查出来比对字段值。如果新增的数据能查到、修改的数据也变新了那这条链路就算真正跑通了。我实际测试时习惯在源表里连续做一个 insert、一个 update、一个 delete然后到 HBase 里分别确认插入生效、更新覆盖、删除标记或物理删除。三种变更都能正确处理链路才算是真的稳了。5. 增量同步常见故障三次排障经历的完整回顾5.1 场景一启动了但一直收不到数据问题出在slaveId冲突有段时间我启动 ponytail 之后控制台安安静静日志里连一条消费记录都没有。Canal 那侧也是正常的消息能收到就是没见 ponytail 消费。当时排查的顺序大致是这样的先看 Canal 是不是活着Zookeeper 里的 instance 有没有正常注册。再看 zookeeper 里消费位点有没有变化一点没动。然后去 MySQL 里查了一下从库列表发现 Canal 注册的 slave 根本不在线。最后才发现是slaveId冲突。Canal 的默认slaveId是 0 或者一个很小的数字和 MySQL 内部某个复制账号占用的 ID 撞了。MySQL 一看这个“从库”的 ID 已经被登记过直接不认Canal 拉 binlog 的请求就一直处于悬空状态自然也没有任何数据能被推给 ponytail。解决的办法不复杂把 Canal 的slaveId改成 100 以上的独立编号确保和现有所有从库都不重复重启 Canal 之后数据立刻就来了。这次排障给我最大的提醒是增量同步链条上的每个组件都有一堆“默认值”而这些默认值凑在一起就会打架动手之前先把每个组件间的连接身份查一遍能省下半天时间。5.2 场景二数据越来越慢写入热点在HBase的Region上堆积第二次比较头疼的问题是同步延迟从最开始的一两秒慢慢涨到十几分钟。HBase 里的老数据没问题新数据迟迟不出现但源库的写入量其实并没有突增。排查思路是先确认哪一段慢。看 ponytail 的日志消费端拉取消息的耗时很小但 Put 的耗时一直在涨。再到 HBase 的 Web UI 上看 Region 的读写请求分布很快就发现问题了某一张表的写入请求几乎集中在一个 Region 上其他 Region 全是空闲的。原因其实很典型——RowKey 设计成单调自增的了。源表的主键是自增 idponytail 同步过来时直接拿这个 id 当 RowKey写入请求在 HBase 里就全集中到了尾部的 Region 上形成了热点。RegionServer 单点处理不过来写入排队延迟自然就上去了。解决方式是从 RowKey 下手。给自增 id 前面拼一个散列前缀比如按用户维度取模后反转拼接让写入尽可能均匀地散到各个 Region 上。改完 RowKey 策略重新跑全量再增量之后延迟在几分钟内就降回了正常水平。这次刷新了我对“同步慢”的认知很多时候瓶颈不在消费端而在 HBase 的表设计端。5.3 场景三源表加了字段HBase 这侧持续报错直到重建映射第三次故障发生在一次源库表结构变更之后。业务同学往一张订单表里加了一个字段加了备注列然后 ponytail 就开始在日志里反复报字段解析失败同步直接卡死。表面上这是个“同步进程报错”的问题实际上是一个上下游 Schema 不一致的问题。binlog 里的 JSON 数据带了新的列ponytail 的映射规则还是旧的解析时发现既定的列没有变化却又多出了没见过的列处理逻辑按严格模式抛了异常。这种问题的根治方法不是祈祷“加了字段之后解析器能智能跳过”而是要在运维侧把表结构变更纳入管控流程。我后来和业务团队达成的默契是任何源表结构变更提前一天通知数据团队数据这边先更新映射配置再放行变更。对于已经产生的故障处理步骤也不复杂同步停下更新映射配置重启进程让位点接着走。这里我也想说明一点并不是所有新增字段都需要同步到 HBase但至少解析器要能容忍增量字段的存在或者配置里要明确忽略哪些列。如果你的 ponytail 版本解析逻辑比较严格升级到有宽容策略的版本或者在上游过滤字段都是有效的办法。6. 进阶优化RowKey设计、预分区与稳定性兜底6.1 RowKey设计直接决定同步能跑多快多稳低延迟的增量同步除了消费端要稳HBase 这侧的表设计也要配合。RowKey 是 HBase 数据分布的最小单元也是写入分布的最大变量。刚才提到的自增 id 直接当 RowKey 会造成热点那反过来想怎么设计才合理核心思路就一句话让 RowKey 的分布尽可能均匀同时保证查询效率不下降。业界常见的做法是加散列前缀比如把用户 ID 做 hash 后取前几位拼在原 ID 前面。这样做的好处是写入会分散到不同的 Region坏处是范围查询的性质变了如果想按原来的 ID 顺序扫描就扫不动了。我自己的取舍标准很简单如果这个表是点查多按某个 key get那就放心做散列前缀如果这个表是范围扫描多按时间范围列数据那就尽量保留原始顺序另想办法缓解热点比如增加 Region 数量。功能决定 RowKeyRowKey 决定写入模型顺序不能反。6.2 预分区用一次手动规划省下长期的热点运维HBase 表的 Region 起始只有一个如果表建完后靠自动 split突增的写入压力会先集中在这个 Region 上自动 split 的触发又有滞后这段时间就是同步延迟的温床。为了规避这个窗口数据量能预估的情况下建议直接预分区。假设你要同步的源表有 1000 万行RowKey 是 hash 过的整数前缀那么可以把 RowKey 空间按 hash 范围切成 20 个区间每段一个 Region。建表的时候指定 SPLITSHBase 会一次性把 Region 都建好后续写入均匀分布到各个 Region 上不用等自动 split。create your_table, f, SPLITS [1,2,3,4,5,6,7,8,9]上面这个只是示意实际切分边界要根据你 RowKey 的取值空间来定。预分区不一定一次到位但比什么规划都不做、全靠 HBase 自动扩容要稳得多。尤其是增量同步持续写入的场景预分区可以说是一项低成本高收益的预防措施。6.3 稳定性的最后一道防线死信、监控与告警再好的配置也挡不住所有意外所以稳定性的最后一道防线是监控与告警。我经历过的比较严重的问题都不是当时立刻爆出来的而是同步进程看似正常实际位点已经不动了下游数据一直用的是昨天的旧值。这种静默故障最危险。针对这类问题我的做法分两层。第一层是进程内兜底在同步程序里把解析失败的消息单独丢到一个“死信表”或专门日志里不要直接阻塞整个消费流程保留现场供排查。第二层是进程外监控盯三个指标消费位点是否持续推进、Put 的平均耗时是否异常升高、连续失败的消息数是否为 0。任何一个指标异常都能拉告警这样至少不用等业务反馈“数据怎么不对”才发现问题。如果你们公司已经有比较成熟的监控体系把这三个指标接进去并不难。没有的话用脚本定时查日志里埋的点位也不算复杂。增量同步这件事做到“出了问题能第一时间知道”基本上就已经及格了。我把 ponytail 用在线上同步任务里一段时间之后最大的感受其实是增量同步本身并不难难的是把每一个环节的假设都验证清楚。你以为配好了 binlog实际没重启你以为 slaveId 没有冲突实际和其他从库撞了你以为 HBase 表建好就能扛写入实际 RowKey 设计不对劲写入全打在一个热点 Region 上。ponytail 只是替你把“消费 Canal、写 HBase”这段路铺平了但这条路前一公里和后一公里的路况还是得自己看清楚。
返回列表