springboot优雅关闭应用详解

2020-05-22  本文已影响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

上一篇下一篇

猜你喜欢

热点阅读