Tomcat,SpringBoot,Docker 的优雅关闭

转自https://my.oschina.net/pengranxiang/blog/4297551

Java,Tomcat,SpringBoot,Docker 的优雅关闭

1 什么是优雅关闭

@RestController
public class DemoController {
 @GetMapping("/demo")
 public String demo() throws InterruptedException {
     // 模拟业务耗时处理流程
  Thread.sleep(20 * 1000L);
  return "hello";
 }
}

当我们流量请求到此接口执行业务逻辑的时候,若服务端此时执行关机(kill),SpringBoot默认会直接关闭容器(Tomcat等),导致此业务逻辑执行失败。在一些业务场景下,会出现业务无法回滚,导致数据不一致的情况。

优雅关闭表示:当收到kill命令后,服务将拒绝新的请求,但会继续完成已有请求的处理.

2. 如何做到优雅关闭

2.1 Linux Kill 命令

kill 命令常用的信号选项:

  1. kill -2 pid 向指定 pid 发送 SIGINT 中断信号, 等同于 ctrl+c.
  2. kill -9 pid, 向指定 pid 发送 SIGKILL 立即终止信号.
  3. kill -15 pid, 向指定 pid 发送 SIGTERM 终止信号.
  4. kill pid 等同于 kill 15 pid

SIGINT/SIGKILL/SIGTERM 信号的区别:

  • SIGINT (ctrl+c) 信号 (信号编号为 2), 信号会被当前进程树接收到, 也就说, 不仅当前进程会收到该信号, 而且它的子进程也会收到.
  • SIGKILL 信号 (信号编号为 9), 程序不能捕获该信号, 最粗暴最快速结束程序的方法.
  • SIGTERM 信号 (信号编号为 15), 信号会被当前进程接收到, 但它的子进程不会收到, 如果当前进程被 kill 掉, 它的的子进程的父进程将变成 init 进程 (init 进程是那个 pid 为 1 的进程)

2.2 Java 进程的优雅关闭

Java 语言底层有机制能捕获到 OS 的 SIGINT (kill -2 / ctrl + c) / SIGTERM (kill -15)信号。注意 SIGKILL(kill -9)信号无法捕捉。

通过 Runtime.getRuntime().addShutdownHook() 向 JVM 中注册一个 ShutdownHook 线程,当 JVM 收到停止信号后,该线程将被激活运行。

可以在Hook线程向其他线程发出中断指令,然后等待其他线程执行完毕,进而优雅地关闭整个程序。

public class ShutDown {

    public static void main(String[] args) {
        System.out.println("MainThread 启动");

        final Thread mainThread = Thread.currentThread();

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("接收到关闭信号");

            // 给主线程发送中断信号
            mainThread.interrupt();

            try {
                // 等待主线程正常执行完成
                mainThread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("优雅关闭完成");
        }));

        try {
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            // 中断响应
            System.out.println("主线程被中断,处理中断逻辑");
        }

        System.out.println("Main Thread执行完毕");
    }
}

启动后,按 ctrl + c 的执行结果: 

ShutdownHook 的使用注意点:

  • shutdownhook的调用是不保证顺序的
  • shutdownhook是JVM结束前调用的线程,所以该线程中的方法应尽量短,并且保证不能发生死锁的情况,否则也会阻止JVM的正常退出
  • shutdownhook中不能执行System.exit(),否则会导致虚拟机卡住,而不得不强行杀死进程

2.3 Tomcat 的优雅关闭

2.3.1 Tomcat 启动过程

Tomcat启动的入口是Bootstrap类中的main方法,而后根据server.xml中的配置,对Server、Service、Enigin、Connector、Host、Context等组件进行初始化,之后便是启动这些组件。

2.3.2 Tomcat 关闭过程

2.3.2.1 shutdown 脚本

shutdown 主要做了两件事:

  1. 初始化Server组件,和Tomcat启动时类似,这一步主要是解析server.xml文件,然后根据server.xml中的属性初始化Tomcat组件的成员变量,这里主要关注Server组件的几个成员变量:port、address、shutdown,默认值分别为8005、127.0.0.1、SHUTDOWN等,需要和启动时读取的server.xml保持一致。
  2. 往address port所监听的Socket端口发生“SHUTDOWN”字符串。对应启动的第三种阻塞情况,"SHUTDOWN"字符串让main主线程结束了等待状态,并在接下来通过调用各组件的stop()和destroy()方法进行资源的释放。

