一、背景与问题
在大数据流式处理领域,乱序一直是一个无法越过的问题,如何正确处理乱序数据也是流式组件不断努力优化的方向,比如FLink提供的watermark机制(forBoundedOutOfOrderness/allowedLateness/sideOutputLateData)也是应对数据延迟乱序的设计。
Hudi作为实时湖存储表格式,提供流式数据的插入、更新、删除能力,自身也对乱序有一定的处理策略,特别是Upsert操作模式下,支持基于precombine.field字段解决同主键数据冲突,基于hoodie.is_deleted字段实现物理删除。
最近,我们遇到了一个头疼的问题,有项目组反馈一张Flink(1.16)实时Upsert写入的Hudi(0.14)表数据量不对,与离线Hive表数据做核验发现少了几万条。
二、问题排查与分析
我们首先检查FLink作业监控与运行日志,发现指标监控与日志均正常,没有任何反压、Exception等问题,排除了作业运行异常导致的数据问题。我们找项目组调研了作业的数据链路与处理逻辑:通过Canal实时采集上游Mysql的Binlog日志到Kafka,Flink消费Kafka以Upsert模式写入Hudi表,为了处理主键冲突配置了业务时间(TMP_TIME)为预合并字段。这是一个简单而常规的实时入湖链路,理论上应该不会有问题。
我们找了其他几张实时Upsert入湖的Hudi表与Hive表数据条数进行核验,误差都在可控范围内(实时与离线难以精准100%对齐),不像出问题的这张表误差有几万条。接着推测会不会是乱序原因导致预合并失效,找到Canal采集项目组确认Kafka-Topic推送策略,对方反馈出于性能与负载均衡考虑采取的轮询推送策略,同时发现该Kafka-Topic分区数为10。从技术上分析,轮询+多分区的写入搭配,确实容易出现同一条主键的数据发往多个分区,出现乱序处理场景。
基于此思路,我们在测试环境做了乱序模拟验证,模拟更新删除乱序下预合并的表现。
- 数据顺序到达或仅插入场景:处理结果符合预期,正常。
- 数据乱序到达仅更新场景:因建表未设置乱序处理策略,默认按照OverwriteWithLatestAvroPayload,即按最新到达覆盖旧数据;设置为EventTimeAvroPayload可按预合并字段值排序处理。
- 数据乱序到达涉及更新删除场景:建表设置乱序处理策略为EventTimeAvroPayload,String与Decimal类型预合并字段因类型不匹配导致处理排序不生效。
我们将问题表的预合并字段类型修改为bigint或timestamp,进行验证后Hudi预合并处理符合预期。接着我们进行端到端数据验证,发现在更新删除场景下仍然存在数据对不上的问题,除了预合并阶段,我们思考是否还有其他环节也存在处理异常呢?
我们查阅官方文档以及源码,Hudi在处理同主键数据的变更合并时,涉及三个关键阶段,每个阶段都可能成为问题的源头:
- 预合并阶段:当多条相同主键的记录同时出现在内存缓冲区时,Hudi会在写入前调用HoodieRecordPayload#preCombine进行去重合并。该过程仅保留预合并字段值最大的记录,此阶段对应调用链:EventTimeAvroPayload.preCombine→ compareTo。
- 读时合并阶段:在MOR表读取时,当Log文件中存在删除记录,而Base Parquet文件中存在对应插入记录时,Hudi需要通过HoodieMergedLogRecordScanner#processNextDeletedRecord来协调二者,该过程需依据预合并字段值判断是保留插入记录还是应用删除记录。
- 合并阶段:在Compaction(表合并)阶段,当Log和Parquet文件中存在同主键记录时,通过EventTimeAvroPayload#combineAndGetUpdateValue进行最终合并。
我们查看Compaction源码,发现在处理delete记录合并时,Hudi以删除记录优先,未进行预合并字段比较排序。到这里,问题表的异常原因基本就清晰了,是Hudi三个合并处理阶段对乱序删除场景下存在逻辑处理异常。上述问题根因汇总如下:
| 序号 | 问题阶段 | 根本原因 | 影响范围 |
|---|---|---|---|
| 1 | 预合并 | String/Decimal类型比较时类不匹配(Utf8 vs String,GenericData.Fixed vs BigDecimal) | 预合并结果不符合预期 |
| 2 | 读时合并 | processNextDeletedRecord中String/Decimal类型比较时类不匹配 | 删除记录与插入记录取舍错误 |
| 3 | 合并 | combineAndGetUpdateValue中删除优先原则未考虑预合并字段值比较 | 删除操作可能误删更新记录 |
三、解决方案
- 最直接的解决方案是修复Hudi在三个合并阶段的源码逻辑BUG,能正常处理相同主键的记录判断与取舍操作。(此问题已记录Issue#17642,修复pr#17713,影响0.14/1.x等多个版本,暂未合入Release。)
- 此问题是相同主键乱序处理引发的,保障数据顺序到达Hudi可以规避。
- Canal采集策略由轮询改造为Hash,保障局部有序。(相同主键数据有序)
- 在Flink写入Hudi前增加排序预处理。(如row_num等)
最终,我们通过修改源码重新编译打包上线解决,灰度验证准出通过;另一方面也在推动整体的Canal采集策略调整(经过理论分析与数据验证,具备可行性)。
四、总结展望
本次生产问题的排查过程,是一次由表及里的诊断剖析(对账-核验-溯源)。Hudi的Payload机制和preCombine机制虽然功能强大,但深入理解其在不同场景下的行为差异至关重要。在后续使用过程中,我们也要不断完善测试用例、监控体系,及时跟进社区技术发展新动态,推动Hudi技术栈的稳定高效使用。