springboot springcloud实现平滑上线 gracefully退出

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/singgel/article/details/88615330

背景介绍:易车的推荐系统是有后端的推荐算法,推荐数据流通过接口将推荐数据传递到中台,中台的同学将推荐团队的数据进行封装,传递至各个业务线的前端展示,中间就导致了一些线上的问题,推荐系统的api 是采用的SpringCloud的技术栈来实现的,api 的接口服务在升级的时候,不可避免的要进行重启,但是线上重启如果不能够平滑的实现的话,就将会导致线上事故,推荐系统的下游调用方,如果没有做服务降级和异常处理的话,将会导致用户的正常请求因为api 的重启显示服务异常,为此,需要寻找像Tomcat和nginx一样平滑重启方式。

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

  • 00 前言

什么叫优雅停机?简单说就是,在对应用进程发送停止指令之后,能保证正在执行的业务操作不受影响。应用接收到停止指令之后的步骤应该是,停止接收访问请求,等待已经接收到的请求处理完成,并能成功返回,这时才真正停止应用。
这种完美的应用停止方式如何实现呢?就Java语言生态来说,底层的技术是支持的,所以我们才能实现在Java语言之上的各个web容器的优雅停机。
在普通的外置的tomcat中,有shutdown脚本提供优雅的停机机制,但是我们在使用Spring boot的过程中发现web容器都是内置(当然也可使用外置,但是不推荐),这种方式提供简单的应用启动方式,方便的管理机制,非常适用于微服务应用中,但是默认没有提供优雅停机的方式。这也是本文探索这个问题的根本原因。
应用是否是实现了优雅停机,如何才能验证呢?这需要一个处理时间较长的业务逻辑,模拟这样的逻辑应该很简单,使用线程sleep或者长时间循环。我的模拟业务逻辑代码如下:

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

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

  • 01 Java 语言的优雅停机

从上面的介绍中我们发现,Java语言本身是支持优雅停机的,这里就先介绍一下普通的java应用是如何实现优雅停止的。
当我们使用kill PID的方式结束一个Java应用的时候,JVM会收到一个停止信号,然后执行shutdownHook的线程。一个实现示例如下:

public class ShutdownHook extends Thread {
    private Thread mainThread;
    private boolean shutDownSignalReceived;

    @Override
    public void run() {
        System.out.println("Shut down signal received.");
        this.shutDownSignalReceived=true;
        mainThread.interrupt();
        try {
            mainThread.join(); //当收到停止信号时,等待mainThread的执行完成
        } catch (InterruptedException e) {
        }
        System.out.println("Shut down complete.");
    }

    public ShutdownHook(Thread mainThread) {
        super();
        this.mainThread = mainThread;
        this.shutDownSignalReceived = false;
        Runtime.getRuntime().addShutdownHook(this);
    }

    public boolean shouldShutDown(){
        return shutDownSignalReceived;
    }

}

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

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

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

public class TestMain {
    private ShutdownHook shutdownHook;
    public static void main( String[] args ) {
        TestMain app = new TestMain();
        System.out.println( "Hello World!" );
        app.execute();
        System.out.println( "End of main()" );
    }
    public TestMain(){
        this.shutdownHook = new ShutdownHook(Thread.currentThread());
    }
    public void execute(){
        while(!shutdownHook.shouldShutDown()){
            System.out.println("I am sleep");
            try {
                Thread.sleep(1*1000);
            } catch (InterruptedException e) {
                System.out.println("execute() interrupted");
            }
            System.out.println("I am not sleep");
        }
        System.out.println("end of execute()");
    }
}

启动测试代码,之后再发送一个中断信号,控制台输出:
I am sleep
I am not sleep
I am sleep
I am not sleep
I am sleep
I am not sleep
I am sleep
Shut down signal received.
execute() interrupted
I am not sleep
end of execute()
End of main()
Shut down complete.

Process finished with exit code 130 (interrupted by signal 2: SIGINT)

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

  • 02 actuator/shutdown of Spring boot

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

management:
  server:
    port: 8001
    servlet:
      context-path: /
    ssl:
      enabled: false
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always
    shutdown:
      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中。我测试的版本是1.5.9.RELEASE,已通过wilkinsona提交的答案实现了优雅退出。更多关于springboot相关的疑难杂症,欢迎进入我的github寻找答案:https://github.com/singgel/SpringBoot-Templates

package com.yiche.bdc;

import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