我们的线程是在loader中被尝试停止的,而loader的stop方法在listenerStop方法之后,也就是说,即使loader成功终止了用户自己启动的线程,依然有可能在线程终止之前使用Sping框架,而此时Spring框架已经在Listener中关闭了!况且在loader的清理线程过程中只有配置了clearReferencesStopThreads参数,用户自己启动的线程才会被强制终止(使用Thread.stop()),而在大多数情况下,为了保证数据的完整性,这个参数不会被配置。也就是说,在WebApp中,用户自己启动的线程(包括Executors),都不会因为容器的退出而终止。

2.3.2.2 Kill 命令

当 server.xml 的 Server 节点的 port 属性配置为 -1 时, shutdown脚本将无法停止Tomcat。当 port 为正常端口的时候,只要用户有权限能访问该端口,并向其发送“SHUTDOWN”,则服务会被关闭,有一定的风险。所以生产环境中也经常配置为 -1。

在这种情况下,可以使用 kill pid 或 kill -15 pid 来触发 Tomcat 优雅关闭。

Tomcat的关闭钩子的定义是在Catalina类中,有一个名为CatalinaShutdownHook内部类,继承了Thread类。跟着这个线程类中的run()方法往下看,其调用了Catalina的stop()方法,而此处stop方法,除了正常去停止各组件外,还会去中断并快速结束main主线程(如果主线程还存在的话),最后再调用各组件的destroy()方法进行资源释放。

protected class CatalinaShutdownHook extends Thread {
  public void run() {
    if (server != null) {
      try { 
        ((Lifecycle) server).stop(); 
      } catch (LifecycleException e) { 
        System.out.println("Catalina.stop: " + e);
        e.printStackTrace(System.out); 
        if (e.getThrowable() != null) {
          System.out.println("----- Root Cause -----"); 
          e.getThrowable().printStackTrace(System.out); 
        }
      } 
    }
  }
}

2.4 SpringBoot 的优雅关闭

SpringBoot Web 项目, 如果使用的是外置 tomcat, 可以直接使用上面 tomcat 命令完成优雅停机. 但通常使用的是内置 tomcat 服务器, 这时就需要编写代码来支持优雅停止.

2.4.1 SpringBoot 2.3.0 之前

网上很多文章都提及 Actuator 的 shutdown 提供优雅停机功能, 官方文档也是这么宣传的, 其实并没有实现优雅停机功能, 在 github issues/4657 也有提及, 也许将来会实现, https://github.com/spring-projects/spring-boot/issues/4657

测试代码(SpringBoot版本:2.1.5.RELEASE):(摘自:https://dzone.com/articles/graceful-shutdown-spring-boot-applications )

@RequestMapping("/long-process")
public String pause() throws InterruptedException {
  Thread.sleep(20*1000);
  System.out.println("Process finished");
  return "Process finished";
}

appication.properties 文件内容:

management.endpoint.shutdown.enabled=true
management.endpoints.web.exposure.include=*

测试步骤:

  1. POST http://localhost:8080/long-process , 紧接访问actuator shutdown 
  2. 端点: POST http://localhost:8080/actuator/shutdown , 当应用程序停止时, GET请求并没有得到返回值, 可见 Actuator 并没有提供优雅停机功能. 
  3. 服务报错 

2.4.1.1 增加 GracefulShutdown Connector 监听类

当 tomcat 收到 kill 信号后, web程序先关闭新的请求, 然后等待 30 秒, 最后结束整个程序.

public 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();
            }
        }
    }
}

2.4.1.2 注册自定义的 Connector 监听器

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @Bean
    public GracefulShutdown gracefulShutdown() {
        return new GracefulShutdown();
    }

    @Bean
    public ConfigurableServletWebServerFactory webServerFactory(final GracefulShutdown gracefulShutdown) {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.addConnectorCustomizers(gracefulShutdown);
        return factory;
    }
}

测试步骤:

  1. POST http://localhost:8080/long-process , 紧接访问actuator shutdown 
  2. 端点: POST http://localhost:8080/actuator/shutdown ,访问立即响应,但是服务未停止,等待 long-process 执行完成 
  3. 控制台日志: 

