zoukankan      html  css  js  c++  java
  • springboot优雅关机

    Spring boot 2.0 之优雅停机

    96 rabbitGYK 关注

    2018.05.20 18:41* 字数 1794 阅读 2638评论 0喜欢 22

    spring boot 框架在生产环境使用的有一段时间了,它“约定大于配置”的特性,体现了优雅流畅的开发过程,它的部署启动方式(java -jar xxx.jar)也很优雅。但是我使用的停止应用的方式是 kill -9 进程号,即使写了脚本,还是显得有些粗鲁。这样的应用停止方式,在停止的那一霎那,应用中正在处理的业务逻辑会被中断,导致产生业务异常情形。这种情况如何避免,本文介绍的优雅停机,将完美解决该问题。

    00 前言

    什么叫优雅停机?简单说就是,在对应用进程发送停止指令之后,能保证正在执行的业务操作不受影响。应用接收到停止指令之后的步骤应该是,停止接收访问请求,等待已经接收到的请求处理完成,并能成功返回,这时才真正停止应用。

    这种完美的应用停止方式如何实现呢?就Java语言生态来说,底层的技术是支持的,所以我们才能实现在Java语言之上的各个web容器的优雅停机。

    在普通的外置的tomcat中,有shutdown脚本提供优雅的停机机制,但是我们在使用Spring boot的过程中发现web容器都是内置(当然也可使用外置,但是不推荐),这种方式提供简单的应用启动方式,方便的管理机制,非常适用于微服务应用中,但是默认没有提供优雅停机的方式。这也是本文探索这个问题的根本原因。

    应用是否是实现了优雅停机,如何才能验证呢?这需要一个处理时间较长的业务逻辑,模拟这样的逻辑应该很简单,使用线程sleep或者长时间循环。我的模拟业务逻辑代码如下:

    1. @GetMapping(value = "/sleep/one", produces = "application/json")
    2. public ResultEntity<Long> sleepOne(String systemNo){
    3. logger.info("模拟业务处理1分钟,请求参数:{}", systemNo);
    4. Long serverTime = System.currentTimeMillis();
    5. // try {
    6. // Thread.sleep(60*1000L);
    7. // } catch (InterruptedException e) {
    8. // e.printStackTrace();
    9. // }
    10. while (System.currentTimeMillis() < serverTime + (60 * 1000)){
    11. logger.info("正在处理业务,当前时间:{},开始时间:{}", System.currentTimeMillis(), serverTime);
    12. }
    13. ResultEntity<Long> resultEntity = new ResultEntity<>(serverTime);
    14. logger.info("模拟业务处理1分钟,响应参数:{}", resultEntity);
    15. return resultEntity;
    16. }

    验证方式就是,在触发这个接口的业务处理之后,业务逻辑处理时间长达1分钟,需要在处理结束前,发起停止指令,验证是否能够正常返回。验证时所使用的kill指令:kill -2(Ctrl + C)kill -15kill -9

    01 Java 语言的优雅停机

    从上面的介绍中我们发现,Java语言本身是支持优雅停机的,这里就先介绍一下普通的java应用是如何实现优雅停止的。

    当我们使用kill PID的方式结束一个Java应用的时候,JVM会收到一个停止信号,然后执行shutdownHook的线程。一个实现示例如下:

    1. public class ShutdownHook extends Thread {
    2. private Thread mainThread;
    3. private boolean shutDownSignalReceived;
    4. @Override
    5. public void run() {
    6. System.out.println("Shut down signal received.");
    7. this.shutDownSignalReceived=true;
    8. mainThread.interrupt();
    9. try {
    10. mainThread.join(); //当收到停止信号时,等待mainThread的执行完成
    11. } catch (InterruptedException e) {
    12. }
    13. System.out.println("Shut down complete.");
    14. }
    15. public ShutdownHook(Thread mainThread) {
    16. super();
    17. this.mainThread = mainThread;
    18. this.shutDownSignalReceived = false;
    19. Runtime.getRuntime().addShutdownHook(this);
    20. }
    21. public boolean shouldShutDown(){
    22. return shutDownSignalReceived;
    23. }
    24. }

    其中关键语句Runtime.getRuntime().addShutdownHook(this);,注册一个JVM关闭的钩子,这个钩子可以在以下几种场景被调用:

    1. 程序正常退出
    2. 使用System.exit()
    3. 终端使用Ctrl+C触发的中断
    4. 系统关闭
    5. 使用Kill pid命令干掉进程

    测试shutdownHook的功能,代码示例:

    1. public class TestMain {
    2. private ShutdownHook shutdownHook;
    3. public static void main( String[] args ) {
    4. TestMain app = new TestMain();
    5. System.out.println( "Hello World!" );
    6. app.execute();
    7. System.out.println( "End of main()" );
    8. }
    9. public TestMain(){
    10. this.shutdownHook = new ShutdownHook(Thread.currentThread());
    11. }
    12. public void execute(){
    13. while(!shutdownHook.shouldShutDown()){
    14. System.out.println("I am sleep");
    15. try {
    16. Thread.sleep(1*1000);
    17. } catch (InterruptedException e) {
    18. System.out.println("execute() interrupted");
    19. }
    20. System.out.println("I am not sleep");
    21. }
    22. System.out.println("end of execute()");
    23. }
    24. }

    启动测试代码,之后再发送一个中断信号,控制台输出:

    1. I am sleep
    2. I am not sleep
    3. I am sleep
    4. I am not sleep
    5. I am sleep
    6. I am not sleep
    7. I am sleep
    8. Shut down signal received.
    9. execute() interrupted
    10. I am not sleep
    11. end of execute()
    12. End of main()
    13. Shut down complete.
    14. Process finished with exit code 130 (interrupted by signal 2: SIGINT)

    可以看出,在接收到中断信号之后,整个main函数是执行完成的。

    02 actuator/shutdown of Spring boot

    我们知道了java本身在支持优雅停机上的能力,然后在Spring boot中又发现了actuator/shutdown的管理端点。于是我把优雅停机的功能寄希望于此,开始配置测试,开启配置如下:

    1. management:
    2. server:
    3. port: 10212
    4. servlet:
    5. context-path: /
    6. ssl:
    7. enabled: false
    8. endpoints:
    9. web:
    10. exposure:
    11. include: "*"
    12. endpoint:
    13. health:
    14. show-details: always
    15. shutdown:
    16. enabled: true #启用shutdown端点

    测试结果很失望,并没有实现优雅停机的功能,就是将普通的kill命令,做成了HTTP端点。于是开始查看Spring boot的官方文档和源代码,试图找到它的原因。

    在官方文档上对shutdown端点的介绍:

    shutdown    Lets the application be gracefully shutdown.
    

    从此介绍可以看出,设计上应该是支持优雅停机的。但是为什么现在还不够优雅,在github上托管的Spring boot项目中发现,有一个issue一直处于打开状态,已经两年多了,里面很多讨论,看完之后发现在Spring boot中完美的支持优雅停机不是一件容易的事,首先Spring boot支持web容器很多,其次对什么样的实现才是真正的优雅停机,讨论了很多。想了解更多的同学,把这个issue好好阅读一下。

    这个issue中还有一个重要信息,就是这个issue曾经被加入到2.0.0的milestone中,后来由于没有完成又移除了,现在状态是被添加在2.1.0的milestone中。我测试的版本是2.0.1,期待官方给出完美的优雅停机方案。

    03 Spring boot 优雅停机

    虽然官方暂时还没有提供优雅停机的支持,但是我们为了减少进程停止对业务的影响,还是要给出能满足基本需求的方案来。

    针对tomcat的解决方案是:

    1. package com.epay.demox.unipay.provider;
    2. import org.apache.catalina.connector.Connector;
    3. import org.slf4j.Logger;
    4. import org.slf4j.LoggerFactory;
    5. import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
    6. import org.springframework.context.ApplicationListener;
    7. import org.springframework.context.event.ContextClosedEvent;
    8. import org.springframework.stereotype.Component;
    9. import java.util.concurrent.Executor;
    10. import java.util.concurrent.ThreadPoolExecutor;
    11. import java.util.concurrent.TimeUnit;
    12. /**
    13. * @Author: guoyankui
    14. * @DATE: 2018/5/20 12:59 PM
    15. *
    16. * 优雅关闭 Spring Boot tomcat
    17. */
    18. @Component
    19. public class GracefulShutdownTomcat implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
    20. private final Logger log = LoggerFactory.getLogger(GracefulShutdownTomcat.class);
    21. private volatile Connector connector;
    22. private final int waitTime = 30;
    23. @Override
    24. public void customize(Connector connector) {
    25. this.connector = connector;
    26. }
    27. @Override
    28. public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
    29. this.connector.pause();
    30. Executor executor = this.connector.getProtocolHandler().getExecutor();
    31. if (executor instanceof ThreadPoolExecutor) {
    32. try {
    33. ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
    34. threadPoolExecutor.shutdown();
    35. if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
    36. log.warn("Tomcat thread pool did not shut down gracefully within " + waitTime + " seconds. Proceeding with forceful shutdown");
    37. }
    38. } catch (InterruptedException ex) {
    39. Thread.currentThread().interrupt();
    40. }
    41. }
    42. }
    43. }
    1. public class UnipayProviderApplication {
    2. public static void main(String[] args) {
    3. SpringApplication.run(UnipayProviderApplication.class);
    4. }
    5. @Autowired
    6. private GracefulShutdownTomcat gracefulShutdownTomcat;
    7. @Bean
    8. public ServletWebServerFactory servletContainer() {
    9. TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
    10. tomcat.addConnectorCustomizers(gracefulShutdownTomcat);
    11. return tomcat;
    12. }
    13. }

    该方案的代码来自官方issue中的讨论,添加这些代码到你的Spring boot项目中,然后再重新启动之后,发起测试请求,然后发送kill停止指令(kill -2(Ctrl + C)kill -15)。测试结果:

    1. Spring boot的健康检查,为UP
    2. 正在执行操作不会终止,直到执行完成。
    3. 不再接收新的请求,客户端报错信息为:Connection reset by peer
    4. 最后正常终止进程(业务执行完成后,立即进程停止)。

    从测试结果来看,是满足我们的需求的。当然如果发送指令kill -9,进程会立即停止。

    针对undertow的解决方案是:

    1. package com.epay.demox.unipay.provider;
    2. import io.undertow.Undertow;
    3. import io.undertow.server.ConnectorStatistics;
    4. import org.springframework.beans.factory.annotation.Autowired;
    5. import org.springframework.boot.web.embedded.undertow.UndertowServletWebServer;
    6. import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
    7. import org.springframework.context.ApplicationListener;
    8. import org.springframework.context.event.ContextClosedEvent;
    9. import org.springframework.stereotype.Component;
    10. import java.lang.reflect.Field;
    11. import java.util.List;
    12. /**
    13. * @Author: guoyankui
    14. * @DATE: 2018/5/20 5:47 PM
    15. *
    16. * 优雅关闭 Spring Boot undertow
    17. */
    18. @Component
    19. public class GracefulShutdownUndertow implements ApplicationListener<ContextClosedEvent> {
    20. @Autowired
    21. private GracefulShutdownUndertowWrapper gracefulShutdownUndertowWrapper;
    22. @Autowired
    23. private ServletWebServerApplicationContext context;
    24. @Override
    25. public void onApplicationEvent(ContextClosedEvent contextClosedEvent){
    26. gracefulShutdownUndertowWrapper.getGracefulShutdownHandler().shutdown();
    27. try {
    28. UndertowServletWebServer webServer = (UndertowServletWebServer)context.getWebServer();
    29. Field field = webServer.getClass().getDeclaredField("undertow");
    30. field.setAccessible(true);
    31. Undertow undertow = (Undertow) field.get(webServer);
    32. List<Undertow.ListenerInfo> listenerInfo = undertow.getListenerInfo();
    33. Undertow.ListenerInfo listener = listenerInfo.get(0);
    34. ConnectorStatistics connectorStatistics = listener.getConnectorStatistics();
    35. while (connectorStatistics.getActiveConnections() > 0){}
    36. }catch (Exception e){
    37. // Application Shutdown
    38. }
    39. }
    40. }
    1. package com.epay.demox.unipay.provider;
    2. import io.undertow.server.HandlerWrapper;
    3. import io.undertow.server.HttpHandler;
    4. import io.undertow.server.handlers.GracefulShutdownHandler;
    5. import org.springframework.stereotype.Component;
    6. /**
    7. * @Author: guoyankui
    8. * @DATE: 2018/5/20 5:50 PM
    9. */
    10. @Component
    11. public class GracefulShutdownUndertowWrapper implements HandlerWrapper {
    12. private GracefulShutdownHandler gracefulShutdownHandler;
    13. @Override
    14. public HttpHandler wrap(HttpHandler handler) {
    15. if(gracefulShutdownHandler == null) {
    16. this.gracefulShutdownHandler = new GracefulShutdownHandler(handler);
    17. }
    18. return gracefulShutdownHandler;
    19. }
    20. public GracefulShutdownHandler getGracefulShutdownHandler() {
    21. return gracefulShutdownHandler;
    22. }
    23. }
    1. public class UnipayProviderApplication {
    2. public static void main(String[] args) {
    3. SpringApplication.run(UnipayProviderApplication.class);
    4. }
    5. @Autowired
    6. private GracefulShutdownUndertowWrapper gracefulShutdownUndertowWrapper;
    7. @Bean
    8. public UndertowServletWebServerFactory servletWebServerFactory() {
    9. UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();
    10. factory.addDeploymentInfoCustomizers(deploymentInfo -> deploymentInfo.addOuterHandlerChainWrapper(gracefulShutdownUndertowWrapper));
    11. factory.addBuilderCustomizers(builder -> builder.setServerOption(UndertowOptions.ENABLE_STATISTICS, true));
    12. return factory;
    13. }
    14. }

    该方法参考文章,采用与tomcat同样的测试方案,测试结果:

    1. Spring boot的健康检查,为UP
    2. 正在执行操作不会终止,直到执行完成。
    3. 不再接收新的请求,客户端报错信息为:503 Service Unavailable
    4. 最后正常终止进程(在业务执行完成后的一分钟进程停止)。

    04 结束

    到此为止,对Java和Spring boot应用的优雅停机机制有了基本的认识。虽然实现了需求,但是这其中还有很多知识点需要探索,比如Spring上下文监听器,上下文关闭事件等,还有undertow提供的GracefulShutdownHandler的原理是什么,为什么是1分钟之后进程再停止,这些问题等研究明白,再来一篇续。如果又哪位同学能解答我的疑惑,请在评论区留言。

  • 相关阅读:
    嵌入式系统及应用-知识点总结
    C语言程序设计(基础)- 第14、15周作业
    数组章节知识点
    单词长度试题的分析
    C语言程序设计(基础)- 第7周作业(新)
    C语言程序设计(基础)- 第7周作业
    C语言程序设计(基础)- 第6周作业
    C语言程序设计(基础)- 第4周作业
    北京大学信息科学技术学院本科生课程体系课程大纲选登——数据结构与算法
    北京大学信息科学技术学院本科生课程体系课程大纲选摘-程序设计基础(大一上学期课程)
  • 原文地址:https://www.cnblogs.com/jpfss/p/10860390.html
Copyright © 2011-2022 走看看