• Spring——项目优雅停机


    前言

    最近,公司项目要做灰度发布,则要先实现项目无缝上下线,如丝般顺滑,我们给应用添加优雅停机功能。

    什么是优雅停机:

    • 就是对应用进程发送停止指令之后,执行的一系列保证应用正常关闭的操作。这些操作往往包括等待已有请求执行完成、关闭线程、关闭连接和释放资源等
    • 就是对应用进程发送停止指令之后,能保证正在执行的业务操作不受影响,可以继续完成已有请求的处理,但是停止接受新请求
    • 本质上是JVM即将关闭前执行的一些额外的处理代码
    • 可以避免非正常关闭程序可能造成数据异常或丢失,应用异常等问题

    优雅停机主要处理:

    • 池化资源的释放:数据库连接池,HTTP 连接池,线程池
    • 在处理线程的释放:已经被连接的HTTP请求
    • mq消费者的处理:正在处理的消息
    • 隐形受影响的资源的处理:Zookeeper、Nacos实例下线等

    未优雅停机:

    当我们停止正在运行的应用程序或进程时,底层操作系统会向进程发送终止信号。在没有启用任何优雅关闭机制的情况下(如:kill -9),Spring Boot 应用程序将在收到信号后立即终止。

    此时一些没有执行完的程序就会直接退出,可能导致业务逻辑执行失败,在一些业务场景下:会出现数据不一致的情况,事务逻辑不会回滚。

    优雅停机使用场景: 

    • 一个基于springboot的服务,服务从网络接收请求,再把请求任务放入队列里交给线程池取异步消费请求任务。怎么样停止服务才能保证任务队列里的请求都处理完成了呢?

    JVM 中的实现

    编程语言都会提供监听当前线程终结的函数,比如在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 的模式

    Spring一个IOC容器,他能够管理的是收到其托管的对象,因此我们也可以很合理的想到我们需要定义托管对象的解构函数才能够被Spring在退出时释放,我们将问题简单化点,有三个事情是Spring需要解决的:

    • Spring 需要知道 Runtime 在退出
    • Spring 知道需要释放哪些资源
    • Spring 需要知道如何释放资源

    因为 Spring 版本繁杂,以 org.springframework.boot:2.1.14.RELEASE 版本分析为例。

    Spring  需要知道 Runtime 在退出

    毕竟 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 知道需要释放哪些资源

    回忆一下 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 来说,真正关闭的顺序是:

    1. 发布一个关闭事件
    2. 调用生命周期处理器
    3. 销毁所有的Bean
    4. 关闭Bean工厂
    5. 调用子类的Close函数

    对于 ➀➁ 是回调机制,不涉及到对象的销毁,➄ 是对于继承类的调用,所有的销毁都在 ➂ 中。受到 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对象。

    Spring 需要知道如何释放资源

    其实这是一个不是问题的问题,对于 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()

    对于大部分的资源比如数据库,服务发现,等等都是这样的销毁方式。

    进阶: Web 容器

    一个疑问

    对于普通的 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 的版本内。

    如何优雅停机

    传统的 Tomcat 容器

    我们执行 catalina.sh stop <WAITING SECONDS> 就可以执行 Tomcat 的优雅停机。

    Spring Boot > 2.3.0:内置容器

    Springboot2.3.0 之后默认完成了优雅停机。

    • 启用正常停机

    可以通过在应用程序配置文件中设置两个属性来进行:

    # 开启优雅停机
    server.shutdown=graceful
    spring.lifecycle.timeout-per-shutdown-phase=30s

    1、 server.shutdown 属性可以支持的值有两种:

    • immediate 这是默认值,配置后服务器立即关闭,无优雅停机逻辑。
    • graceful 开启优雅停机功能,并遵守 spring.lifecycle.timeout-per-shutdown-phase 属性中给出的超时来作为服务端等待的最大时间。

    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 需引入依赖如下:

      <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

    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 Map<String, String> shutdown() {
            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();
        }
    }

    Spring Boot < 2.3.0:内置容器

    Springboot2.3.0 之前需要自己实现优雅停机。

    • 方法 1:自己实现优雅停机

    创建SafetyShutDownConfig类实现TomcatConnectorCustomizer,ApplicationListener<ContextClosedEvent>接口即可,kill -2和kill -15就可以进行测试。

    实现 TomcatConnectorCustomizer 接口,定制 Connector 的行为,实现 ApplicationListener<ContextClosedEvent> 接口,监听 Spring 容器的关闭事件,即当前的 ApplicationContext 执行 close() 方法,

    这样我们就可以在请求处理完毕后进行 Tomcat 线程池的关闭,具体的实现代码如下:

    @Bean
    public GracefulShutdown gracefulShutdown() {
        return new GracefulShutdown();
    }
    
    private static class GracefulShutdown implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
        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;
    }
    • 方法 2:基于平台实现

    现在的很多应用都跑在 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 应用正常分批发布的流程:

    1. 服务发布前,消费者根据负载均衡规则调用服务提供者,业务正常。
    2. 服务提供者 B 需要发布新版本,先对其中的一个节点进行操作,先是正常停止 Java 进程。
    3. 服务停止过程中,首先去注册中心注销服务,然后等待服务端线程处理完成,再停止服务。
    4. 注册中心则将通知消费者,其中的一个服务提供者节点已下线。这个过程包含推送和轮询两种方式,推送可以认为是准实时的,轮询的耗时由服务消费者轮询间隔决定,最差的情况下需要 1 分钟。
    5. 服务消费者刷新服务列表,感知到服务提供者已经下线了一个节点,但是这个过程中Spring Cloud 的负载均衡组件 Ribbon 默认的刷新时间是 30 秒 ,最差情况下需要耗时 30 秒。
    6. 服务消费者不再调用已经下线的节点。

    我们看到,当一个Spring Cloud服务端通过SpringBoot提供的graceful shutdown下线时,它会拒绝客户端新的请求,并且等待已经在处理的线程处理完成后,或者在配置的应用最长等待时间到了之后进行下线。

    但是在服务端重启开始拒绝客户端新的请求的时刻开始,即执行了Connectors.stop开始,到客户端感知到服务端该实例下线这段时间内,客户端向该实例发起的所有请求都会被拒绝,从而引起服务调用异常。

    如果客户端考虑增加重试能力,这一定程度上可以缓解发布过程中服务调用报错的问题,但是无法根本上保证下线过程的无损,

    如果服务调用报错期过程,或者分批发布时候同一批次下线的节点数过多,无法保证仅仅增加多次重试就能够调用到未下线的节点上。

    这不能根本解决问题!同时需要考虑配置重试带来的业务上存在不幂等的风险。 

    阿里云EDAS无损下线

    阿里云EDAS应用无损下线的设计:

    如图看到,我们通过3个步骤的增强,主动注销、服务提供者通知下线信息、服务消费者调用其他服务提供者。

    可以看到,真正做到无损下线能力是需要客户端增强一起联动的

    • 主动注销:我们在应用服务下线前,主动通知注册中心注销该实例
    • 通知下线信息:我们会在服务端实例下线前主动通知客户端,该服务节点下线的信息
    • 调用其他提供者:我们在客户端增强其负载均衡能力,在服务端下线后,客户端主动调用其他服务提供者节点

    本人生产环境无损下线

    项目说明:

    非容器环境,我们应用服务之间使用openFeign进行通信,使用ribbon进行负载均衡,ribbon会缓存服务列表(每30s刷新一次),默认情况下,nacos服务下线不会即时刷新ribbon缓存服务列表。

    借鉴了阿里云EDAS应用无损下线的设计思想,在自己公司的微服务中采用以下方法实现无损下线:

    1. 实现单个springboot应用的优雅停机;
    2. 配置客户端重试机制,服务端接口支持幂等操作;
    3. springboot应用中暴露actuator的service-registry端点(即暴露nacos服务下线的http请求地址);
    4. 编写停止应用的脚本:首先调用服务下线的请求地址让服务下线,然后休眠一段时间(ribbon服务列表刷新时间),最后再停止应用;
    #!/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优雅下线

    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 Set<ApplicationContext> CONTEXTS = new ConcurrentHashSet<ApplicationContext>();
        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 !");
        }

    mq消费者优雅关闭

    Spring托管的MQ消费者默认完成了优雅关闭。 

    添加ShutdownHook

    自定义添加ShutdownHook,有几种简单的方式。执行顺序:contextCloseEvent > disposableBean.destroy() > @PreDestroy

    • 实现ApplicationListener接口,实现contextClosedEvent事件
    public class APIService implements ApplicationListener<ContextClosedEvent>
    {
        @Override
        public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
            //Do shutdown work.
        }
    }
    • 实现DisposableBean接口,实现destroy方法
    @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(); } }
    • 使用@PreDestroy注解
    @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(); } }

    引用:

  • 相关阅读:
    Spring Cloud Gateway配置自定义异常返回
    C#开机启动,托盘图标等小功能
    微信内置浏览器搞事情之调试模式
    物联网架构成长之路(56)-SpringCloudGateway+JWT实现网关鉴权
    物联网架构成长之路(55)-Gateway+Sentinel实现限流、熔断
    物联网架构成长之路(53)-Sentinel流量控制中间件入门
    物联网架构成长之路(54)-基于Nacos+Gateway实现动态路由
    物联网架构成长之路(52)-基于Nacos+prometheus+grafana的监控
    物联网架构成长之路(51)-Nacos微服务配置中心、服务注册服务发现
    物联网架构成长之路(50)-EMQ配置SSL证书,实现MQTTs协议
  • 原文地址:https://www.cnblogs.com/caoweixiong/p/15650333.html
Copyright © 2020-2023  润新知