2.4.2 SpringBoot 2.3.0 之后

在最新的 SpringBoot 2.3.0 版本中,正式内置了优雅停机功能,不需要再自行扩展线程池来处理。

当启动server.shutdown=graceful,在 web 容器关闭时,web服务器将不再接受新请求(Tomcat、Reactor Netty : 等待超时,Undertow:直接返回503),并等待活动请求完成的缓冲时间。

# 开启优雅停机,默认值:immediate 为立即关闭
server.shutdown=graceful

# 设置缓冲期,最大等待时间,默认:30秒
spring.lifecycle.timeout-per-shutdown-phase=60s

测试代码回到 2.4.1 小节,SpringBoot版本2.3.0,测试步骤:

    1. POST http://localhost:8080/long-process , 紧接访问actuator shutdown 
  1. 端点: POST http://localhost:8080/actuator/shutdown ,访问立即响应,但是服务未停止,等待 long-process 执行完成 
  2. 控制台日志: 

2.5 Docker 服务的优雅关闭

使用 docker stop 关闭容器时, 只有 init(pid 1)进程能收到中断信号, 如果容器的pid 1 进程是 sh 进程, 它不具备转发结束信号到它的子进程的能力, 所以我们真正的java程序得不到中断信号, 也就不能实现优雅关闭. 解决思路是: 让pid 1 进程具备转发终止信号, 或者将 java 程序配成 pid 1 进程.

需要说明的是, docker stop 默认是等待10秒钟, 这个时间有点太短了, 可以加 -t 参数, 比如 -t 30 等待30秒钟.

上面的 Dockerfile 的pid 1是一个 sh 命令,并不能实现优雅关闭, 需要再改进.

ENTRYPOINT 的几种写法, 会影响 pid 1 进程的产生:

  1. ENTRYPOINT top -b
    • PID 1 是 /bin/sh -c shell top -b
    • 另外有个 pid 7 是 top -b
  2. ENTRYPOINT exec top -b
    • PID 1 是 top -b
  3. ENTRYPOINT ["top", "-b"]
    • PID 1 是 top -b

3 参考资料

  1. http://blog.itpub.net/69912579/viewspace-2675115/
  2. https://mp.weixin.qq.com/s/oMBpwju6p6VXZHJDhd4JNA
  3. https://my.oschina.net/u/2552286/blog/3039592
  4. https://dzone.com/articles/graceful-shutdown-spring-boot-applications
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
要在Spring Boot中使用Docker,您可以按照以下步骤进行操作: 1. 首先,确保您的主机已经安装并启动了Docker。您可以使用以下命令检查Docker的版本和状态: ``` docker -v ``` 2. 接下来,您需要构建一个Spring Boot应用程序的Docker镜像。您可以使用Dockerfile来定义构建镜像的步骤和依赖项。在Dockerfile中,您可以指定要使用的基础镜像、将应用程序打包为JAR文件的命令以及其他配置选项。 3. 在构建镜像之前,确保您的Spring Boot应用程序已经被打包为可执行的JAR文件。您可以使用以下命令进行打包: ``` mvn clean package ``` 4. 创建一个Dockerfile并将其放置在Spring Boot应用程序的根目录中。在Dockerfile中,您可以使用以下命令来构建镜像: ``` FROM openjdk:8-jdk-alpine COPY target/my-application.jar app.jar ENTRYPOINT ["java","-jar","/app.jar"] ``` 5. 使用Docker命令来构建镜像。在终端中,进入到应用程序的根目录,并运行以下命令: ``` docker build -t my-application . ``` 6. 构建镜像完成后,您可以使用以下命令来运行Spring Boot应用程序的Docker容器: ``` docker run -p 8080:8080 my-application ``` 7. 现在,您的Spring Boot应用程序已经在Docker容器中运行,并且可以通过访问`http://localhost:8080`来访问它。 请注意,这只是使用Docker部署Spring Boot应用程序的基本步骤。根据您的需求,您可能需要进行更多的配置和调整。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [SpringBootDocker](https://blog.csdn.net/qq_45738810/article/details/108572410)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [Spring Boot系列第五篇:Spring Boot与Docker](https://blog.csdn.net/qq_29445811/article/details/117035658)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值