你的服务停机够优雅吗?很多情况下,开发人员并不够关心服务停止时候的断后处理,导致了很多线上故障的发生。比如文件被破坏造成服务崩溃、流量有损影响用户影响、兜底措施不完善导致数据不一致问题出现等等,这些均是因为服务停机没有做到优雅所导致的,当服务需要停止时,其实还有很多线程正在工作,还有很多数据处于内存中间状态,尤其是在微服务架构里,优雅停机是一个不容忽视的问题,下面就来说说怎么做到优雅停机。
JAVA原生是如何实现优雅停机?
Runtime.getRuntime.addShutdownHook(new Thread(->{ //可以处理停止时的一些善后方面的处理}));public void addShutdownHook(Thread hook) { SecurityManager sm = System.getSecurityManager; if (sm != null) { sm.checkPermission(new RuntimePermission("shutdownHooks")); } ApplicationShutdownHooks.add(hook); }
JAVA 原生可以通过Runtime去注册钩子,以便在服务接收到Kill -15 关闭信号时,能执行回调的逻辑,这里有几个要注意的点。
应用的关闭钩子,采用的是一个Set的集合进行存储,因此是无序,且采用的多线程进行执行,可以看出在java原生中,在执行关闭回调钩子时是异步无序执行,不保证类之间的依赖关系,如果要实现类之间的依赖关系的Closed,那需要自己去写一个线程将它们关闭的顺序进行编排起来,以达到顺序执行的目的。
static void runHooks { Collection
Spring是如何实现优雅停机?
在spring 中的ApplicationContext中也有注册shutdown 钩子的地方,前面讲到了JAVA原生注册的Closed钩子是按照多线程并发且无序执行,而spring 本身也注册了钩子,那么如果我们按照JAVA原生的去添加钩子的话,可能会出现我们自己注册的钩子还没有执行完,而spring 容器已经关闭的场景出现,那在spring中如何实现优雅停机处理?
protected void doClose { if (this.active.get && this.closed.compareAndSet(false, true)) { 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. if (this.lifecycleProcessor != null) { try { this.lifecycleProcessor.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; this.active.set(false); } }
看一段spring 容器关闭的代码,从代码中可以看出关闭的执行顺序:
1、unregisterApplicationContext
移除应用上下文
2、publishEvent
发出 ContextCloseEvent 事件
3、lifecycleProcessor.onClose
停止所有bean的生命周期
4、destryBeans
销毁bean,常用的 @PreDestory 注解或者实现 DisposableBean 接口之类的操作,在Bean销毁前做一些操作
5、closeBeanFactory
关闭上下文,bean工厂引用置为null
6、onClose
做一些最后的善后处理。
根据上面执行流程,我们可以监听Spring Bean CloseEvent 事件,并指定它们的执行顺序,这样就可以做到Bean的有序退出了。
dubbo是如何实现优雅停机?
在非spring 环境下,Dubbo自身DefaultApplicationDeployer在初始化的时候就会注册一个Shutdown的钩子,在spring环境它监听了Spring ClosedEvent,在此执行了shutdown的操作。
private void onContextClosedEvent(ContextClosedEvent event) { try { Object value = moduleModel.getAttribute(ModelConstants.KEEP_RUNNING_ON_SPRING_CLOSED); boolean keepRunningOnClosed = Boolean.parseBoolean(String.valueOf(value)); if (!keepRunningOnClosed && !moduleModel.isDestroyed) { moduleModel.destroy; } } catch (Exception e) { logger.error("An error occurred when stop dubbo module: " + e.getMessage, e); } // remove context bind cache DubboSpringInitializer.remove(event.getApplicationContext); }
那是否存在问题?刚刚前面讲到,通过DefaultApplicationDeployer在初始化的时候注册了shutdownhook,而在spring容器中又监听了Closed Event,这就会造成两边都有可能在执行,从而导致异常,幸运的是,dubbo在3.2版本已修复这个bug,https://github.com/apache/dubbo/pull/10730
Rocketmq是如何实现优雅停机?
如果引用的是rocketmq-spring-boot-starter包的话,那么在DefaultRocketMQListenerContainer里面分别实现了SmartLifecycle和DisposableBean接口
SmartLifecycle:
@Overridepublic void stop(Runnable callback) { stop; callback.run; } @Overridepublic void stop { if (this.isRunning) { if (Objects.nonNull(consumer)) { consumer.shutdown; } setRunning(false); } }@Override public int getPhase { // Returning Integer.MAX_VALUE only suggests that // we will be the first bean to shutdown and last bean to start return Integer.MAX_VALUE; }
DisposableBean
@Overridepublic void destroy { this.setRunning(false); if (Objects.nonNull(consumer)) { consumer.shutdown; } log.info("container destroyed, {}", this.toString); }
RocketMQ在这里进行的几个步骤需要关注
首先它将getPhase的值设置为最大,使得在容器关闭的第一时间调用stop方法
同时实现了SmartLifecycle和DisposableBean接口,分别实现了stop和destroy方法, 进行双重关闭,如果和destroy先执行了,则将running设置为false,将不再执行stop
最后
服务的优雅停机常常是开发者最容易忽视的一个地方,JAVA原生和Spring 都为我们提供了这方面的能力,需要注意的是在实际运用过程中,要重点关注它们的执行顺序以及是否在多个入口实现了停机的逻辑,避免并发执行所带来新的问题。