import org.apache.catalina.connector.Connector;
import org.apache.tomcat.util.threads.ThreadPoolExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@EnableEurekaClient
@SpringBootApplication
@EnableCaching
@EnableAsync
public class ApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiApplication.class, args);
    }

    /**
     * 用于接受 shutdown 事件
     */
    @Bean
    public GracefulShutdown gracefulShutdown() {
        return new GracefulShutdown();
    }

    @Bean
    public EmbeddedServletContainerCustomizer tomcatCustomizer() {
        return new EmbeddedServletContainerCustomizer() {

            @Override
            public void customize(ConfigurableEmbeddedServletContainer container) {
                if (container instanceof TomcatEmbeddedServletContainerFactory) {
                    ((TomcatEmbeddedServletContainerFactory) container)
                            .addConnectorCustomizers(gracefulShutdown());
                }

            }
        };
    }

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

    }
}
  • 03 Spring boot 优雅停机

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

package com.yiche.bdc.api.config;

import org.apache.catalina.connector.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @Author: heks
 * @DATE: 2019/3/15 12:59 PM
 *
 * 优雅关闭 Spring Boot tomcat
 */

@Component
public class GracefulShutdownTomcat implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
    private final Logger log = LoggerFactory.getLogger(GracefulShutdownTomcat.class);
    private volatile Connector connector;
    private final int waitTime = 30;
    @Override
    public void customize(Connector connector) {
        this.connector = connector;
    }
    @Override
    public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
        this.connector.pause();
        Executor executor = this.connector.getProtocolHandler().getExecutor();
        if (executor instanceof ThreadPoolExecutor) {
            try {
                ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
                threadPoolExecutor.shutdown();
                if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
                    log.warn("Tomcat thread pool did not shut down gracefully within " + waitTime + " seconds. Proceeding with forceful shutdown");
                }
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

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

    @Autowired
    private GracefulShutdownTomcat gracefulShutdownTomcat;

    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
        tomcat.addConnectorCustomizers(gracefulShutdownTomcat);
        return tomcat;
    }
}

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

Spring boot的健康检查,为UP。
正在执行操作不会终止,直到执行完成。
不再接收新的请求,客户端报错信息为:Connection reset by peer。
最后正常终止进程(业务执行完成后,立即进程停止)。从测试结果来看,是满足我们的需求的。当然如果发送指令kill -9,进程会立即停止。
针对undertow的解决方案是:

package com.yiche.bdc.api.config;

import io.undertow.Undertow;
import io.undertow.server.ConnectorStatistics;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServer;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.List;

/**
 * @Author: heks
 * @DATE: 2019/3/15 12:59 PM
 *
 * 优雅关闭 Spring Boot undertow
 */
@Component
public class GracefulShutdownUndertow implements ApplicationListener<ContextClosedEvent> {

    @Autowired
    private GracefulShutdownUndertowWrapper gracefulShutdownUndertowWrapper;

    @Autowired
    private ServletWebServerApplicationContext context;

    @Override
    public void onApplicationEvent(ContextClosedEvent contextClosedEvent){
        gracefulShutdownUndertowWrapper.getGracefulShutdownHandler().shutdown();
        try {
            UndertowServletWebServer webServer = (UndertowServletWebServer)context.getWebServer();
            Field field = webServer.getClass().getDeclaredField("undertow");
            field.setAccessible(true);
            Undertow undertow = (Undertow) field.get(webServer);
            List<Undertow.ListenerInfo> listenerInfo = undertow.getListenerInfo();
            Undertow.ListenerInfo listener = listenerInfo.get(0);
            ConnectorStatistics connectorStatistics = listener.getConnectorStatistics();
            while (connectorStatistics.getActiveConnections() > 0){}
        }catch (Exception e){
            // Application Shutdown
        }
    }
}
package com.yiche.bdc.api.config;

import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.GracefulShutdownHandler;
import org.springframework.stereotype.Component;

/**
 * @Author: heks
 * @DATE: 2019/3/15 12:59 PM
 */
@Component
public class GracefulShutdownUndertowWrapper implements HandlerWrapper {
    private GracefulShutdownHandler gracefulShutdownHandler;
    @Override
    public HttpHandler wrap(HttpHandler handler) {
        if(gracefulShutdownHandler == null) {
            this.gracefulShutdownHandler = new GracefulShutdownHandler(handler);
        }
        return gracefulShutdownHandler;
    }
    public GracefulShutdownHandler getGracefulShutdownHandler() {
        return gracefulShutdownHandler;
    }
}

public class UnipayProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(UnipayProviderApplication.class);
    }
    @Autowired
    private GracefulShutdownUndertowWrapper gracefulShutdownUndertowWrapper;
    @Bean
    public UndertowServletWebServerFactory servletWebServerFactory() {
        UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();
        factory.addDeploymentInfoCustomizers(deploymentInfo -> deploymentInfo.addOuterHandlerChainWrapper(gracefulShutdownUndertowWrapper));
        factory.addBuilderCustomizers(builder -> builder.setServerOption(UndertowOptions.ENABLE_STATISTICS, true));
        return factory;
    }
}

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

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

猜你喜欢

转载自blog.csdn.net/singgel/article/details/88615330