JDK ShutdownHook - 优雅地停止服务
一、什么是ShutdownHook?
在Java程序中可以通過添加關閉鉤子,實現(xiàn)在程序退出時關閉資源、平滑退出的功能。?
使用Runtime.addShutdownHook(Thread hook)方法,可以注冊一個JVM關閉的鉤子,這個鉤子可以在以下幾種場景被調用:?
1. 程序正常退出?
2. 使用System.exit()?
3. 終端使用Ctrl+C觸發(fā)的中斷?
4. 系統(tǒng)關閉?
5. 使用Kill pid命令干掉進程
?
Runtime.java中相關方法源碼
public void addShutdownHook(Thread hook) {SecurityManager sm = System.getSecurityManager();if (sm != null) {sm.checkPermission(new RuntimePermission("shutdownHooks"));}ApplicationShutdownHooks.add(hook); }public boolean removeShutdownHook(Thread hook) {SecurityManager sm = System.getSecurityManager();if (sm != null) {sm.checkPermission(new RuntimePermission("shutdownHooks"));}return ApplicationShutdownHooks.remove(hook); }
ApplicationShutdownHooks.java
class ApplicationShutdownHooks {/* The set of registered hooks */private static IdentityHashMap<Thread, Thread> hooks;static {try {Shutdown.add(1 /* shutdown hook invocation order */,false /* not registered if shutdown in progress */,new Runnable() {public void run() {runHooks();}});hooks = new IdentityHashMap<>();} catch (IllegalStateException e) {// application shutdown hooks cannot be added if// shutdown is in progress.hooks = null;}}private ApplicationShutdownHooks() {}/* Add a new shutdown hook. ?Checks the shutdown state and the hook itself,* but does not do any security checks.*/static synchronized void add(Thread hook) {if(hooks == null)throw new IllegalStateException("Shutdown in progress");if (hook.isAlive())throw new IllegalArgumentException("Hook already running");if (hooks.containsKey(hook))throw new IllegalArgumentException("Hook previously registered");hooks.put(hook, hook);}/* Remove a previously-registered hook. ?Like the add method, this method* does not do any security checks.*/static synchronized boolean remove(Thread hook) {if(hooks == null)throw new IllegalStateException("Shutdown in progress");if (hook == null)throw new NullPointerException();return hooks.remove(hook) != null;}/* Iterates over all application hooks creating a new thread for each* to run in. Hooks are run concurrently and this method waits for* them to finish.*/static void runHooks() {Collection<Thread> threads;synchronized(ApplicationShutdownHooks.class) {threads = hooks.keySet();hooks = null;}for (Thread hook : threads) {hook.start();}for (Thread hook : threads) {try {hook.join();} catch (InterruptedException x) { }}}
}
二、java進程平滑退出的意義
很多時候,我們會有這樣的一些場景,比如說nginx反向代理若干個負載均衡的web容器,又或者微服務架構中存在的若干個服務節(jié)點,需要進行無間斷的升級發(fā)布。?
在重啟服務的時候,除非我們去變更nginx的配置,否則重啟很可能會導致正在執(zhí)行的線程突然中斷,本來應該要完成的事情只完成了一半,并且調用方出現(xiàn)錯誤警告。?
如果能有一種簡單的方式,能夠讓進程在退出時能執(zhí)行完當前正在執(zhí)行的任務,并且讓服務的調用方將新的請求定向到其他負載節(jié)點,這將會很有意義。?
自己注冊ShutdownHook可以幫助我們實現(xiàn)java進程的平滑退出。
?
三、java進程平滑退出的思路
?
四、如何屏敝第三方組件的ShutdownHook
我們會發(fā)現(xiàn),有一些第三方組件在代碼中注冊了關閉自身資源的ShutdownHook,這些ShutdownHook對于我們的平滑退出有時候起了反作用。?
比如dubbo,在static方法塊里面注冊了自己的關閉鉤子,完全不可控。在進程退出時直接就把長連接給斷開了,導致當前的執(zhí)行線程無法正常完成,源碼如下:
從Runtime.java和ApplicationShutdownHooks.java的源碼中,我們看到并沒有一個可以遍歷操作shutdownHook的方法。?
Runtime.java僅有的一個removeShutdownHook的方法,對于未寫線程名的匿名類來說,無法獲取對象的引用,也無法分辨出彼此。?
ApplicationShutdownHooks.java不是public的,類中的hooks也是private的。?
只有通過反射的方式才能獲取并控制它們。定義ExcludeIdentityHashMap類來幫助我們阻止非自己的ShutdownHook注入
通過反射的方式注入自己的ShutdownHook并清除其他Thread
五、實現(xiàn)服務的平滑退出
對于一般的微服務來說,有這幾種任務的入口:Http請求、dubbo請求、RabbitMQ消費、Quartz任務
5.1 Http請求
測試發(fā)現(xiàn)Jetty容器在stop的時候不能實現(xiàn)平滑退出,springboot默認使用的tomcat容器可以,以下是部分代碼示例:
EmbeddedWebApplicationContext embeddedWebApplicationContext = (EmbeddedWebApplicationContext) applicationContext; EmbeddedServletContainer embeddedServletContainer = embeddedWebApplicationContext.getEmbeddedServletContainer(); if (embeddedServletContainer instanceof TomcatEmbeddedServletContainer) {Connector[] connectors = tomcatEmbeddedServletContainer.getTomcat().getService().findConnectors();for (Connector connector : connectors) {connector.pause();}for (Connector connector : connectors) {Executor executor = connector.getProtocolHandler().getExecutor();if (executor instanceof ThreadPoolExecutor) {try {ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;threadPoolExecutor.shutdown();if (!threadPoolExecutor.awaitTermination(5, TimeUnit.SECONDS)) {log.warn("Tomcat thread pool did not shutdown gracefully within 5 seconds. Proceeding with forceful shutdown");}} catch (InterruptedException e) {log.warn("TomcatShutdownHook interrupted", e);}}} }
5.2 dubbo請求
嘗試了許多次,看了相關的源碼,dubbo不支持平滑退出;解決方法只有一個,那就是修改dubbo的源碼,以下兩個地址有詳細介紹:?
http://frankfan915.iteye.com/blog/2254097?
https://my.oschina.net/u/1398931/blog/790709
5.3 RabbitMQ消費
以下是SpringBoot的示例,不使用Spring原理也是一樣的
RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry = applicationContext.getBean(RabbitListenerConfigUtils.RABBIT_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME,RabbitListenerEndpointRegistry.class); Collection<MessageListenerContainer> containers = rabbitListenerEndpointRegistry.getListenerContainers(); for (MessageListenerContainer messageListenerContainer : containers) {messageListenerContainer.stop(); }
5.4 Quartz任務
quartz也比較簡單
Scheduler scheduler = applicationContext.getBean(Scheduler.class); scheduler.shutdown(true);
六、為何重啟時有時會有ClassNotFoundException
springboot通過java -jar example.jar的方式啟動項目,在使用腳本restart的時候,首先覆蓋舊的jar包,然后stop舊線程,啟動新線程,這樣就可能會出現(xiàn)此問題。因為在stop的時候,ShutdownHook線程被喚醒,在其執(zhí)行過程中,某些類(尤其是匿名類)還未加載,這時候就會通知ClassLoader去加載;ClassLoader持有的是舊jar包的文件句柄,雖然新舊jar包的名字路徑完全一樣,但是ClassLoader仍然是使用open著的舊jar包文件,文件已經(jīng)找不到了,所以類加載不了就ClassNotFound了。
如何解決呢?也許有更優(yōu)雅的方式,但是我沒有找到;但是我們可以簡單地把順序調整一下,先stop、再copy覆蓋、最后start,這樣就OK了。
?
總結
以上是生活随笔為你收集整理的JDK ShutdownHook - 优雅地停止服务的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 3分钟了解dubbo服务调试管理实用命令
- 下一篇: 如何在一分钟内搞定面试官?