Flink生产问题排障-Failed to deserialize consumer record

5 阅读4分钟

一、背景与问题

公司内部基于 Canal + Kafka + Flink + Hudi构建了实时数据入湖链路,Canal实时采集Mysql的Binlog日志推送到Kafka Topic,Flink 作业从 Kafka 消费 JSON 格式数据,经过清洗转换后写入 Hudi 表,支撑近实时分析。(Kafka-2.4、Flink-1.16、Hudi-0.14)

某一天,通过运维监控发现两个关键作业频繁失败自动重启,表现为:

1.作业长时间停滞在某个 Kafka 偏移量(offset),无法继续消费;

2.checkpoint超时失败,同时在 TaskManager 日志中发现大量反序列化报错;

3.手工重启 Flink 作业后,之前卡住的 offset 数据能正常消费并写入 Hudi。

二、排查与分析

1.排查监控与日志Flink作业的运行监控指标除了numRestarts与checkpoint指标异常,其他均正常;查看TM运行日志,发现有大量的反序列化异常日志:Failed to deserialize consumer record due to ...,无法将element发送给下一个算子。 异常信息表明Flink 在解析某条 Kafka 消息的 JSON 内容(offset=167857)时抛出异常,触发作业自动重启从之前的状态点位重新消费后,到达offset=167857这条数据又触发了反序列化的异常报错,如此循环往复。

另一个疑惑点是,当手动重启Flink作业后,Flink又能正常消费offset=167857这条数据写入Hudi表,不过后面也会在其他的offset点位继续卡住,说明数据本身可能并未损坏,而是 Flink 或 Hudi 的内部状态出现了问题。

2.异常数据格式验证通过Kafka抓取到反序列化的offset=167857数据,在测试环境下启动相同Flink作业进行消费验证,能够正常解析写入Hudi表,验证了数据格式无误。

3.根据异常日志堆栈分析Flink源码我们深入 Flink 1.16 源码,梳理了 Kafka 消息反序列化的完整调用链:KafkaRecordEmitter.emitRecord:调用 deserializationSchema.deserialize,若失败则抛出 IOException("Failed to deserialize consumer record due to ", e)。 接着KafkaDeserializationSchemaWrapper.deserialize调用DynamicKafkaDeserializationSchema.deserialize,继续调用ValueDeserialization.deserialize,最后进入到JsonRowDataDeserializationSchema.deserialize方法: 其中 convertToRowData(deserializeToJsonNode(message))是故障高发区,当ignoreParseErrors为false时,以下场景会直接抛出异常:JSON 字符串包含未转义的控制字符、Decimal 类型精度溢出,超出 Flink RowData 的存储范围、非 UTF8 编码的二进制数据,无法正常解析为 JSON等。虽然反序列化异常是直接表象,但数据格式符合规范,且重启后能正常消费,说明异常可能是偶发性的,正常能被 Flink 正确处理。

4.分析Hudi sink源码既然反序列化异常本身不应导致作业永久卡住,我们怀疑问题出在 Hudi Sink 的 checkpoint 机制上。深入 Hudi 源码(AbstractStreamWriteFunction.java)后,我们发现了问题的核心: 当 Flink 作业开启 Exactly-Once 语义时,Hudi Sink 的instantToWrite方法会等待 Checkpoint 确认(即等待 commit 成功)。当某个 checkpoint 对应的 Hudi commit 一直处于 pending 状态(未完成也未失败),此方法会陷入无限等待,导致后续 checkpoint 无法推进,最终整个 Flink 作业超时失败。

另一方面,我们并行查阅了Hudi官方社区文档,发现这个是开源BUG(HUDI-9041)。Flink 的 KafkaConsumer 在 emitRecord中捕获到反序列化异常后,会抛出 IOException,此时作业会根据重启策略尝试重启,但在重启之前,Hudi sink 可能已经部分执行了 checkpoint 操作(例如开启了新 instant),而异常导致该 instant 未能正常完成。当作业重启后,Hudi sink 恢复时,会发现存在一个 pending instant,于是进入上述等待逻辑。正常情况下,Flink 的 checkpoint 完成或失败会通知 sink 做出相应处理。但根据 HUDI-9041的描述,在特定故障恢复场景下,缺少一个“保护性 commit”操作,导致 pending instant 无法被正确清理,从而引起永久阻塞。

三、解决方案

1.临时处置

通过优化重启策略增加重试次数与开启json.ignore-parse-errors为true,提升重试成功概率或不阻塞数据处理主流程(异常数据会过滤,若无法接受可采用人工干预重启方式)。

2.根本解决

按社区HUDI-9041的修复方案进行代码修复,重新出包上线部署。

四、总结

此次排障过程从 “作业重启、offset 卡住” 的现象出发,逐步排查反序列化异常、Flink Kafka Connector 逻辑,最终定位到 Hudi 写入与 Checkpoint 交互的底层 bug。我们在日常运维故障处理中,排障思路要由表及里,先从应用层日志和配置入手,再深入组件源码。通过这次实战,我们不仅解决了一个棘手的生产故障,也为后续保障实时链路的稳定性积累了宝贵经验。