Spring微服务项目实现优雅停机(平滑退出)
为什么要优雅停机(平滑退出)
不论是生产环境还是测试环境,在公布新代码的时候,不可避免的进行我的项目的重启
kill -9 `ps -ef|grep tomcat|grep -v grep|grep server_0001|awk '{print $2}'
以上是我司生产环境停机脚本,能够看出应用了 kill -9 命令把服务过程杀掉了,这个命令是十分暴力的,相似于间接按了这个服务的电源,显然这种形式对进行中的服务是很不友善的,当在停机时,正在进行RPC调用、执行批处理、缓存入库等操作,会造成不可挽回的数据损失,减少前期保护老本。
所以就须要优雅停机出场了,让服务在收到停机指令时,从容的回绝新申请的进入,并执行完当前任务,而后敞开服务。
Java优雅停机(平滑退出)实现原理
linux信号机制
简略来说,信号就是为 linux 提供的一种解决异步事件的办法,用来实现服务的软中断。
服务间能够通过 kill -数字 PID 的形式来传递信号
linux信号表
kill -l
能够通过 kill -l 命令来查看信号列表:
取值 | 名称 | 解释 | 默认动作 |
---|---|---|---|
1 | SIGHUP | 挂起 | |
2 | SIGINT | 中断 | |
3 | SIGQUIT | 退出 | |
4 | SIGILL | 非法指令 | |
5 | SIGTRAP | 断点或陷阱指令 | |
6 | SIGABRT | abort收回的信号 | |
7 | SIGBUS | 非法内存拜访 | |
8 | SIGFPE | 浮点异样 | |
9 | SIGKILL | kill信号 | 不能被疏忽、解决和阻塞 |
10 | SIGUSR1 | 用户信号1 | |
11 | SIGSEGV | 有效内存拜访 | |
12 | SIGUSR2 | 用户信号2 | |
13 | SIGPIPE | 管道破损,没有读端的管道写数据 | |
14 | SIGALRM | alarm收回的信号 | |
15 | SIGTERM | 终止信号 | |
16 | SIGSTKFLT | 栈溢出 | |
17 | SIGCHLD | 子过程退出 | 默认疏忽 |
18 | SIGCONT | 过程持续 | |
19 | SIGSTOP | 过程进行 | 不能被疏忽、解决和阻塞 |
20 | SIGTSTP | 过程进行 | |
21 | SIGTTIN | 过程进行,后盾过程从终端读数据时 | |
22 | SIGTTOU | 过程进行,后盾过程想终端写数据时 | |
23 | SIGURG | I/O有紧急数据达到以后过程 | 默认疏忽 |
24 | SIGXCPU | 过程的CPU工夫片到期 | |
25 | SIGXFSZ | 文件大小的超出下限 | |
26 | SIGVTALRM | 虚构时钟超时 | |
27 | SIGPROF | profile时钟超时 | |
28 | SIGWINCH | 窗口大小扭转 | 默认疏忽 |
29 | SIGIO | I/O相干 | |
30 | SIGPWR | 关机 | 默认疏忽 |
31 | SIGSYS | 零碎调用异样 |
Java通过ShutdownHook钩子接管linux停机信号
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { @Override public void run() { logger.info("========收到敞开指令========"); logger.info("========登记Dubbo服务========"); shutdownDubbo(); logger.info("========登记ActiveMQ服务========"); shutdownActiveMQ(); logger.info("========登记Quartz服务========"); shutdownQuartzJobs(); } }, SHUTDOWN_HOOK));//public static final String SHUTDOWN_HOOK = "Manual-ShutdownHook-@@@"
java提供了以上办法给程序注册钩子(run()办法外部为自定义的清理逻辑),来接管停机信息,并执行停机前的自定义代码。
钩子在以下场景会被触发:
- 程序失常退出
- 应用System.exit()
- 终端应用Ctrl+C触发的中断
- 零碎敞开
- 应用Kill pid命令干掉过程(kill -9 不会触发)
咱们应用的是kill -15 PID命令来触发钩子
定义进行钩子的危险
- 钩子run()办法的执行速度会重大影响服务敞开的快慢
- run()办法内务必保障不会呈现死锁、死循环,否则会导致服务长时间不能失常敞开
Java优雅停机(平滑退出)实现
注册自定义钩子并移除服务默认注册的钩子
下面代码咱们曾经注册了本人的钩子,外面调用了几个停服务的办法,那为什么要删除其余钩子呢
很多服务都会注册本人的钩子,注册的中央能够看出,每个钩子都是一个新的线程,所以当收到敞开指令时,这些钩子之间是并发执行的,一些服务之间的依赖关系会被突破,导致不能按咱们的想法正确的停掉服务。
取出并停掉shutdownhook的办法很简略,ApplicationShutdownHooks类外部保护了IdentityHashMap<Thread, Thread> hooks,外面存着所有已注册的钩子,咱们只须要把他取出来,而后革除掉就能够了
@Override public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { logger.info("========初始化ShutDownHook========"); try { Class<?> clazz = Class.forName(SHUTDOWN_HOOK_CLAZZ);//SHUTDOWN_HOOK_CLAZZ = "java.lang.ApplicationShutdownHooks"; Field field = clazz.getDeclaredField("hooks"); field.setAccessible(true); IdentityHashMap<Thread, Thread> excludeIdentityHashMap = new IdentityHashMap<>(); synchronized (clazz) { IdentityHashMap<Thread, Thread> map = (IdentityHashMap<Thread, Thread>) field.get(clazz); for (Thread thread : map.keySet()) { logger.info("查问到默认hook: " + thread.getName()); if (StringUtils.equals(thread.getName(), SHUTDOWN_HOOK)) {//SHUTDOWN_HOOK = "Manual-ShutdownHook-@@@"; excludeIdentityHashMap.put(thread, thread); } } field.set(clazz, excludeIdentityHashMap); } } catch (Exception e) { logger.info("========初始化ShutDownHook失败========", e); } }
这里应用了该类继承了ApplicationListener<ContextRefreshedEvent>应用onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) 办法,能够在我的项目启动后,对注册的钩子进行清理
shutdownhook实现Dubbo优雅停机(平滑退出)
对于Dubbo的优雅停机,网上七嘴八舌,大部分说法是不反对优雅停机,想反对优雅停机的话,须要批改源码。而且2.6以下的版本,与spring的钩子之间不兼容,导致服务停机会出现异常本司用的Dubbo版本为2.5.6,我批改了Dubbo连贯参数后,在本地测试的话,是能够失常跑完服务并敞开连贯的(实时上是先敞开与注册核心的连贯,而后业务执行结束,敞开提供者与消费者之间的长连贯)
Dubbo在优雅停机(平滑退出)时都干了什么
Dubbo登记残缺代码
private static void shutdownDubbo() { AbstractRegistryFactory.destroyAll(); try { Thread.sleep(NOTIFY_TIMEOUT); } catch (InterruptedException e) { logger.warn("Interrupted unexpectedly when waiting for registry notification during shutdown process!"); } ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class); for (String protocolName : loader.getLoadedExtensions()) { try { Protocol protocol = loader.getLoadedExtension(protocolName); if (protocol != null) { protocol.destroy(); } } catch (Throwable t) { logger.warn(t.getMessage(), t); } } }
先来看看Dubbo在AbstractConfig中本人注册的shutdownhook:
static { Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { public void run() { if (logger.isInfoEnabled()) { logger.info("Run shutdown hook now."); } ProtocolConfig.destroyAll(); } }, "DubboShutdownHook")); }
只是在run()办法中调用了ProtocolConfig.destroyAll()办法
// TODO: 2017/8/30 to move this method somewhere elsepublic static void destroyAll() { if (!destroyed.compareAndSet(false, true)) { return; } AbstractRegistryFactory.destroyAll(); ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class); for (String protocolName : loader.getLoadedExtensions()) { try { Protocol protocol = loader.getLoadedExtension(protocolName); if (protocol != null) { protocol.destroy(); } } catch (Throwable t) { logger.warn(t.getMessage(), t); } }}
AbstractRegistryFactory.destroyAll()
AbstractRegistryFactory.destroyAll()办法的作用是敞开所有已创立注册核心,会调用每个ZkClient的close()办法来从注册核心登记掉
AbstractRegistryFactory.destroyAll()办法执行前
[zk: 2] ls /Dubbo/com.ebiz.ebiz.demo.service.ShutDownHookService/providers [Dubbo%3A%2F%2F10.14.0.221%3A21761%2Fcom.ebiz.ebiz.demo.service.ShutDownHookService%3Fanyhost%3Dtrue%26application%3Dsale-center%26Dubbo%3D2.5.6%26generic%3Dfalse%26interface%3Dcom.ebiz.ebiz.demo.service.ShutDownHookService%26methods%3DdoService%26pid%3D8787%26side%3Dprovider%26timeout%3D50000%26timestamp%3D1615362505995]
AbstractRegistryFactory.destroyAll()办法执行后 (Debug进行在前面一行)
[zk: 3] ls /Dubbo/com.ebiz.ebiz.demo.service.ShutDownHookService/providers []
特地留神的是:
这里只是从注册核心登记掉,并不会敞开正在执行业务的长连贯,不影响以后正在解决业务的响应与返回
当服务从注册核心登记掉之后,咱们在敞开以后执行的长连贯之前,须要进行一段时间,来保障消费者均收到注册核心发送的销毁申请,不再向本台机器发送申请。
Thread.sleep(NOTIFY_TIMEOUT);//Long NOTIFY_TIMEOUT = 10000L;
AbstractRegistryFactory.destroyAll()执行实现后,循环执行protocol.destroy();
public void destroy() { for (String key : new ArrayList<String>(serverMap.keySet())) { ExchangeServer server = serverMap.remove(key); if (server != null) { try { if (logger.isInfoEnabled()) { logger.info("Close Dubbo server: " + server.getLocalAddress()); } server.close(getServerShutdownTimeout()); } catch (Throwable t) { logger.warn(t.getMessage(), t); } } }......
protocol.destroy()的作用:
- 勾销该协定所有曾经裸露和援用的服务
- 开释协定所占用的所有资源,比方连贯和端口
在destroy()办法中,对server和client别离进行销毁,调用 server.close(getServerShutdownTimeout());
public void close(final int timeout) { startClose(); if (timeout > 0) { final long max = (long) timeout; final long start = System.currentTimeMillis(); if (getUrl().getParameter(Constants.CHANNEL_SEND_READONLYEVENT_KEY, true)) { sendChannelReadOnlyEvent(); } while (HeaderExchangeServer.this.isRunning() && System.currentTimeMillis() - start < max) { try { Thread.sleep(10); } catch (InterruptedException e) { logger.warn(e.getMessage(), e); } } } doClose(); server.close(timeout);}
能够看出当咱们配置了敞开连贯的超时工夫时,敞开前会期待直到超时工夫完结,以保障服务在此段时间内实现响应
所有咱们给提供者和消费者同时配置雷同的断开超时工夫 wait="50000":
<Dubbo:registry protocol="zookeeper" address="127.0.0.1:2181" wait="50000"/>
这里提供者和消费者必须都配置,否则会在业务实现前敞开连贯
shutdownhook实现ActiveMQ优雅停机(平滑退出)
在手动敞开Mq监听的时候,发现我的项目代码外面,DefaultMessageListenerContainer 是没被spring治理的,咱们敞开监听,登记Consumers时须要调用它的shutdown()办法,所以手动保护了一个HashSet<JmsDestinationAccessor> 来治理
JmsConnectionRegistry
@Configurationpublic class JmsConnectionRegistry { public HashSet<JmsDestinationAccessor> containers = new HashSet<>(); @Bean public JmsConnectionRegistry getBean() { return new JmsConnectionRegistry(); }}
手动治理containers:
JmsConnectionRegistry jmsConnectionRegistry = (JmsConnectionRegistry) SpringContext.getBean("jmsConnectionRegistry");...jmsConnectionRegistry.containers.add(listenerContainer);
ActiveMQ登记代码:
private static void shutdownActiveMQ() { //敞开监听 JmsConnectionRegistry jmsConnectionRegistry = (JmsConnectionRegistry) SpringContext.getBean("jmsConnectionRegistry"); for (JmsDestinationAccessor container : jmsConnectionRegistry.containers) { try { ((DefaultMessageListenerContainer) container).shutdown(); } catch (JmsException e) { logger.warn(e.getMessage(), e); } } }
MQ的停机逻辑就是敞开监听的task
shutdownhook实现Quartz优雅停机(平滑退出)
这个比较简单,只须要调用scheduler.shutdown(true);
public static void shutdownQuartzJobs() { Scheduler scheduler = SpringContext.getBean(Scheduler.class); try { scheduler.shutdown(true); } catch (SchedulerException e) { logger.warn(e.getMessage(), e); } }
shutdownhook实现Restful接口(HTTP申请)优雅停机(平滑退出)
万万没想到,当我着手做服务平滑退出的时候,认为敞开Servlet很简略,当执行spring contex的销毁办法时,会登记掉所有的bean以及bean工厂,以致所有Http申请都不能正确散发并返回404。而后我就放弃了这个“最简略的”,去摸索Dubbo服务的优雅敞开了。等到我回到阻断Http申请进入服务的时候,所有和我想的齐全不一样,让咱们一起看看,到底要怎么阻止Http申请进入服务
Spring 是怎么定义本人的shutdownhook的
Spring这么优良的框架,也设计了注册钩子的入口。不过我的项目中应用的spring mvc3.2.16 默认并没有注册钩子,可能是没有开启注册钩子的监听器。
public void registerShutdownHook() { if (this.shutdownHook == null) { // No shutdown hook registered yet. this.shutdownHook = new Thread() { @Override public void run() { doClose(); } }; Runtime.getRuntime().addShutdownHook(this.shutdownHook); }}
下面就是spring context中注册钩子的入口,和咱们注册钩子的操作是一样的。
销毁的外围就是doClose()办法
protected void doClose() { boolean actuallyClose; synchronized (this.activeMonitor) { actuallyClose = this.active && !this.closed; this.closed = true; } if (actuallyClose) { if (logger.isInfoEnabled()) { logger.info("Closing " + this); } LiveBeansView.unregisterApplicationContext(this); try { // Publish shutdown event. publishEvent(new ContextClosedEvent(this)); } catch (Throwable ex) { logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex); } // Stop all Lifecycle beans, to avoid delays during individual destruction. try { getLifecycleProcessor().onClose(); } catch (Throwable ex) { logger.warn("Exception thrown from LifecycleProcessor on context close", ex); } // Destroy all cached singletons in the context's BeanFactory. destroyBeans(); // Close the state of this context itself. closeBeanFactory(); // Let subclasses do some final clean-up if they wish... onClose(); synchronized (this.activeMonitor) { this.active = false; } }}
其实下面代码的逻辑很简略,外围是 destroyBeans(); closeBeanFactory();两个办法
- destroyBeans()
protected void destroyBeans() { getBeanFactory().destroySingletons();}public void destroySingletons() { if (logger.isInfoEnabled()) { logger.info("Destroying singletons in " + this); } synchronized (this.singletonObjects) { this.singletonsCurrentlyInDestruction = true; } String[] disposableBeanNames; synchronized (this.disposableBeans) { disposableBeanNames = StringUtils.toStringArray(this.disposableBeans.keySet()); } for (int i = disposableBeanNames.length - 1; i >= 0; i--) { destroySingleton(disposableBeanNames[i]); } this.containedBeanMap.clear(); this.dependentBeanMap.clear(); this.dependenciesForBeanMap.clear(); synchronized (this.singletonObjects) { this.singletonObjects.clear(); this.singletonFactories.clear(); this.earlySingletonObjects.clear(); this.registeredSingletons.clear(); this.singletonsCurrentlyInDestruction = false; }} 。。。protected void removeSingleton(String beanName) { synchronized (this.singletonObjects) { this.singletonObjects.remove(beanName); this.singletonFactories.remove(beanName); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.remove(beanName); } }
这里其实做的就是从缓存中,把可移除的所有bean都删除调。
- closeBeanFactory() 就是登记掉BeanFactory。
最开始想的就是用这种方法,用spring本人的形式去销毁,所以有了上面第一次的谬误尝试
Spring mvc 自定义钩子的形式销毁Servlet的谬误尝试
以下是没能胜利拦挡Http申请的谬误摸索方向
为了拿到两个上下文,我定义了一个类缓存启动时创立的两个上下文spring应用mvc时会产生两个context上下文,一个是ContextLoaderListener产生的,一个是由DispatcherServlet产生的,它们俩是父子关系
public class DemoCache { public static Set<ContextRefreshedEvent> contextRefreshedEvents = new HashSet<>();}
获取到上下文后,调用context的destroy办法来销毁for (ContextRefreshedEvent contextRefreshedEvent : DemoCache.contextRefreshedEvents) { ((AbstractRefreshableWebApplicationContext) contextRefreshedEvent.getSource()).destroy(); }
destroy():public void destroy() { close();}public void close() { synchronized (this.startupShutdownMonitor) { doClose(); // If we registered a JVM shutdown hook, we don't need it anymore now: // We've already explicitly closed the context. if (this.shutdownHook != null) { try { Runtime.getRuntime().removeShutdownHook(this.shutdownHook); } catch (IllegalStateException ex) { // ignore - VM is already shutting down } } } }
能够看出,destroy()办法就是间接运行了doClose();并试图销毁之前注册的钩子为了验证Bean是不是全都被销毁了,我尝试在destroy()后,获取我要执行办法的Beanfinal Object shutDownHookServiceImpl = context.getBean("shutDownHookServiceImpl");final Object demoController = context.getBean("demoController");
不出意外,我收到了:java.lang.IllegalStateException:BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
到这里所有进行的还很顺利,我注掉获取bean的办法并应用sleep使以后敞开前解决办法停在destroy()办法之后,避免办法完结后整个程序退出,而后用postman对服务发动Http申请,后果,喜剧产生了,申请还会如常的响应,并且Controller外面应用@Resource注入的 Sercice仍旧能够失常运行。
为什么销毁Context,还是不能拦挡Http申请?
很显然,Http申请中用到的Controller以及Service并不是咱们在context中销毁掉的,或者说,他们只是在mvc的上下文中被清理了,然而在接管Restful申请的时候,还能够从别的中央拿到。
那所有的源头,就要从申请的入口DispatcherServlet来看了。
DispatcherServlet继承了HttpServlet,是tomcat与spring之间的纽带,当tomcat接管到申请时,会转发到DispatcherServlet,并由它对申请依据mapping进行散发。
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { ... doDispatch(request, response); ...}protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ... mappedHandler = getHandler(processedRequest, false); ...}
DispatcherServlet中的doService()办法,是申请的入口,外面的doDispatch(HttpServletRequest request, HttpServletResponse response)办法是理论解决申请散发的办法
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { for (HandlerMapping hm : this.handlerMappings) { if (logger.isTraceEnabled()) { logger.trace( "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'"); } HandlerExecutionChain handler = hm.getHandler(request); if (handler != null) { return handler; } } return null;}
而getHandler则解决的是申请改如何散发,这外面,所有handlerMapping都是贮存在这个对象的实例中,debug看一下,外面都存了什么。
如图所示,对于mapping “/shutdown”,DispatcherServlet在服务启动,初始化的时候,本人保护了mapping对应的Controller,以及controller外部的属性以及属性的属性,所以,从这里登程,申请还是能够残缺的执行实现的。
至此,新的思路呈现了,当咱们把DispatcherServlet中的 this.handlerMappings 中的数据清空,申请进来时,没有目的地能够散发,就能胜利阻止Http申请的进入。
定义DispatcherServlet子类来缓存DispatcherServlet对象,也就是this
为了能拿到DispatcherServlet对象,咱们能够定义一个ManualDispatcherServlet来继承DispatcherServlet,并重写init(ServletConfig config),在初始化时,缓存servlet。
public class ManualDispatcherServlet extends DispatcherServlet { private static DispatcherServlet servlet; private final static String DISPATCHER_SERVLET ="org.springframework.web.servlet.DispatcherServlet"; private final static String HANDLER_MAPPINGS ="handlerMappings"; @Override public void init(ServletConfig config) throws ServletException { super.init(config); servlet = this; } /** * 提供DispatcherServlet中handlerMappings销毁的办法 * 供JVM优雅退出时,阻断新Restful申请进入服务 * @return * @author Youdmeng * Date 2021-03-12 **/ public static void cleanHandlerMappings() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { Class<?> dispatcherServlet = Class.forName(DISPATCHER_SERVLET); Field handlerMappings = dispatcherServlet.getDeclaredField(HANDLER_MAPPINGS); handlerMappings.setAccessible(true); handlerMappings.set(servlet, new ArrayList<HandlerMapping>()); }}
还须要记得将web.xml中注册的servlet替换成本人的,来使自定义文件失效。
将
<servlet> <servlet-name>web</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup></servlet>
替换成:
<servlet> <servlet-name>web</servlet-name> <servlet-class>com.ebiz.ebiz.demo.ManualDispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup></servlet>
当须要回绝http申请时,调用cleanHandlerMappings()办法利用反射,获取到handlerMappings,并将其赋值为空集合。这样一来,Http优雅停机(平滑退出)也就实现了。
还有个小问题,当拒绝请求进入后,对于依然处在运行中的申请,我还没能在线程池中精确定位或者辨认哪些来自DispatcherServlet并期待其敞开,下周过去在钻研钻研
更多好玩难看的内容,欢送到我的博客交换,共同进步 胡萝卜啵的博客