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分钟之后进程再停止,这些问题等研究明白,再来一篇续。如果又哪位同学能解答我的疑惑,请在评论区留言。

  • 相关阅读:
    Apache 虚拟主机 VirtualHost 配置
    EAX、ECX、EDX、EBX寄存器的作用
    Python中文文档 目录(转载)
    八度
    POJ 3268 Silver Cow Party (最短路)
    POJ 2253 Frogger (求每条路径中最大值的最小值,Dijkstra变形)
    2013金山西山居创意游戏程序挑战赛——复赛(1) HDU 4557 非诚勿扰 HDU 4558 剑侠情缘 HDU 4559 涂色游戏 HDU 4560 我是歌手
    HDU 4549 M斐波那契数列(矩阵快速幂+欧拉定理)
    UVA 11624 Fire! (简单图论基础)
    HDU 3534 Tree (树形DP)
  • 原文地址:https://www.cnblogs.com/jpfss/p/10860390.html
Copyright © 2011-2022 走看看