美文网首页
springboot优雅关闭应用详解

springboot优雅关闭应用详解

作者: 定金喜 | 来源:发表于2020-05-22 00:18 被阅读0次

    为什么需要优雅关闭

    最常用的关闭应用的方法是kill -9 PID 暴力关闭,但是暴力关闭会带来很多问题,例如会造成数据的不完整性。我们公司需要做一个异步同步考勤记录的功能,同步完成后会更新redis的相关key的值为完成状态,如果此时应用被暴力关闭了,会导致此状态不会更新,进度条会一直卡在同步中,需要等待超时后重试,如果正好更新到最后一个考勤记录被强制kill了,必须要重新同步一次,对用户来说体验非常差。

    优雅关闭的原理

    调用spring上下文close函数关闭容器,在此函数中进行spring bean的移除和tomcat线程池的释放等操作,但是不能对代码中自定义的线程或者线程池的关闭,需要自己去释放,释放的契机是相关的类需要实现以下三种的一种
    @PreDestroy注解
    destory-method方法
    DisposableBean接口
    结合例子来说明

    1.通过 actuator 实现优雅停机

    引入maven依赖

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

    然后将shutdown节点打开,也将/actuator/shutdown暴露web访问也设置上,除了shutdown之外还有health, info的web访问都打开的话将management.endpoints.web.exposure.include=*就可以。将如下配置设置到application.properties里边,设置一下服务的端口号为6666。

    server.port=6666
    management.endpoint.shutdown.enabled=true
    management.endpoints.web.exposure.include=shutdown
    

    编写controller类

    package com.hqs.springboot.shutdowndemo.controller;
    
    import com.google.common.util.concurrent.ThreadFactoryBuilder;
    import org.springframework.beans.BeansException;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    import org.springframework.context.ConfigurableApplicationContext;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.annotation.PreDestroy;
    import java.util.concurrent.LinkedBlockingDeque;
    import java.util.concurrent.ThreadFactory;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author huangqingshi
     * @Date 2019-08-17
     */
    @RestController
    public class ShutDownController implements ApplicationContextAware {
    
        private ApplicationContext context;
    
        private static final ThreadPoolExecutor TRACK_LOG_EXECUTORS;
    
        static {
            // 初始化线程池
            ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("track-log-%d").build();
            TRACK_LOG_EXECUTORS = new ThreadPoolExecutor(3, 5, 0L, TimeUnit.SECONDS, new LinkedBlockingDeque<>(200), threadFactory, new ThreadPoolExecutor.AbortPolicy());
        }
    
        @PostMapping("/shutDownContext")
        public String shutDownContext() {
            ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) context;
            ctx.close();
            return "context is shutdown";
        }
    
        @GetMapping("/")
        public String getIndex() {
            return "OK";
        }
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            context = applicationContext;
        }
    
        @PreDestroy
        public void preDestroy() {
            System.out.println(getCurrentDate()+":ShutDownController is destroyed");
        }
    
        private String getCurrentDate() {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            return simpleDateFormat.format(new Date());
        }
    }
    

    启动程序,调用curl -X POST http://localhost:6666/actuator/shutdown,控制台输出:

    2020-05-21 23:30:36.459  INFO 67909 --- [           main] c.h.s.s.ShutdowndemoApplication          : Starting ShutdowndemoApplication on xianchengs-MacBook-Pro.local with PID 67909 (/Users/ding/Downloads/shutdowndemo-master/target/classes started by ding in /Users/ding/Downloads/shutdowndemo-master)
    2020-05-21 23:30:36.462  INFO 67909 --- [           main] c.h.s.s.ShutdowndemoApplication          : No active profile set, falling back to default profiles: default
    2020-05-21 23:30:37.581  INFO 67909 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 6666 (http)
    2020-05-21 23:30:37.600  INFO 67909 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
    2020-05-21 23:30:37.600  INFO 67909 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.22]
    2020-05-21 23:30:37.674  INFO 67909 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
    2020-05-21 23:30:37.674  INFO 67909 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1168 ms
    2020-05-21 23:30:38.110  INFO 67909 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
    2020-05-21 23:30:38.299  INFO 67909 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 1 endpoint(s) beneath base path '/actuator'
    2020-05-21 23:30:38.371  INFO 67909 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 6666 (http) with context path ''
    2020-05-21 23:30:38.375  INFO 67909 --- [           main] c.h.s.s.ShutdowndemoApplication          : Started ShutdowndemoApplication in 2.222 seconds (JVM running for 2.917)
    2020-05-21 23:30:38.847  INFO 67909 --- [)-192.168.0.102] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
    2020-05-21 23:30:38.848  INFO 67909 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
    2020-05-21 23:30:38.852  INFO 67909 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet        : Completed initialization in 4 ms
    2020-05-21 23:30:45.165  INFO 67909 --- [      Thread-16] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
    2020-05-21 23:30:45:ShutDownController is destroyed
    
    Process finished with exit code 0
    

    为了试验一下优雅关闭只会关闭springboot托管的tomcat的线程池,不会关闭自定义的线程池,我们增加两个接口:

    /**
         * 测试容器线程池
         * @return
         * @throws Exception
         */
        @GetMapping("/testTomcatThreads")
        public String testTomcatThreads() throws Exception{
            try {
                while (true){
                }
            }catch (Exception ex){
                ex.printStackTrace();
            }
            return "OK";
        }
    
        /**
         * 测试自定义线程池
         * @return
         * @throws Exception
         */
        @GetMapping("/testOwnerThreads")
        public String testOwnerThreads() throws Exception{
            TRACK_LOG_EXECUTORS.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(30000L);
                    }catch (Exception ex){
                        ex.printStackTrace();
                    }
                    System.out.println(getCurrentDate()+":自定义线程池执行完毕");
                }
            });
            return "OK";
        }
    

    先测试容器线程池
    curl -X GET http://localhost:6666/testTomcatThreads 后立马调用
    curl -X POST http://localhost:6666/actuator/shutdown,控制台输出:

    2020-05-21 23:32:30.018  INFO 67924 --- [           main] c.h.s.s.ShutdowndemoApplication          : Starting ShutdowndemoApplication on xianchengs-MacBook-Pro.local with PID 67924 (/Users/ding/Downloads/shutdowndemo-master/target/classes started by ding in /Users/ding/Downloads/shutdowndemo-master)
    2020-05-21 23:32:30.021  INFO 67924 --- [           main] c.h.s.s.ShutdowndemoApplication          : No active profile set, falling back to default profiles: default
    2020-05-21 23:32:31.215  INFO 67924 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 6666 (http)
    2020-05-21 23:32:31.235  INFO 67924 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
    2020-05-21 23:32:31.235  INFO 67924 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.22]
    2020-05-21 23:32:31.307  INFO 67924 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
    2020-05-21 23:32:31.307  INFO 67924 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1248 ms
    2020-05-21 23:32:31.724  INFO 67924 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
    2020-05-21 23:32:31.913  INFO 67924 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 1 endpoint(s) beneath base path '/actuator'
    2020-05-21 23:32:31.986  INFO 67924 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 6666 (http) with context path ''
    2020-05-21 23:32:31.990  INFO 67924 --- [           main] c.h.s.s.ShutdowndemoApplication          : Started ShutdowndemoApplication in 2.323 seconds (JVM running for 2.906)
    2020-05-21 23:32:32.366  INFO 67924 --- [)-192.168.0.102] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
    2020-05-21 23:32:32.366  INFO 67924 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
    2020-05-21 23:32:32.372  INFO 67924 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet        : Completed initialization in 5 ms
    2020-05-21 23:32:41.034  INFO 67924 --- [      Thread-16] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
    2020-05-21 23:32:41:ShutDownController is destroyed
    
    Process finished with exit code 0
    

    应用结束了死循环被强制关闭,应用/testTomcatThreads该接口使用的是容器创建的线程,容器close时会强制关闭容器线程池的所有线程的任务。
    再测试一下自定义线程池
    curl -X GET http://localhost:6666/testOwnerThreads 后立马调用
    curl -X POST http://localhost:6666/actuator/shutdown,控制台输出:

    2020-05-21 23:34:07.266  INFO 67942 --- [           main] c.h.s.s.ShutdowndemoApplication          : Starting ShutdowndemoApplication on xianchengs-MacBook-Pro.local with PID 67942 (/Users/ding/Downloads/shutdowndemo-master/target/classes started by ding in /Users/ding/Downloads/shutdowndemo-master)
    2020-05-21 23:34:07.268  INFO 67942 --- [           main] c.h.s.s.ShutdowndemoApplication          : No active profile set, falling back to default profiles: default
    2020-05-21 23:34:08.419  INFO 67942 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 6666 (http)
    2020-05-21 23:34:08.439  INFO 67942 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
    2020-05-21 23:34:08.439  INFO 67942 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.22]
    2020-05-21 23:34:08.516  INFO 67942 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
    2020-05-21 23:34:08.516  INFO 67942 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1216 ms
    2020-05-21 23:34:08.883  INFO 67942 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
    2020-05-21 23:34:09.074  INFO 67942 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 1 endpoint(s) beneath base path '/actuator'
    2020-05-21 23:34:09.145  INFO 67942 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 6666 (http) with context path ''
    2020-05-21 23:34:09.149  INFO 67942 --- [           main] c.h.s.s.ShutdowndemoApplication          : Started ShutdowndemoApplication in 2.158 seconds (JVM running for 2.626)
    2020-05-21 23:34:09.214  INFO 67942 --- [)-192.168.0.102] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
    2020-05-21 23:34:09.214  INFO 67942 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
    2020-05-21 23:34:09.301  INFO 67942 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet        : Completed initialization in 86 ms
    2020-05-21 23:34:24.075  INFO 67942 --- [      Thread-15] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
    2020-05-21 23:34:24:ShutDownController is destroyed
    2020-05-21 23:34:54:自定义线程池执行完毕
    

    从日志中看到

    2020-05-21 23:34:24:ShutDownController is destroyed
    2020-05-21 23:34:54:自定义线程池执行完毕
    

    这两个相差30秒,正好是sleep的秒数,但是我们一直等待发现未正常关闭容器,始终没出现Process finished with exit code 0,明明线程已经执行完毕,为啥程序不能退出,经过定位发现,自定义线程池设置的最小核心线程个数为3:TRACK_LOG_EXECUTORS = new ThreadPoolExecutor(3, 5, 0L, TimeUnit.SECONDS, new LinkedBlockingDeque<>(200), threadFactory, new ThreadPoolExecutor.AbortPolicy()),虽然业务线程执行完毕,但是核心线程还在,将最小核心线程个数设置为0后,再测试一次容器正常关闭,或者自己手动去调用线程池的关闭接口

    @PreDestroy
        public void preDestroy() {
            /**
             * 关闭线程池
             */
            TRACK_LOG_EXECUTORS.shutdown();
            System.out.println(getCurrentDate()+":ShutDownController is destroyed");
        }
    

    重新测试后容器正常关闭退出

    分析原理:

    先找到定义/actuator/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 的处理一样
            this.context.close();
        }
    }
    

    this.context.close()调用的是AbstractApplicationContext类的close函数

    public void close() {
            synchronized(this.startupShutdownMonitor) {
                this.doClose();
                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. 获取程序启动时候的context,然后关闭主程序启动时的context

    curl -X GET http://localhost:6666/shutDownContext
    控制台输出结果:

    ...
    020-05-22 00:03:33.922  INFO 68220 --- [)-192.168.0.102] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
    2020-05-22 00:03:33.922  INFO 68220 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
    2020-05-22 00:03:34.013  INFO 68220 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet        : Completed initialization in 91 ms
    2020-05-22 00:03:43.607  INFO 68220 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
    2020-05-22 00:03:43:ShutDownController is destroyed
    
    Process finished with exit code 0
    

    3.通过钩子实现

    springboot启动时,AbstractApplicationContext类已经注册好钩子:

    public void registerShutdownHook() {
            if (this.shutdownHook == null) {
                this.shutdownHook = new Thread() {
                    public void run() {
                        synchronized(AbstractApplicationContext.this.startupShutdownMonitor) {
                            AbstractApplicationContext.this.doClose();
                        }
                    }
                };
                Runtime.getRuntime().addShutdownHook(this.shutdownHook);
            }
        }
    

    钩子函数里面也是调用的doClose函数,所以所有的方法底层原理都是一样的,只是触发的方式不同。
    这种方法原理是:在springboot启动的时候将进程号写入一个app.pid文件,生成的路径是可以指定的,可以通过命令 cat /Users/dxc/app.id | kill -15 命令直接停止服务(kill -9 不会触发钩子线程),这个时候bean对象的PreDestroy方法也会调用的,而且会自动调用钩子线程,控制台输出为:

    ...
    2020-05-22 00:11:59.248  INFO 68380 --- [)-192.168.0.102] o.s.web.servlet.DispatcherServlet        : Completed initialization in 80 ms
    2020-05-22 00:12:43.286  INFO 68380 --- [       Thread-6] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
    2020-05-22 00:12:43:ShutDownController is destroyed
    
    Process finished with exit code 143 (interrupted by signal 15: SIGTERM)
    

    总结

    优雅关闭的底层原理一致,调用springboot容器的close方法释放回收所有bean和容器创建的系统线程池applicationTaskExecutor,自定义线程池通过在代码类的@PreDestroy注解或者destory-method方法或者DisposableBean接口进行释放操作,来优雅地关闭容器

    参考文章:
    https://www.cnblogs.com/huangqingshi/p/11370291.html
    https://cloud.tencent.com/developer/article/1629897

    相关文章

      网友评论

          本文标题:springboot优雅关闭应用详解

          本文链接:https://www.haomeiwen.com/subject/savzohtx.html