什么是优雅停机:
优雅停机主要处理:
未优雅停机:
当我们停止正在运行的应用程序或进程时,底层操作系统会向进程发送终止信号。在没有启用任何优雅关闭机制的情况下(如:kill -9),Spring Boot 应用程序将在收到信号后立即终止。
此时一些没有执行完的程序就会直接退出,可能导致业务逻辑执行失败,在一些业务场景下:会出现数据不一致的情况,事务逻辑不会回滚。
优雅停机使用场景:
编程语言都会提供监听当前线程终结的函数,比如在Java中,我们可以通过如下操作监听我们的退出事件:
public class Main { public static void main(String[] args) { Runtime.getRuntime().addShutdownHook(new ExitHook()); System.out.println("Do something, will exit"); } } class ExitHook extends Thread{ public void run() { System.out.println("exiting. clear resources..."); } }
我们会得到如下的结果:
Do something, will exit exiting. clear resources...
聪明的你一定发现了,我们可以在 ExitHook
去处理那些资源的释放。那么,在实际应用中是如何体现优雅停机呢?
kill -15 pid
通过以上命令发送一个关闭信号给到jvm, 然后就开始执行 Shutdown Hook 了。但是值得注意的是不能够使用以下命令:
kill -9 pid
如果这么干的话,相当于从OS方面直接将其所有的资源回收,类比一下好比强行断电,就没有任何进行优雅停机的机会了。
不过这里是最简单的实现,在实际的工作中,我们会遇见各种情况:在清理退出的时候出现异常怎么处理?清理的时间过长怎么处理?等等问题。
因此在Spring中,为了简化这样的操作已经帮助我们封装了一些。
Spring一个IOC容器,他能够管理的是收到其托管的对象,因此我们也可以很合理的想到我们需要定义托管对象的解构函数才能够被Spring在退出时释放,我们将问题简单化点,有三个事情是Spring需要解决的:
因为 Spring 版本繁杂,以 org.springframework.boot:2.1.14.RELEASE
版本分析为例。
毕竟 Spring 也是 JVM 上的实现,这一切势必也依赖于 JVM 的 Shuthook,秘密就在 org.springframework.context.support.AbstractApplicationContext#registerShutdownHook
处:
@Override public void registerShutdownHook() { if (this.shutdownHook == null) { // No shutdown hook registered yet. this.shutdownHook = new Thread() { public void run() { synchronized (startupShutdownMonitor) { doClose(); } } }; Runtime.getRuntime().addShutdownHook(this.shutdownHook); } }
回忆一下 Spring Bean 生命周期。
Bean的生命周期销毁:ContextClosedEvent、@PreDestroy、DisposableBean
当 Spring Context 销毁的时候,会调用 destroy()
函数:org.springframework.context.support.AbstractApplicationContext#destroy
@Deprecated //Spring 5 即将废弃 public void destroy() { close(); } @Override 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 } } } }
实则我们定位到最终的释放资源处就是 org.springframework.context.support.AbstractApplicationContext#doClose
函数,我们在尽情的分析一下。
protected void doClose() { LiveBeansView.unregisterApplicationContext(this); try { // Publish shutdown event. publishEvent(new ContextClosedEvent(this)); ➀ } // 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(); ➄ // Reset local application listeners to pre-refresh state. if (this.earlyApplicationListeners != null) { this.applicationListeners.clear(); this.applicationListeners.addAll(this.earlyApplicationListeners); } // Switch to inactive. this.active.set(false); }
剪去那些无影响的代码部分,我们可以发现对于 Spring 来说,真正关闭的顺序是:
对于 ➀➁ 是回调机制,不涉及到对象的销毁,➄ 是对于继承类的调用,所有的销毁都在 ➂ 中。受到 Spring 托管的对象繁多,不一定所有的对象都需要销毁行为。进一步定位一下,我们就发现了:
public void destroySingleton(String beanName) { this.removeSingleton(beanName); DisposableBean disposableBean; synchronized(this.disposableBeans) { disposableBean = (DisposableBean)this.disposableBeans.remove(beanName); } this.destroyBean(beanName, disposableBean); }
实际上那些需要销毁的对象都应该是 DisposableBean
对象。
那我们对于第二个问题也知道了,Spring会销毁那些 DisposableBean
类型的 Bean对象。
其实这是一个不是问题的问题,对于 DisposableBean
来说仅仅需要实现一个接口
public interface DisposableBean { void destroy() throws Exception; }
对于需要实现释放资源的对象需要自行实现此接口。
我们已经知道我们最开始提出的 3 个问题,让我们试着用这3个问题的答案拼凑处一个 Spring Web Server 是如何优雅的停机的。
那我们需要证实一件事情: Web Server内部的资源都是 DisposableBean,并且受 Spring 托管。
通过反向定位的办法,可以快速的定位到比如 数据库的资源 org.springframework.orm.jpa.AbstractEntityManagerFactoryBean#destroy
在销毁的阶段会将 Entity 对象进行销毁。
对于收到 Spring 托管的对象的优雅停机的路径是:
Runtine Shutdown Hook -> Context:destory() -> DisposableBean:destroy()
对于大部分的资源比如数据库,服务发现,等等都是这样的销毁方式。
一个疑问
对于普通的 Bean 的销毁我们已经完全了解,但是对于动手做实验的不知道有没有发现,其实 Web Server
并不是在 destroySingleton
阶段进行销毁的,那他是在哪里销毁的呢?
回忆一下,我们除了销毁Beans 之外,是不是还有最后一个 close()
函数可以调用,没有错!对于 Tomcat ...
这些web容器来说,本身就是 ApplicationContext
的一个子类并非是 Bean
一部分,
因此他们的 close()
函数在 org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext
处
protected void onClose() { super.onClose(); this.stopAndReleaseWebServer(); }
因此 stop webserver
是在 destory context
之后销毁的,那我们岂不是会出现一边在接受请求但是这些请求都是会失败的吗?如果是是这样的,可真是太愚蠢的设计了。
另辟蹊径
对于这样的情况,我们在 shutdown 之前让 Web server 停止接受任何的请求,但可惜的是在此版本的 tomcat
不支持此特效,需要待 9.0.33+
spring-boot-2-3-0-available-now。
在早期的版本中(spring boot 2.3.0 之前),我们依然可以通过一些额外的方式将这件事情做到:请查阅下面【Spring Boot < 2.3.0:内置容器】章节内容
又一个疑问?
在 Spring-Boot-2.3.0
之前我们都需要这么处理吗?我的天,很多写了 Spring 已经超过十年了,难道 Spring
一直没有解决这个问题吗?答案也是否定的。
还记得我们在 Spring 的早期阶段,我们通过 XML 来构造一个Spring项目的时候吗?
那时候的方案是将我们的Web 程序作为一个 War 提供给 Tomcat
的 webapps中,此时tomcat会尝试构造我们的 DispatcherServlet
将整个系统运作起来,
而在 Shutdown 阶段,这样的逻辑也是由 Tomcat
进行处理的。也就是说,对于 Embeded Web Server
的 Spring Boot
和 传统的 Web Server
在 Destory Spring Applicaion Context
这一步的时间是不一样的,
Spring Boot with Embeded Web Server
在 2.3.0
之前的版本都是先关闭 Context
上下文再关闭 Web容器,而传统的 Web Server
是先关闭 Web容器
再去关闭 Context
上下文。
对于 传统的 Web Server
:
Runtime Shutdwon Hook -> org.apache.catalina.util.LifecycleBase#stop -> Spring Context Stop
因此出现优雅停机问题的集中在 Spring Boot < 2.3.0
的版本内。
我们执行 catalina.sh stop
就可以执行 Tomcat 的优雅停机。
Springboot2.3.0 之后默认完成了优雅停机。
可以通过在应用程序配置文件中设置两个属性来进行:
# 开启优雅停机 server.shutdown=graceful spring.lifecycle.timeout-per-shutdown-phase=30s
1、 server.shutdown 属性可以支持的值有两种:
2、spring.lifecycle.timeout-per-shutdown-phase 服务端等待最大超时时间,采用java.time.Duration格式的值,默认30s。
当我们使用了如上配置开启了优雅停机功能,当我们通过SIGTERM信号关闭 Spring Boot 应用时:
Spring Boot 的所有嵌入式服务器都支持优雅终止。但是,拒绝新请求的方式可能会因各个服务器的实现而异(见下图)。
web 容器名称 | 行为说明 |
---|---|
Tomcat 9.0.33+ | 停止接受网络层的请求,客户端新请求等待超时。 |
Reactor Netty | 停止接受网络层的请求,客户端新请求等待超时。 |
Undertow | 接受请求,客户端新请求直接返回 503。 |
Jetty | 停止接受网络层的请求,客户端新请求等待超时。 |
1、执行 kill -2 或者 kill -15
kill -2或-15 相当于快捷键 Ctrl + C 会触发 Java 的 ShutdownHook 事件处理,一定不要使用 kill -9,暴力美学强制杀死进程,不会执行 ShutdownHook。
优雅停机或者一些后置处理可参考以下源码:
public abstract class AbstractApplicationContext { ...... public void registerShutdownHook() { if (this.shutdownHook == null) { this.shutdownHook = new Thread("SpringContextShutdownHook") { public void run() { synchronized(AbstractApplicationContext.this.startupShutdownMonitor) { AbstractApplicationContext.this.doClose(); } } }; Runtime.getRuntime().addShutdownHook(this.shutdownHook); } } /** @deprecated */ @Deprecated public void destroy() { this.close(); } public void close() { Object var1 = this.startupShutdownMonitor; synchronized(this.startupShutdownMonitor) { this.doClose(); //重点:销毁bean if (this.shutdownHook != null) { try { Runtime.getRuntime().removeShutdownHook(this.shutdownHook); } catch (IllegalStateException var4) { ; } } } } protected void doClose() { if (this.active.get() && this.closed.compareAndSet(false, true)) { if (this.logger.isDebugEnabled()) { this.logger.debug("Closing " + this); } LiveBeansView.unregisterApplicationContext(this); try { this.publishEvent((ApplicationEvent)(new ContextClosedEvent(this))); } catch (Throwable var3) { this.logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", var3); } if (this.lifecycleProcessor != null) { try { this.lifecycleProcessor.onClose(); } catch (Throwable var2) { this.logger.warn("Exception thrown from LifecycleProcessor on context close", var2); } } this.destroyBeans(); this.closeBeanFactory(); this.onClose(); if (this.earlyApplicationListeners != null) { this.applicationListeners.clear(); this.applicationListeners.addAll(this.earlyApplicationListeners); } this.active.set(false); } } ...... }
2、通过 actuate 端点实现优雅停机
POST 请求 /actuator/shutdown 即可执行优雅关机。
pom.xml 需引入依赖如下:
org.springframework.boot spring-boot-starter-actuator
application.properties 需配置如下:
management.endpoint.shutdown.enabled=true management.endpoints.web.exposure.include=shutdown
优雅停机或者一些后置处理可参考以下源码:
@Endpoint(id = "shutdown", enableByDefault = false) public class ShutdownEndpoint implements ApplicationContextAware { @WriteOperation public Mapshutdown() { Thread thread = new Thread(this::performShutdown); thread.setContextClassLoader(getClass().getClassLoader()); thread.start(); } private void performShutdown() { try { Thread.sleep(500L); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } // 此处close 逻辑和上边 shutdownhook 的处理一样,其实就是调用AbstractApplicationContext的close()方法 this.context.close(); } }
Springboot2.3.0 之前需要自己实现优雅停机。
创建SafetyShutDownConfig类实现TomcatConnectorCustomizer,ApplicationListener
实现 TomcatConnectorCustomizer
接口,定制 Connector
的行为,实现 ApplicationListener
接口,监听 Spring 容器的关闭事件,即当前的 ApplicationContext 执行 close()
方法,
这样我们就可以在请求处理完毕后进行 Tomcat 线程池的关闭,具体的实现代码如下:
@Bean public GracefulShutdown gracefulShutdown() { return new GracefulShutdown(); } private static class GracefulShutdown implements TomcatConnectorCustomizer, ApplicationListener{ private static final Logger log = LoggerFactory.getLogger(GracefulShutdown.class); private volatile Connector connector; @Override public void customize(Connector connector) { this.connector = connector; } @Override public void onApplicationEvent(ContextClosedEvent event) { this.connector.pause(); Executor executor = this.connector.getProtocolHandler().getExecutor(); if (executor instanceof ThreadPoolExecutor) { try { ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor; threadPoolExecutor.shutdown(); if (!threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS)) { log.warn("Tomcat thread pool did not shut down gracefully within 30 seconds. Proceeding with forceful shutdown"); } } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } }
有了定制的 Connector
回调,还需要在启动过程中添加到内嵌的 Tomcat 容器中,然后等待监听到关闭指令时执行,addConnectorCustomizers
方法可以把定制的 Connector
行为添加到内嵌的 Tomcat 中,具体代码如下:
@Bean public ConfigurableServletWebServerFactory tomcatCustomizer() { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); factory.addConnectorCustomizers(gracefulShutdown()); return factory; }
现在的很多应用都跑在 kubernetes
这样的容器平台上,此时我们 POD 在进行 terminated 操作的时候,会首先向运行的进程发送一个 SIGTERM
指令,然后等待 30秒,
在30后没有终结的话,会再次发送一个 SIGKILL
进行强制终结,因此对于容器平台,我们要注意的这一个等待时间是否足够进行资源的回收。
但是我们还有一个问题,就是正在请求的流量问题,对于这个问题我们需要进行组合拳,还记得 kubernetes
中有 readinessProbe
的概念吗?
当我们的Pod启动的时候,如果 Readiness
未就绪, kubernetes
也不会将我们的 POD 作为 SVC
的可选地址(采用SVC负债均衡的情况下)。
因此我们的应用在接受到 SIGTERM
的第一时刻就将Readiness
的地址进行失败行为,并且等待一定时间之后再进行 Spring Context Shutdown
操作。
但是对于超长时间的请求依然会有失败的可能,不过对于大部分的应用来说,优雅停机本身也只是等待固定时间,因此对于超长持续的请求让其失败也是可选的方案。
前面说的,是基于单机版本的优雅停机,在关闭时,只是保证了服务端内部线程执行完毕,调用方的状态是没关注的。
不论是Dubbo还是Spring Cloud 的分布式服务框架,需要关注的是怎么能在服务停止前,先将提供者在注册中心进行反注册,然后在停止服务提供者,这样才能保证业务系统不会产生各种503、timeout等现象。
在生产环境中,随着云原生架构的发展,自动的弹性伸缩、滚动升级、分批发布等云原生能力让用户享受到了资源、成本、稳定性的最优解。
但是在应用的缩容、发布等过程中,由于实例下线处理得不够优雅,将会导致短暂的服务不可用,短时间内业务监控会出现大量 io 异常报错;
如果业务没做好事务,那么还会引起数据不一致的问题,那么需要紧急手动订正错误数据;甚至每次发布,您需要发告示停机发布,否则您的用户会出现一段时间服务不可用。
没处理好服务实例下线,无论发生上述哪种情况,都会对您业务的连续性造成困扰。
对于任何一个线上应用,如何在服务更新部署过程中保证业务无感知是开发者必须要解决的问题,即从应用停止到重启恢复服务这个阶段不能影响正常的业务请求,
这使得无损下线成为应用生命周期中必不可少的一个环节。
一个 Spring Cloud 应用正常分批发布的流程:
我们看到,当一个Spring Cloud服务端通过SpringBoot提供的graceful shutdown下线时,它会拒绝客户端新的请求,并且等待已经在处理的线程处理完成后,或者在配置的应用最长等待时间到了之后进行下线。
但是在服务端重启开始拒绝客户端新的请求的时刻开始,即执行了Connectors.stop开始,到客户端感知到服务端该实例下线这段时间内,客户端向该实例发起的所有请求都会被拒绝,从而引起服务调用异常。
如果客户端考虑增加重试能力,这一定程度上可以缓解发布过程中服务调用报错的问题,但是无法根本上保证下线过程的无损,
如果服务调用报错期过程,或者分批发布时候同一批次下线的节点数过多,无法保证仅仅增加多次重试就能够调用到未下线的节点上。
这不能根本解决问题!同时需要考虑配置重试带来的业务上存在不幂等的风险。
阿里云EDAS应用无损下线的设计:
如图看到,我们通过3个步骤的增强,主动注销、服务提供者通知下线信息、服务消费者调用其他服务提供者。
可以看到,真正做到无损下线能力是需要客户端增强一起联动的
项目说明:
非容器环境,我们应用服务之间使用openFeign进行通信,使用ribbon进行负载均衡,ribbon会缓存服务列表(每30s刷新一次),默认情况下,nacos服务下线不会即时刷新ribbon缓存服务列表。
借鉴了阿里云EDAS应用无损下线的设计思想,在自己公司的微服务中采用以下方法实现无损下线:
#!/bin/bash APP_NAME="masl" APP_PORT="8080" jar_name="$APP_NAME.jar" pid=`ps -ef | grep $jar_name | grep -v grep | awk '{print $2}'` if [ -n "$pid" ] then echo "begin call nacos offline." curl -X "POST" "http://localhost:$APP_PORT/$APP_NAME/actuator/service-registry?status=DOWN" -H "Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8" echo "begin sleep 10s, wait ribbon server list refresh." sleep 30 echo "begin stop app, kill pid:" $pid kill -15 $pid fi
注:因nacos服务下线不会即时刷新ribbon缓存服务列表,所以步骤3要休眠一段时间,这种方法不够优雅,可以有更好的方法,但需要扩展实现:NamingService通过subscribe方法如何感知到实例的上下线。
dubbo默认开启了优雅停机。下线分为从注册中心下线,关闭协议。2.7之后源码如下:ShutdownHookListener,继承 Spring ApplicationListener 接口,用以监听 Spring 相关事件。
这里 ShutdownHookListener 仅仅监听 Spring 关闭事件,当 Spring 开始关闭,将会触发 ShutdownHookListener 内部逻辑。
public class SpringExtensionFactory implements ExtensionFactory { private static final Logger logger = LoggerFactory.getLogger(SpringExtensionFactory.class); private static final SetCONTEXTS = new ConcurrentHashSet (); private static final ApplicationListener SHUTDOWN_HOOK_LISTENER = new ShutdownHookListener(); public static void addApplicationContext(ApplicationContext context) { CONTEXTS.add(context); if (context instanceof ConfigurableApplicationContext) { // 注册 ShutdownHook ((ConfigurableApplicationContext) context).registerShutdownHook(); // 取消 AbstractConfig 注册的 ShutdownHook 事件 DubboShutdownHook.getDubboShutdownHook().unregister(); } BeanFactoryUtils.addApplicationListener(context, SHUTDOWN_HOOK_LISTENER); } // 继承 ApplicationListener,这个监听器将会监听容器关闭事件 private static class ShutdownHookListener implements ApplicationListener { @Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof ContextClosedEvent) { DubboShutdownHook shutdownHook = DubboShutdownHook.getDubboShutdownHook(); shutdownHook.doDestroy(); } } } }
注:通过配置dubbo.application.shutwait=30s可以设置dubbo等待时间。
Spring托管的线程池默认完成了优雅关闭。自定义的线程池优雅关闭的方法如下:
private ThreadPoolExecutor executor; @Bean @Primary public ThreadPoolExecutor asyncServiceExecutor() { executor = new ThreadPoolExecutor(5, 20, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000), new ThreadPoolExecutor.CallerRunsPolicy()); return executor; } @PreDestroy public void destroyThreadPool() { if (!executor.isTerminated()){ executor.shutdown(); try { executor.awaitTermination(10, TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); executor.shutdownNow(); } } log.info("ThreadPoolExecutor destroyed !"); }
Spring托管的MQ消费者默认完成了优雅关闭。
自定义添加ShutdownHook,有几种简单的方式。执行顺序:contextCloseEvent > disposableBean.destroy() > @PreDestroy
public class APIService implements ApplicationListener{ @Override public void onApplicationEvent(ContextClosedEvent contextClosedEvent) { //Do shutdown work. } }
@Slf4j @Service public class DefaultDataStore implements DisposableBean { private final ExecutorService executorService = new ThreadPoolExecutor( OSUtil.getAvailableProcessors(), OSUtil.getAvailableProcessors() + 1, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(200), new DefaultThreadFactory("UploadVideo")); @Override public void destroy() throws Exception { log.info("准备优雅停止应用使用 DisposableBean"); executorService.shutdown(); } }
@Slf4j @Service public class DefaultDataStore { private final ExecutorService executorService = new ThreadPoolExecutor( OSUtil.getAvailableProcessors(), OSUtil.getAvailableProcessors() + 1, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(200), new DefaultThreadFactory("UploadVideo")); @PreDestroy public void shutdown() { log.info("准备优雅停止应用 @PreDestroy"); executorService.shutdown(); } }