JDK ShutdownHook - 优雅地停止服务
一、什么是ShutdownHook?
在Java程序中可以通過(guò)添加關(guān)閉鉤子,實(shí)現(xiàn)在程序退出時(shí)關(guān)閉資源、平滑退出的功能。?
使用Runtime.addShutdownHook(Thread hook)方法,可以注冊(cè)一個(gè)JVM關(guān)閉的鉤子,這個(gè)鉤子可以在以下幾種場(chǎng)景被調(diào)用:?
1. 程序正常退出?
2. 使用System.exit()?
3. 終端使用Ctrl+C觸發(fā)的中斷?
4. 系統(tǒng)關(guān)閉?
5. 使用Kill pid命令干掉進(jìn)程
?
Runtime.java中相關(guān)方法源碼
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進(jìn)程平滑退出的意義
很多時(shí)候,我們會(huì)有這樣的一些場(chǎng)景,比如說(shuō)nginx反向代理若干個(gè)負(fù)載均衡的web容器,又或者微服務(wù)架構(gòu)中存在的若干個(gè)服務(wù)節(jié)點(diǎn),需要進(jìn)行無(wú)間斷的升級(jí)發(fā)布。?
在重啟服務(wù)的時(shí)候,除非我們?nèi)プ兏黱ginx的配置,否則重啟很可能會(huì)導(dǎo)致正在執(zhí)行的線(xiàn)程突然中斷,本來(lái)應(yīng)該要完成的事情只完成了一半,并且調(diào)用方出現(xiàn)錯(cuò)誤警告。?
如果能有一種簡(jiǎn)單的方式,能夠讓進(jìn)程在退出時(shí)能執(zhí)行完當(dāng)前正在執(zhí)行的任務(wù),并且讓服務(wù)的調(diào)用方將新的請(qǐng)求定向到其他負(fù)載節(jié)點(diǎn),這將會(huì)很有意義。?
自己注冊(cè)ShutdownHook可以幫助我們實(shí)現(xiàn)java進(jìn)程的平滑退出。
?
三、java進(jìn)程平滑退出的思路
?
四、如何屏敝第三方組件的ShutdownHook
我們會(huì)發(fā)現(xiàn),有一些第三方組件在代碼中注冊(cè)了關(guān)閉自身資源的ShutdownHook,這些ShutdownHook對(duì)于我們的平滑退出有時(shí)候起了反作用。?
比如dubbo,在static方法塊里面注冊(cè)了自己的關(guān)閉鉤子,完全不可控。在進(jìn)程退出時(shí)直接就把長(zhǎng)連接給斷開(kāi)了,導(dǎo)致當(dāng)前的執(zhí)行線(xiàn)程無(wú)法正常完成,源碼如下:
從Runtime.java和ApplicationShutdownHooks.java的源碼中,我們看到并沒(méi)有一個(gè)可以遍歷操作shutdownHook的方法。?
Runtime.java僅有的一個(gè)removeShutdownHook的方法,對(duì)于未寫(xiě)線(xiàn)程名的匿名類(lèi)來(lái)說(shuō),無(wú)法獲取對(duì)象的引用,也無(wú)法分辨出彼此。?
ApplicationShutdownHooks.java不是public的,類(lèi)中的hooks也是private的。?
只有通過(guò)反射的方式才能獲取并控制它們。定義ExcludeIdentityHashMap類(lèi)來(lái)幫助我們阻止非自己的ShutdownHook注入
通過(guò)反射的方式注入自己的ShutdownHook并清除其他Thread
五、實(shí)現(xiàn)服務(wù)的平滑退出
對(duì)于一般的微服務(wù)來(lái)說(shuō),有這幾種任務(wù)的入口:Http請(qǐng)求、dubbo請(qǐng)求、RabbitMQ消費(fèi)、Quartz任務(wù)
5.1 Http請(qǐng)求
測(cè)試發(fā)現(xiàn)Jetty容器在stop的時(shí)候不能實(shí)現(xiàn)平滑退出,springboot默認(rèn)使用的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請(qǐng)求
嘗試了許多次,看了相關(guān)的源碼,dubbo不支持平滑退出;解決方法只有一個(gè),那就是修改dubbo的源碼,以下兩個(gè)地址有詳細(xì)介紹:?
http://frankfan915.iteye.com/blog/2254097?
https://my.oschina.net/u/1398931/blog/790709
5.3 RabbitMQ消費(fèi)
以下是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任務(wù)
quartz也比較簡(jiǎn)單
Scheduler scheduler = applicationContext.getBean(Scheduler.class); scheduler.shutdown(true);
六、為何重啟時(shí)有時(shí)會(huì)有ClassNotFoundException
springboot通過(guò)java -jar example.jar的方式啟動(dòng)項(xiàng)目,在使用腳本restart的時(shí)候,首先覆蓋舊的jar包,然后stop舊線(xiàn)程,啟動(dòng)新線(xiàn)程,這樣就可能會(huì)出現(xiàn)此問(wèn)題。因?yàn)樵趕top的時(shí)候,ShutdownHook線(xiàn)程被喚醒,在其執(zhí)行過(guò)程中,某些類(lèi)(尤其是匿名類(lèi))還未加載,這時(shí)候就會(huì)通知ClassLoader去加載;ClassLoader持有的是舊jar包的文件句柄,雖然新舊jar包的名字路徑完全一樣,但是ClassLoader仍然是使用open著的舊jar包文件,文件已經(jīng)找不到了,所以類(lèi)加載不了就ClassNotFound了。
如何解決呢?也許有更優(yōu)雅的方式,但是我沒(méi)有找到;但是我們可以簡(jiǎn)單地把順序調(diào)整一下,先stop、再copy覆蓋、最后start,這樣就OK了。
?
總結(jié)
以上是生活随笔為你收集整理的JDK ShutdownHook - 优雅地停止服务的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 3分钟了解dubbo服务调试管理实用命令
- 下一篇: 如何在一分钟内搞定面试官?