一、背景与问题
在实时计算场景中,Flink 常通过 HBase Connector 与 HBase 进行数据交互,用于维表关联、结果写入等业务场景。HBase 作为分布式列存储数据库,其数据按 Region 划分并分布在不同 RegionServer 上,HBase 的 region 是数据分布和负载均衡的基本单元,随着数据写入或运维操作,region 会分裂、合并或迁移。为了提升元数据访问效率,HBase 客户端会缓存 meta 表的路由信息(即 MetaCache),避免每次请求都查询 meta 表。
去年我们完成了Flink版本的升级(1.14->1.16)与任务迁移,在迁移初期,我们收到用户反馈有部分Flink任务总是在凌晨2-4点间失败,重新拉起又恢复正常,异常时间很有规律。
二、问题排查
1.排查失败的任务日志发现,任务失败停止前,TM日志均报错org.apache.hadoop.hbase.NotServingRegionException,同时伴随 Unable to load exception received from server的异常跟随日志(截图不清晰,看看大概轮廓)。
2.查看任务失败时间段的运行指标监控,除了重启次数、lag延迟等引发的结果现象,其他无明显异常。
3.与用户沟通Flink任务的处理逻辑与使用场景,了解到这些任务都使用了到了 HBase 维表,且这些HBase表每天凌晨2-4点会通过跑批任务进行全量刷新(overwrite 逻辑),即删除前一天的 表region 并生成新的 表region。通过日志发现,任务失败的时间点与每天定时跑批、清理旧 表Region 的时间点高度吻合。
4.详细排查Flink容错重试的几次NotServingRegionException日志发现,在HBase表数据每天跑批全量更新后,FLink任务重试几次仍然不断地去请求HBase表的旧RegionId,而没有刷新cache重新查询meta表元数据。基于上述排查现象,此问题初步判断是 HBase 客户端的 Meta 缓存未及时更新,导致访问了已失效的 Region,直至Flink任务重试失败停止。
三、问题分析
1.HBase客户端MetaCache机制分析:HBase 客户端在建立 Connection时会缓存 meta 表中的 region 信息,存储在 MetaCache中,这个缓存默认不会主动过期,只有当客户端收到某些特定异常(如 NotServingRegionException)时,才会触发缓存刷新并重试请求。
2.HBase客户端异常处理分析:当服务端返回 NotServingRegionException时,客户端本应触发缓存刷新逻辑,但日志显示并未执行,而是直接抛出了 DoNotRetryIOException。进一步检查客户端对服务端返回异常的处理逻辑。在 ConnectionImplementation中,有一个方法用于将服务端抛出的远程异常转换为本地异常,如下图:
服务端返回的异常类型名称为 org.apache.hadoop.hbase.NotServingRegionException。客户端尝试用 Class.forName加载这个类,如果失败则捕获 ClassNotFoundException,最终包装成 DoNotRetryIOException抛出。DoNotRetryIOException是一个不可重试异常,客户端收到后会直接失败,不会刷新 MetaCache。
3.ClassNotFoundException分析:在 Flink 任务中使用的是 Flink 官方提供的 HBase Connector,该 Connector 为了避免与 Hadoop/HBase 客户端产生依赖冲突,对 HBase 客户端进行了 relocation(包重定向)。原本的 org.apache.hadoop.hbase包被 shade 成了 org.apache.flink.hbase.shaded.org.apache.hadoop.hbase。因此,当客户端尝试加载原始类名 org.apache.hadoop.hbase.NotServingRegionException时,自然找不到,抛出 ClassNotFoundException。任务日志中确实打印了 “Unable to load exception received from server” ,刚好印证了这一推断。
四、解决方案
问题的核心是客户端在运行时无法加载到服务端返回的异常类,最简单的办法就是让 Flink 任务的 classpath 中包含原始的 HBase 客户端类,我们正是采用这种办法,添加后经过数据测试验证,客户端能够成功加载原始异常类,进入正常的重试逻辑,MetaCache 得以刷新,任务恢复稳定。
另外,为避免包冲突的潜在隐患,提升整体Flink任务的健壮性,我们同时制定了以下长期优化方案:
1.缓存主动刷新:在 Flink 任务中,定期调用 HBase 客户端的 clearMetaCache()方法,主动清空缓存,强制从 Meta 表拉取最新 Region 信息。
2.HBase批量卸数优化:将全量卸数调整为增量卸数,避免阻塞HBase表数据请求服务。
3.HBase-connector源码二开:优化完善客户端异常处理逻辑、优化shade配置等,完成定制化优化。
五、总结
在 Flink 与外部系统(如 Kafka、HDFS、Redis)交互的场景中,都可能遇到因依赖冲突、缓存失效或异常处理不当导致的问题,我们要从现象入手,结合日志监控和组件技术原理,必要时配合源码分析,层层递进定位到根因,才能知其然知其所以然,达到自主掌控。