背景
最近在排查FlinkOnYarn作业被人为通过yarn kill之后未清理HDFS临时文件的问题,发现FlinkOnYarn作业启动时候虽然注册了JVM Shutdown Hook,但并没有在该Hook线程中进行相关清理操作,这部分我已进行简单的源码适配,后续发文详解。
本文主要描述已有JVMShutdownHook线程类JvmShutdownSafeguard有意思的用法:防止shutdownHooks线程异常导致JVM不能正常退出。
JVM ShutdownHook
介绍JvmShutdownSafeguard线程类之前,我们先描述下JVM原生ShutdownHook机制。JDK源码Runtime.java类文件有详细的描述。
大概内容如下:通过Runtime.getRuntime().addShutdownHook方法就可以注册一个Hook线程。所谓注册该线程仅仅是进行线程初始化,并不是线程执行。JVM ShutdownHook适用于:
正常退出场景:程序运行完成、kill -15、ctrl + c、System.exit(0)等。
异常退出场景:OOM/RuntimeException造成的程序中断等。
并不适用于强制退出场景:kill -9、Runtime.halt、系统关机/crash等)。
当JVM正常退出或者异常退出的时候,会触发执行之前注册的所有Hook线程,这些Hook线程执行特点:并发且无序、要求线程安全、尽量避免死锁、要求耗时短。一旦Hook线程触发执行,只能通过halt方法强制停止。
通过几个小case可以模拟上述场景,以验证Hook线程触发机制、触发时间。场景完整测试用例见github:
https://github.com/felixzh2020/felixzh-learning-java/tree/master/JVMHookCase/src/main/java/cases
正常退出场景
//添加钩子Runtime.getRuntime().addShutdownHook(new ShutDownHookThread(0));//业务工作work();//正常退出System.out.println("==== normal exit ====");

可以看出,触发了ShutdownHookThread。
异常退出场景
//添加钩子Runtime.getRuntime().addShutdownHook(new ShutDownHookThread(0));//业务工作work();//异常退出throw new RuntimeException("exception");

可以看出,触发了ShutdownHookThread。
强制退出场景
//添加钩子Runtime.getRuntime().addShutdownHook(new ShutDownHookThread(0));//业务工作work();//模拟常驻Thread.sleep(600_000);
将打包jar拷贝到Linux机器,启动之后,通过kill -9 杀掉进程。

可以看出,ctrl+c可以触发,但是kil -9 不能触发ShutdownHookThread。
所以,如果通过kill -9强制停止进程,想通过ShutdownHook进行清理操作并不可行。
JvmShutdownSafeguard
言归正传,我们回到正题,看下Flink中JvmShutdownSafeguard实现机制。该类的源码属于flink-runtime模块,位于:
flink\flink-runtime\src\main\java\org\apache\flink\runtime\util\JvmShutdownSafeguard.java
该类为Hook线程类,所以继承了Thread类。
考虑上述提到的Hook线程类的特点,该类在run方法中另拉起一个Daemon线程。该Daemon线程执行一段时间sleep(即容忍的JVM退出超时时间),一旦超时不管其他Hook线程是否执行完成,通过调用Runtime.getRuntime().halt方法强制关闭JVM。
通过这样的方法保证JVM的关闭退出是可控的、不受Hook线程异常或者死锁之类的影响。
关键代码如下:
// JvmShutdownSafeguard线程run方法@Overridepublic void run() {// Because this thread is registered as a shutdown hook, we cannot// wait here and then call for termination. That would always delay the JVM shutdown.// Instead, we spawn a non shutdown hook thread from here.// That thread is a daemon, so it does not keep the JVM alive.terminator.start();}
//Daemon线程run方法@Overridepublic void run() {try {Thread.sleep(delayMillis);} catch (Throwable t) {// catch all, including thread death, etc}Runtime.getRuntime().halt(EXIT_CODE);}
JvmShutdownSafeguard线程类超时保护机制可以用于任何使用ShutdownHook的项目中,比如我将其应用到上述描述的正常退出场景:
//添加钩子,Hook线程sleep 10sRuntime.getRuntime().addShutdownHook(new ShutDownHookThread(10));//增加超时保护,超时5s,强制关闭JVMRuntime.getRuntime().addShutdownHook(new JvmShutdownSafeguard(5));//业务工作work();//正常退出System.out.println("==== normal exit ====");

可以看出:ShutdownHookThread线程还没结束(结束会打印finish),JvmShutdownSafeguard超时保护机制已经触发,强制关闭了JVM。
后续
本文梳理JVM ShutdownHook机制和Flink基于该机制实现的JVM退出时的超时保护机制。后续再深入介绍基于该机制我们如何让FlinkOnYarn作业被yarn kill时自动清理临时文件。
温馨提示:微信公众号私信有时效性、过期不可回复,很多朋友私信我没及时看到、未能回复。最近新建钉钉群(44703541),欢迎朋友加入!




