一、背景与问题
基于 Flink + Hudi(0.14)构建的实时入湖链路,我们生产有一个Flink作业消费 Kafka 实时数据,关联 HBase 维表进行数据补全后 Upsert 写入 Hudi表,其中Hudi表是按日期分区、配置了bucket索引。
有一次收到这个作业频繁重启的告警,没过多久这个作业就失败停止了,其中运行日志中报错:
Duplicate fileId xxxx from bucket 3 of partition dt=xxxx found druing xxx
二、排查与分析
1.初步排查
检查Flink作业的异常时间段的运行日志,发现频繁重启是因为上游有一波批处理大流量数据到达引发OOM,触发Flink自身的容错重启策略。随后的运行日志不断报错 Duplicate fileId xxxx from bucket异常,最终达到重试阈值而失败停止。Flink作业的异常重启策略是基于checkpoint的容错机制,结合生产上的历史运行情况看应该不会出现影响作业的正常数据处理,那为什么会引发这个奇怪的异常呢?
从日志的异常堆栈信息判断问题发生在 Hudi Bucket 索引的初始化引导阶段,核心是BucketStreamWriteFunction在构建索引时发现一个bucket桶找到了多个fileId,抛出了此异常。我们去HDFS排查对应Hudi表的目录文件,确实存在一个bucket桶对应5个fileId的现象。
2.原因分析
Hudi-Bucket索引是一种基于哈希的索引机制,根据记录的主键(Record Key)计算哈希值,然后根据哈希值将记录映射到固定的、预定义数量的“桶”(Bucket)中。每个桶直接对应一个File Group(即一个File ID)。这意味着,一条记录通过其主键哈希后,其所有版本的数据(无论新增还是更新)都会被路由到同一个File Group中,这种设计避免了全局索引需要扫描所有文件的性能开销。
在 Hudi 中,Bucket Index 将记录映射到固定的桶(bucket),每个桶对应一个文件组。写入时,需要先对每一条记录执行 tagging,即判断该记录是 update(存在)还是 insert(不存在),决定写入方式。它通过索引查询当前记录的目标文件组 ID,若索引返回该记录已存在,则标记为 update;否则标记为 insert。这一过程依赖于索引状态的准确性。
关键问题在于tagging 过程和后续的实际写入(insert/update)不是原子事务。当作业异常重启,可能发生以下时序:
- 第 1 次运行:tagging 阶段查询索引,认为某分区无数据(所有记录均为 insert),于是生成新的文件组并写入数据,同时更新索引。
- 还未持久化索引时作业崩溃:新写入的数据文件已落盘,但索引更新尚未完成或未 checkpoint。
- 作业重启:Flink 从最近 checkpoint 恢复状态,Hudi writer 重新初始化,再次执行 tagging。由于索引状态回滚到了写入前,因此误认为该分区没有数据,再次将所有记录标记为 insert,并写入新的文件组(同一 bucket 生成新的文件 ID)。
- 最终结果:同一主键的多条记录分散在不同的文件组中,且都标记为 insert,导致逻辑上重复,后续Hudi发现映射问题抛出 DuplicateFileIdException。
基于上述排查思路,我们在Hudi官方社区找到了Issue,确认是开源Bug(HUDI-8123、Issue-11784)。
三、解决方案
1.临时方案
通过将Hudi表删除重建,同时调大Flink作业process资源,降低异常概率。
2.根治方案
该Issue已提供patch,将patch合入现有版本重新打包上线。
四、总结
本次生产故障由 Hudi 底层组件缺陷引发,暴露了在实时入湖场景下,索引状态与写入一致性的重要性。通过本次排障,我们不仅解决了具体的生产问题,更深入理解了 Flink+Hudi 架构下的数据一致性保障机制,为后续实时数仓的稳定性建设提供了宝贵经验。