【微服务】优雅停机

参考:

https://www.linuxprobe.com/micro-service-fa.html

https://www.cnblogs.com/shihaiming/p/11408684.html

https://blog.csdn.net/j3T9Z7H/article/details/82836811

https://blog.csdn.net/w1014074794/article/details/89447401/

https://bhuwanupadhyay.github.io/posts/graceful-shutdown-in-spring-boot/

Springboot中的优雅关机

应用场景:例如项目升级或服务搬迁,需要大规模关闭当前线上服务时。

反面后果:若采用强硬关机(kill -9),则服务会立即停止,客户从此不会收到任何响应,对服务端关闭未知。

亟待改进:在服务终止命令发出后, 程序应该能拒绝新的请求, 但应该继续完成已有请求的处理。

原理: 

https://blog.csdn.net/weixin_37703598/article/details/89702616?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-0&spm=1001.2101.3001.4242


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

============================
Java 对于优雅停机的底层支持
============================
Java 语言底层有机制能捕获到 OS 的 SIGINT/ SIGTERM 停止指令的, 具体是通过 Runtime.getRuntime().addShutdownHook() 向 JVM 中注册一个 Shutdown hook 线程, 当 JVM 收到停止信号后, 该线程将被激活运行, 这时候我们就可以向其他线程发出中断指令, 进而快速而优雅地关闭整个程序.

 Demo1:

https://blog.csdn.net/liwei128/article/details/95794721

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.apache.catalina.connector.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;
/**
* Tomcat优雅停机
* @author LW
* @time 2019年7月13日 上午12:05:47
*/
@Component
public class GracefulShutdownTomcat implements ApplicationListener<ContextClosedEvent> {
private final Logger log = LoggerFactory.getLogger(GracefulShutdownTomcat.class);
private final int waitTime = 30;
private static volatile Connector connector;

/**
* 用于获取tomcat连接器
* @return
*/
@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
tomcat.addConnectorCustomizers(connect->{
connector = connect;
});
return tomcat;
}

/**
* 监听 停止操作kill -15
*/
@Override
public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
long startTime = System.currentTimeMillis();
//tomcat暂停对外服务
connector.pause();
//获取tomcat线程池
Executor executor = connector.getProtocolHandler().getExecutor();
if (executor instanceof ThreadPoolExecutor) {
try {
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
//线程池优雅停止(不接收新的请求,等待任务运行完成后关闭线程池)
threadPoolExecutor.shutdown();
//堵塞等待一定时间,指定时间内关闭成功则返回true,解除堵塞;否则fasle
if (threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
log.info("Tomcat thread pool closed,time:{}ms",System.currentTimeMillis()-startTime);
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}

 Demo2:

public class Test {
    public static void main(String[] args){
        System.out.println("1: Main start");

        Thread mainThread = Thread.currentThread();

        //注册一个 ShutdownHook
        ShutdownSampleHook thread=new ShutdownSampleHook(mainThread);
        Runtime.getRuntime().addShutdownHook(thread);

        try {
            Thread.sleep(30*1000);
        } catch (InterruptedException e) {
            System.out.println("3: mainThread get interrupt signal.");
        }

        System.out.println("4: Main end");  
    }
}

class ShutdownSampleHook extends Thread {
    private Thread mainThread;
    @Override
    public void run() {
        System.out.println("2: Shut down signal received.");
        mainThread.interrupt();//给主线程发送一个中断信号
        try {
            mainThread.join(); //等待 mainThread 正常运行完毕
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("5: Shut down complete.");
    }

    public ShutdownSampleHook(Thread mainThread) {
        this.mainThread=mainThread;

    }
}

关于 mainThread.interrupt() 方法的说明, 该方法将给主线程发送一个中断信号. 如果主线程没有进入阻塞状态, interrupt() 其实没有什么作用; 如果主线程处于阻塞状态, 该线程将得到一个 InterruptedException 异常. 在调用 mainThread.join() 或 mainThread.wait() 之前, 仍可以通过调用 mainThread.interrupted() 来清除中断信号. 
一个线程有三种进入阻塞状态的方法, 分别是调用 Thread.wait() 或 Thread.join() 或 Thread.sleep().

正常情况下, 程序需要运行 30 秒, 程序的输出是:

如果在程序启动后, 按下 Ctrl+C(kill -2 pid), 程序很快就结束了, 最终的输出是:

============================
SpringBoot Web 项目的优雅停机
============================
Java web 服务器通常也支持优雅退出, 比如 tomcat, 提供如下命令:
catalina.sh stop n         , 先等 n 秒后, 然后停止 tomcat. 
catalina.sh stop n -force  , 先等 n 秒后, 然后 kill -9 tomcat. 

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

应用场景之一:

https://stackoverflow.com/questions/39157863/how-to-shutdown-activemq-session-thread-gracefully-on-tomcat

原文地址:https://www.cnblogs.com/cathygx/p/14486452.html