Springboot 程序正常退出问题

场景

通过 jenkins 调用 springboot 任务,任务执行完成之后,jenkins 没有收到任务结束信号,导致定时任务无法多次执行。

image-20210720105541958

image-20210720105724217

分析

  1. 去掉业务调用逻辑,测试程序是否正常退出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ConfigurableApplicationContext applicationContext = SpringApplication
    .run(SurfContentSyncApplication.class, args);
    try {

    log.info("surf-content-sync success!!!");

    //HotVideoSyncService bean = applicationContext.getBean(HotVideoSyncService.class);
    //bean.doSyncHotVideo();

    } catch (Throwable e) {
    log.error("error", e);
    }
    SpringApplication.exit(applicationContext, () -> 0);
    log.info("surf-content-sync end!!!");
  2. 通过 jenkins 调用之后, 程序正常退出了。怀疑是业务逻辑,检查日志没有发现异常,只有 mongodb 有一点疑似异常

    1
    2021-07-19 08:09:25.874  INFO 1 --- [           main] org.mongodb.driver.connection            : Closed connection [connectionId{localValue:239}] to recommend-shard-01-02-hwunn.mongodb.net:27016 because the pool has been closed.
  3. 检查一遍之后,无发现有效信息 ,查阅资料之后发现可以通过强制结束 jvm 来实现退出效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ConfigurableApplicationContext applicationContext = SpringApplication
    .run(SurfContentSyncApplication.class, args);
    try {

    log.info("surf-content-sync success!!!");

    HotVideoSyncService bean = applicationContext.getBean(HotVideoSyncService.class);
    bean.doSyncHotVideo();

    } catch (Throwable e) {
    log.error("error", e);
    }
    System.exit(0);
    log.info("surf-content-sync end!!!");
  4. 业务执行逻辑之后,jenkins 正常识别

思考

为什么去掉业务逻辑之后,可以使用 SpringApplication.exit(applicationContext, () -> 0),加了业务逻辑之后就不可以了。

  1. 首先需要明确的是,jvm 是否退出程序的判断条件是当前进程中是否还有存用用户线程还在运行。守护线程不会影响程序的退出(如 gc 线程)

  2. 下为什么 启动 springboot web 的模块之后,启动主函数后程序没有自动停止,在于调用 run 方法之后通过不断轮询来保持用户线程的运行。

    启动过程部分代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
      ConfigurableApplicationContext applicationContext = SpringApplication
    .run(SurfContentSyncApplication.class, args);


    public ConfigurableApplicationContext run(String... args) {
    ...
    refreshContext(context);
    ...
    return context;
    }


    public void refresh() throws BeansException, IllegalStateException {
    ...
    onRefresh();
    ...
    }

    protected void onRefresh() {
    super.onRefresh();
    try {
    createWebServer();
    }
    catch (Throwable ex) {
    throw new ApplicationContextException("Unable to start web server", ex);
    }
    }


    private void createWebServer() {
    WebServer webServer = this.webServer;
    ServletContext servletContext = getServletContext();
    if (webServer == null && servletContext == null) {
    ServletWebServerFactory factory = getWebServerFactory();
    this.webServer = factory.getWebServer(getSelfInitializer());
    }
    else if (servletContext != null) {
    try {
    getSelfInitializer().onStartup(servletContext);
    }
    catch (ServletException ex) {
    throw new ApplicationContextException("Cannot initialize servlet context",
    ex);
    }
    }
    initPropertySources();
    }


    public WebServer getWebServer(ServletContextInitializer... initializers) {
    ...
    return getTomcatWebServer(tomcat);
    }

    public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
    Assert.notNull(tomcat, "Tomcat Server must not be null");
    this.tomcat = tomcat;
    this.autoStart = autoStart;
    initialize();
    }


    private void initialize() throws WebServerException {
    ...

    // Unlike Jetty, all Tomcat threads are daemon threads. We create a
    // blocking non-daemon to stop immediate shutdown
    startDaemonAwaitThread();
    ...
    }

    private void startDaemonAwaitThread() {
    Thread awaitThread = new Thread("container-" + (containerCounter.get())) {

    @Override
    public void run() {
    TomcatWebServer.this.tomcat.getServer().await();
    }

    };
    awaitThread.setContextClassLoader(getClass().getClassLoader());
    awaitThread.setDaemon(false);
    awaitThread.start();
    }

    public void await() {
    // Negative values - don't wait on port - tomcat is embedded or we just don't like ports
    if (getPortWithOffset() == -2) {
    // undocumented yet - for embedding apps that are around, alive.
    return;
    }
    if (getPortWithOffset() == -1) {
    try {
    awaitThread = Thread.currentThread();
    while(!stopAwait) {
    try {
    Thread.sleep( 10000 );
    } catch( InterruptedException ex ) {
    // continue and check the flag
    }
    }
    } finally {
    awaitThread = null;
    }
    return;
    }
    }

    //部分代码省略了

    最主要的逻辑在 startDaemonAwaitThread 这个方法里面,创建了一个用户线程,run 方法里面不断轮询 stopAwait 状态标示,来保证线程活跃

  3. SpringApplication.exit(applicationContext, () -> 0); 这个方法主要的逻辑是正常停止 spring 容器,销毁bean、执行destroy方法等 ,在 close 方法中,调用TomcatWebServer 中的 stopTomcat 方法,最后在 stopInternal 方法中将 stopAwait 标示改成 true,终止用户线程。

  4. 业务逻辑主要是调用线程池来执行大量同步数据操作。

测试

  1. 增加主动销毁线程池的操作

    1
    2
    3
    4
    5
    6
    7
    @PreDestroy
    public void destroyThread() throws InterruptedException {
    if(executorService != null){
    executorService.shutdown();
    executorService.awaitTermination(5,TimeUnit.SECONDS);
    }
    }

    发现程序能够正常退出了,可以判断是线程池没有关闭,导致线程池中的闲置线程作为用户线程来使 jvm 不能正常退出。

  2. SpringApplication.exit(applicationContext, () -> 0); 方法里面明明有 destroyBeans 方法,按理说应该会主动销毁线程池的,为什么没有,定位线程池创建步骤。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private ExecutorService executorService;

    {
    executorService = new ThreadPoolExecutor(100, 200,
    60l, TimeUnit.MINUTES, new ArrayBlockingQueue(1000), (r, executor) -> {
    if (!executor.isShutdown()) {
    try {
    executor.getQueue().put(r);
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    }
    });
    }

    发现这个是直接通过代码块来创建线程池对象的,并不是由 spring 容器来管理的。

  3. 修改线程池的创建方式,测试。程序正常退出了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Autowired
    private ExecutorService executorService;

    @Bean
    public ExecutorService service() {
    return new ThreadPoolExecutor(100, 200,
    60l, TimeUnit.MINUTES, new ArrayBlockingQueue(1000), (r, executor) -> {
    if (!executor.isShutdown()) {
    try {
    executor.getQueue().put(r);
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    }
    });
    }

总结

  1. jvm 程序能否正常退出的判定标准是当前是否有还存在的用户线程

  2. 终结程序的方法可以通过 SpringApplication.exit() 优雅关闭,也可以通过System.exit(0) 来强制关闭。

  3. SpringApplication.exit() 方法会销毁 spring 容器中的 bean,并关闭 tomcat 服务器。但是如果有对象不在 spring 容器中管理,就会出现没有被销毁的情况。

  4. 创建对象的时候,应该在 spring 的容器中创建,让 spring 来管理 bean 的生命周期,不要在代码块和静态代码块中执行