Flink-Hudi生产问题排障-xxx.parquet is not a Parquet file

0 阅读6分钟

一、背景与问题

在实时数据湖架构中,Flink + Hudi 是构建实时入湖的经典技术栈。Flink 负责将实时数据流高效写入 Hudi 表,Hudi 提供 upsert、增量查询及表服务(如 clustering、cleaning)等能力,支撑下游近实时分析。   

在我们的生产环境中,Kafka+Flink+Hudi+Spark的数据链路已经成熟稳定,通过Kafka+Flink将数据实时写入Hudi表,然后利用Spark进行定时跑批做近实时数据统计分析。   

有一个相同链路的业务采用 Flink(1.14) 以 insert模式写入 Hudi(0.13) mor表,下游依赖 Spark 离线分析任务生成业务报表。有一次,下游Spark任务运行失败触发了告警,同时上游Flink任务也触发健康预警,短时间内频繁重启。

二、问题排查

1.下游Spark任务日志排查   

首先,我们定位到报错的 Spark 任务。从异常栈可以清晰看到,任务在尝试读取一个 Parquet 文件时失败,原因是该文件长度为 0,不是一个有效的 Parquet 文件,报错Caused by: java.lang.RuntimeException: hdfs://xx/../41307bff-173e-497a-a1c4-c3bf58f9337c-2_3-4-9_20241211103654416.parquet is not a Parquet file (too small length: 0)。    通过 HDFS 命令检查,确认该文件确实为 0 字节。这直接表明,问题的根源在于上游写入环节产生了损坏的文件。 2.上游Flink任务日志排查   

我们进一步排查了上游数据写入的Flink实时作业,在 TaskManager 的日志中,发现了大量与 Hudi Clustering(文件合并)相关的错误日志:HoodieClusteringException: Error reading input data 与FileNotFoundException,这些日志表明,Flink 作业在执行 Clustering 操作时,也在尝试读取同样的 0 字节文件并失败。这说明 0 字节文件是在 Flink 写入过程中产生的,并干扰了后续的文件合并操作。 3.排查Flink任务参数配置   

我们怀疑Flink任务写入压力大,排查Flink任务的TM配置与任务Hudi配置如下:

kubernetes.taskmanager.cpu:2
taskmanager.memory.process.size:4g
execution.checkpointing.interval:60000
clustering.async.enabled:true
clustering.schedule.enabled:true
clustering.delta_commits:5
write.task.max.size:2048
write.merge.max_memory:1024

结合Flink任务的频繁重启现象,初步排查结论是Flink insert 写入 Hudi 表异常导致任务频繁重启,重启过程中产生了 0 字节的 Parquet 文件,触发了后续查询异常,导致 Flink 作业 JM 和 TM 重启,Spark 查询任务失败。

三、问题分析

1.Flink流式写入Hudi表过程中的异常中断:当 Flink 作业执行 Checkpoint 时,Hudi 的 Writer 负责将数据刷写(Flush)为 Parquet 文件,而 Coordinator 负责提交元数据(Commit)。如果在此过程中发生 TaskManager 崩溃、网络闪断或 Checkpoint 超时失败,可能导致数据文件(.parquet)已被创建,但对应的提交(.commit)并未成功完成。

2.生成不完整或空的 Parquet 文件:在上述异常场景下,文件系统(如 HDFS)上可能会留下一个文件大小为0字节的 Parquet 文件占位符。当后续的查询引擎(如 Presto、Spark 或 Hive)尝试读取该表时,会因无法解析这个空文件而抛出“is not a Parquet file (too small length: 0)”的错误。

3.Flink任务配置分析:TM总内存4g,实际分配给堆内存约1.6g左右;而Hudi-write最大task内存设置为2g,最大合并内存1g,在大规模上游数据量场景下会大大挤占堆内存使用;另外checkpoint间隔配置1min,加上配置的5个commit触发clustering执行计划,平均5min触发一次clustering操作,加剧了堆内存的资源消耗与GC。

基于上述Hudi写入与任务配置分析,推断原因是Flink insert 写入 Hudi 表的资源不足引起反压,进而影响checkpoint导致任务频繁重启而触发此问题。

此外,我们在Hudi官方社区也找到相同问题现象的issue(issue-8674)。 目前这个issue状态仍是open,我们推断问题的根本原因在于 Hudi 在 Flink 流式写入过程中的容错机制存在间隙。

1.Flink 的 Checkpoint 机制保证了作业状态的一致性,但在 Hudi 的写入流程中,数据文件的物理创建和元数据提交是两个解耦的步骤。

2.当故障发生在“数据已刷写到磁盘形成文件”之后,但在“Coordinator 完成提交并使数据可见”之前,这个中间状态的文件就成为了一个孤立的、不完整的数据碎片。Hudi 或 Flink 的清理机制可能没有及时将其回收。

3.这个空文件或损坏文件仍然存在于表的分区目录下,其 .parquet后缀会误导查询引擎尝试读取,从而导致报错。

四、解决方案

因社区暂未针对此issue提供明确的修复方案,我们主要采用 “事前规避 + 事中修复” 的方案来解决此问题。

1.合理配置Flink任务参数与Hudi配置(调大TM内存、提高checkpoint间隔、调大clustering-commit触发阈值),提升资源匹配度、降低问题暴露概率

kubernetes.taskmanager.cpu:2
taskmanager.memory.process.size:6g
execution.checkpointing.interval:120000
clustering.async.enabled:true
clustering.schedule.enabled:true
clustering.delta_commits:10
write.task.max.size:2048
write.merge.max_memory:1024

2.手动清理空文件,清理掉 0 字节的 Parquet 文件及其相关的元数据删除(需要精确定位文件,多次上线修复),这类空文件通常不会导致数据丢失,因为它们代表未成功提交的数据。同时也根据用户场景提供表分区清理与表重建的修复方式,但带来的是分区数据丢失与重建表初始化补数的代价与成本。

最终我们通过完成空文件清理、优化Flink任务配置,重新上线后,任务一直稳定运行,下游查询任务也未再次出现相同问题。另一方面,我们也持续关注社区的issue动态,及时跟进修复已知的重大开源BUG;现在Hudi已经迭代到1.X的版本,稳定性与健壮性得到极大提升,后续也会将版本升级纳入工作计划。

五、总结

我们在使用Flink与Hudi这类相对年轻的组件做集成服务时,要充分做好事前事后保障措施,事前预防(配置调优需谨慎、测试需充分、监控告警需完善、版本跟踪需重视)重于事后补救(有预案、快速止血、工具化沉淀)。   

另一方面,我们要持续加强技术原理的理解与掌控,只有深入理解 Flink 与 Hudi 的交互机制(如 instant 管理、文件提交协议),才能在遇到诡异问题时快速定位根因。生产环境的稳定性来源于对细节的极致追求。每一次故障都是技术成长的机会,通过复盘和举一反三,我们能构建更健壮的数据架构。