我们希望任何程序的优雅退出,都需要在退出前做好收尾工作,比如我们在关机时,一般都是会按部就班的关机,如果长时间没响应了才会直接去按关机键强制关机。所以我们希望在退出任何程序前,都要先做好相应的收尾工作,那么退出程序一般会分为这几步:
- 切断上游流量入口,确保不再有流量进入到当前节点
- 向应用发送kill 命令,在设定的时间内待应用正常关闭
- 若超时后应用仍然存活,则使用kill -9命令强制关闭
- *** 作系统层面,提供了 kill -9 (SIGKILL)和 kill -15(SIGTERM) 两种停机策略;
- 语言层面,Java 应用有 JVM shutdown hook,让我们有能力在JVM关闭前做一些收尾处理工作;
- 框架层面,Spring的spring-context模块提供了ContextClosedEvent事件,Spring Boot 的 actuator组件提供了Endpoint;
- 容器层面,docker:在执行docker stop 命令时,容器内的进程会收到 SIGTERM 信号,那么 Docker Daemon 会在 10s 后,发出 SIGKILL 信号;k8s 在管理容器生命周期阶段中提供了 prestop 钩子方法
- 系统层面,系统层面就更复杂了,尤其在分布式框架中:将服务节点从注册中心摘除,订阅者接收通知,移除节点,从而优雅停机
- 数据库,可以使用事务的 ACID 特性,需要通过日志和一套完善的崩溃恢复机制来保证即使 crash 停机也能保证不出现异常数据
- 消息队列, 副本机制 +持久化
我们知道在linux中,想有两个关闭进程的命令是:
kill -9 [PID] kill [PID]
那kill背后是什么呢?
实际上,kill命令,其实就是在Linux中发送一个信号,而信号(Signal)其实就是 Linux 进程收到的一个通知。信号有很多种,比如:
- 如果我们按下键盘“Ctrl+C”,当前运行的进程就会收到一个信号 SIGINT 而退出;
- 如果我们的代码写得有问题,导致内存访问出错了,当前的进程就会收到另一个信号 SIGSEGV;
- 我们也可以通过命令 kill ,直接向一个进程发送一个信号,缺省情况下不指定信号的类型,那么这个信号就是 SIGTERM。也可以指定信号类型,比如命令 “kill -9 ”, 这里的 9,就是编号为 9 的信号,SIGKILL 信号。
事实上,Linux 有 31 个基本信号,进程在处理大部分信号时有三个选 择:忽略、捕获和缺省行为。其中两个特权信号 SIGKILL 和 SIGSTOP 不能被忽略或者捕获。那么进程一旦收到 SIGKILL,就要退出。
而kill -9是立刻退出,甚至没办法处理后续事宜,更优雅的做法显然是kill+sleep一定的超时时间,如果还没退出,这时候再强制kill -9。
容器の优雅退出 Docker容器有个很神奇的现象:你没办法通过kill pid 1的方式来重启容器。
对于Dokcer容器,1号进程是它的init 进程,而Linux 内核针对每个 Nnamespace 里的 init 进程,把只有 default handler(就是上面说的Linux在处理信号的时候,捕获(Catch),就是指让用户进程可以注册自己针对这个信号的 handler) 的信号都给忽略了。如果我们自己注册了信号的 handler(应用程序注册信号 handler 被称作"Catch the Signal"),那么这个信号 handler 就不再是 SIG_DFL ,即使是 init 进程在接收到 SIGTERM 之后也是可以退出的。不过,SIGKILL又 是不允许被注册用户 handler 的(还有一 个不允许注册用户 handler 的信号是 SIGSTOP),所以 init 进程是永远不能被 SIGKILL 所杀,但是可以被 SIGTERM 杀死,当然你要先注册 SIGTERM 的 handler。
不过,docker的docker stop 命令显然更加友好,当执行这个命令,容器内的进程会收到 SIGTERM 信号,Docker Daemon 会等待10s ,10s后如果docker还没退出,就直接发出 SIGKILL 信号强制关闭。
JVMの优雅退出 java.lang.System#exit java.lang.System#exit 方法是Java提供的能够停止JVM进程的方法,该方法被触发时,JVM会去调用Shutdown Hook(关闭钩子)方法,直到所有勾子方法执行完毕,才会关闭JVM进程。exit源码如下:
public static void exit(int status) { Runtime.getRuntime().exit(status); }
这个方法的入参status代表终止状态,如果是0代表正常终止,此外都是非正常终止。一旦调用该方法,永远也不会从该方法正常返回:执行完该方法后JVM进程就直接关闭了。
而这个方法又继续调用了Shutdown.exit(status):
static void exit(int status) { ... synchronized (Shutdown.class) { sequence(); //关闭JVM halt(status); }
而sequence方法中就是去调用java.lang.Shutdown#runHooks:
private static void runHooks() { for (int i=0; i < MAX_SYSTEM_HOOKS; i++) { try { Runnable hook; synchronized (lock) { // 这里加锁的目的是为了保证运行钩子期间,hooks的可见性 currentRunningHook = i; hook = hooks[i]; } //获取到了钩子类,然后就会去执行钩子的run方法 if (hook != null) hook.run(); } catch(Throwable t) { if (t instanceof ThreadDeath) { ThreadDeath td = (ThreadDeath)t; throw td; } } } }ShutdownHook源码解读
Spring提供了钩子的机制,如果我们的程序不是web应用程序,可以通过向Spring注册关闭的钩子,来实现在关闭JVM之前,对我们想要回收的资源进行关闭前的收尾工作。Spring的钩子在JDK的基础类库包rt.jar中,全类路径是:java.lang.Shutdown,这个类中持有了钩子的数组,主要代码如下:
class Shutdown { // hooks数组是一种插槽式的注册方式,目前共有3类:0是Console restore hook,1是应用级的钩子:Application hooks;2是DeleteonExit hook,是HotSpot VM上的shutdown钩子 private static final int MAX_SYSTEM_HOOKS = 10; private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS]; static void add(int slot, boolean registerShutdownInProgress, Runnable hook) static void exit(int status) static void shutdown() private static void sequence() private static void runHooks() ... }
接下来看下ApplicationShutdownHooks,也就是应用级关闭钩子类,这个类的主要代码:
class ApplicationShutdownHooks { private static IdentityHashMaphooks; static { //在静态代码块中,就往Shutdown的hooks数组中添加本类,run方法就是调用本类注册的hooks中的钩子方法;注意这个时候只是添加,线程并没有跑起来; try { Shutdown.add(1 ,false , new Runnable() { public void run() { runHooks(); } } ); hooks = new IdentityHashMap<>(); } catch (IllegalStateException e) { hooks = null; } } //添加钩子,把钩子添加到本类的hooksMap中 static synchronized void add(Thread hook) //移除钩子,从本类的Map中移除 static synchronized boolean remove(Thread hook) //遍历本类的hooksMap,运行它们的start方法,等待它们执行完 static void runHooks() }
当然,在Shutdown类的add方法给hooks赋值的时候也加了锁:
static void add(int slot, boolean registerShutdownInProgress, Runnable hook) { synchronized (lock) { ... hooks[slot] = hook; } }
在上面java.lang.Shutdown#runHooks方法中我们看到它实际上是调用了钩子类的run方法,就会遍历到ApplicationShutdownHooks,所以实际上是执行java.lang.ApplicationShutdownHooks#runHooks方法:
static void runHooks() { Collectionthreads; synchronized(ApplicationShutdownHooks.class) { threads = hooks.keySet(); hooks = null; } //并发执行我们注册到ApplicationShutdownHooks中的钩子方法,每个钩子都是个线程 for (Thread hook : threads) { hook.start(); } //阻塞直到所有的钩子方法都执行结束,如果抛出异常也忽略掉 for (Thread hook : threads) { while (true) { try { hook.join(); break; } catch (InterruptedException ignored) { } } } } }
而在Shutdown.exit(status)方法就会等所有的钩子方法执行完毕再真正的关闭JVM。
ShutdownHook注册方式 ConfigurableApplicationContext.registerShutdownHook 在程序刚启动,初始化上下文的时候,就会通过ConfigurableApplicationContext注册ShutdownHook
@Override public void registerShutdownHook() { if (this.shutdownHook == null) { // 这个时候shutdownHook==null,所以会创建name是SpringContextShutdownHook的线程,到时候由这个线程调用doClose()方法:发布ContextClosedEvent,执行bean的 destroyBeans(),销毁bean等等。 this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) { @Override public void run() { synchronized (startupShutdownMonitor) { doClose(); } } }; //调用Runtime的addShutdownHook将钩子注册进去 Runtime.getRuntime().addShutdownHook(this.shutdownHook); } }Runtime.getRuntime().addShutdownHook
测试代码:
public static void main(String[] args) throws InterruptedException { System.out.println(Thread.currentThread().getName()+"==========begin=========="); Thread shunDownThread = new Thread(() -> { System.out.println(Thread.currentThread().getName() + "==========我是ShutdownHook" + ",我被执行了=========="); }); Runtime.getRuntime().addShutdownHook(shunDownThread); System.out.println(Thread.currentThread().getName()+"==========end=========="); }
打断点可以发现,在main方法结束后,JVM会调用DestroyJavaVM来调用钩子方法,然后DestroyJavaVM会一直阻塞直到所有的钩子方法都运行完毕,然后就会关闭JVM。
关于DestroyJavaVM以下内容摘自《Java性能优化权威指南》
如果HotSpot VM启动过程中发生错误,启动器则调用DestroyJavaVM方法关闭HotSpot VM。如果HotSpot VM启动后执行过程中发生很严重的错误,也会调用DestroyJavaVM方法。如果main方法结束,也会调用DestroyJavaVM方法。 DestroyJavaVM按以下步骤停止HotSpot VM。 (1)一直等待,直到只有一个非守护的线程执行,注意此时HotSpot VM仍然可用。 (2)调用java.lang.Shutdown.shutdown(),它会调用Java上的shutdown钩子方法,如果finalization-on-exit为true,则运行Java对象的finalizer。 (3)运行HotSpot VM上的shutdown钩子(通过JVM_onExit()注册),停止以下线程:性能分线器、统计数据抽样器、监控线程及垃圾收集器线程。发出状态事件通知JVMTI,然后关闭JVMTI、停止信号线程。 (4)调用HotSpot的JavaThread::exit()释放JNI处理块,移除保护页,并将当前线程从已知线程队列中移除。从这时起,HotSpot VM就无法执行任何Java代码了。 (5)停止HotSpot VM线程,将遗留的HotSpot VM线程带到安全点并停止JIT编译器线程。 (6)停止追踪JNI,HotSpot VM及JVMTI屏障。 (7)为哪些仍然以本地代码运行的线程设置标记“vm exited”。 (8)删除当前线程。 (9)删除或移除所有的输入/输出流,释放PerfMemory(性能统计内存)资源。 (10)最后返回到调用者。线程池の优雅关闭 ThreadPoolExecutor shutdown+awaitTermination
ThreadPoolExecutor类中有这样两个方法:
//关闭线程池,拒绝接收新任务,执行正在运行的任务。 public void shutdown() //通过 Thread.interrupt,去停止正在执行的方法,如果线程不响应中断就不会被停止;停止正在等待的任务的处理;并返回正在等待执行的任务的列表。 public ListshutdownNow()
相对而言,shutdown显然是更优雅的关闭线程池的方法,但这有一个问题,如果任务空跑导致没办法结束怎么办?这就需要我们配合awaitTermination方法,给它设置超时时间。
ThreadPoolTaskExecutor 使用Spring提供的线程池ThreadPoolTaskExecutor,先将线程池作为Bean给Spring管理:
@Configuration public class BeanConfiguration { @Bean ThreadPoolTaskExecutor threadPool(){ ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); //省略配置 //线程池销毁前调用shutDown方法 threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true); //设置超时时间 threadPoolTaskExecutor.setAwaitTerminationMillis(3000); return threadPoolTaskExecutor; } }
对于ThreadPoolTaskExecutor,setWaitForTasksToCompleteOnShutdown+setAwaitTerminationMillis就相当于ThreadPoolExecutor的shutdown+awaitTermination。
JVM与线程池 但是到这里也还没结束,我们在线程池中使用到了其他资源,比如我们链接Redis查询了数据,然后这个时候恰巧JVM关闭了,那又怎么处理?
经过上面的学习,我们知道JVM的关闭,当JVM接收到kill命令:
- 调用DestroyJavaVM方法,调用java.lang.Shutdown.shutdown()
- 等待Shutdown Hooks执行完毕便可以正常关机;
但是上面提到有一个在Spring启动时注册的钩子:SpringContextShutdownHook,它会对Spring创建的单例Bean进行销毁,调用它们的DisposableBean钩子方法;发布ContextClosedEvent等。那刚刚我们提到的问题:如果我们强制关闭JVM,此时线程池里的任务与Spring Shutdhwon Hook并发地执行,一旦任务执行期,线程池执行的任务所依赖的资源先行被释放,那任务执行时必然会报错,比如我们在线程池中调用了Redis,如果我们的线程池还没关闭,Redis链接先被回收,这个时候就会抛出异常。
解决思路 刚的问题本质上是在退出JVM的时候销毁bean与关闭连接池是同时的,那就有可能先回收了资源,再关闭连接池。如果我们可以在关闭JVM的时候先关闭连接池,再回收资源就可以不会有问题了。所以我们在Spring容器销毁前,或者在bean销毁前关闭线程池就可以了:
- 监听 ContextClosedEvent事件,对我们的线程池做处理
applicationContext.addApplicationListener(new ApplicationListener() { @Override public void onApplicationEvent(ApplicationEvent applicationEvent) { if (applicationEvent instanceof ContextClosedEvent) { //关闭线程池 } } });
- 使用 DisposableBean 接口
public class TestDisposableBean implements DisposableBean { @Override public void destroy() throws Exception { //关闭线程池 } }
- 使用 @PreDestroy 注解
public class TestPreDestroy { @PreDestroy public void preDestroy(){ //关闭线程池 } }
- …
部分内容摘自:
http://www.concurrentaffair.org/2005/12/22/destroyjavavm/
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)