Java优雅停机指南

82 阅读3分钟

背景

  • 为避免服务在停机过程中请求(业务)丢失,包含停机过程中继续有请求进来,或正在运行中的任务被强制中断。需规范各子系统的服务关机流程,实现优雅关机,避免业务中断。

需解决的问题

  • 解决停机时还有流量打到被停机实例上
  • 解决其他线程例如定时任务停机导致线程中断,任务未执行完成的问题。

优雅停机指南

  • 进行优雅停机需使用kill -15命令关停服务

问题一:如何解决停机时还有流量打到被停机服务上

  • Spring原生已有配置可解决这类问题,配置如下:
server:
  shutdown: graceful  ###开启优雅停止容器
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s  ###优雅关闭容器时最长等待时间,默认30s,可根据实际情况加长时间

使用效果:

  • 已经到达服务的请求会继续处理。
  • 后续请求该停机实例的请求会直接拒绝。
  • 达到配置的最大时间,即使还有未处理完成的请求,也会直接关闭容器。

问题二:解决其他线程例如定时任务停机导致线程中断,任务未执行完成

  • 可以通过增加jvm钩子等方法,在关闭时对正在运行的任务进行检测,如未完成可以继续任务或者进行清理工作。

  • 针对本问题,将会给出几种增加关闭spring时触发钩子的写法,优先级从高到低(除方法五),优先级高的写法会优先执行。

方法一:监听事件ContextClosedEvent

  • 使用spring的事件监听机制,监听ApplicationContext的关闭事件。

  • 此种方法优先级最高。会在关闭流程中最先执行。

例:

@Component
public class CloseEventDemo implements ApplicationListener<ContextClosedEvent> {

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        try {
            LogUtils.info("######################## TestCloseEvent start");
            //业务处理逻辑
            LogUtils.info("######################## TestCloseEvent end");
        } catch (InterruptedException e) {
            LogUtils.info("######################## TestCloseEvent error");
            throw new RuntimeException(e);
        }
    }
}

方法二:实现SmartLifecycle接口

  • Spring提供的SmartLifecycle包含在Context创建和关闭时都会执行。在关闭时,会在监听ContextClosedEvent事件的代码执行后按照顺序执行。

  • 可以通过实现getPhase方法控制顺序,启动时按照由小到大执行,关闭时由大到小执行。

  • Spring关闭容器(如tomcat等)就是通过SmartLifecycle关闭的,可参考WebServerGracefulShutdownLifecycle,此lifecycle设置的顺序为int类型的最大值,也就是关闭时第一个执行。

例:

@Component
public class LifecycleHookDemo implements SmartLifecycle {

    private volatile boolean running = false;

    @Override
    public void start() {
        running = true;
        LogUtils.info("#####应用启动/重启后执行初始化...");
    }

    @Override
    public void stop() {
        running = false;
        LogUtils.info("#######应用停止/重启前执行清理...");
    }

    @Override
    public boolean isRunning() {
        return running;
    }

    // 设置phase值控制执行顺序(启动时由小到大,关闭时由大到小)
    @Override
    public int getPhase() {
        return SmartLifecycle.DEFAULT_PHASE;
    }
}

方法三:使用@PreDestroy注解

  • 关闭时,spring会调用标记@PreDestroy注解的方法,执行顺序在SmartLifecycle之后。

例:

@Component
public class PreDestroyDemo {

    @PreDestroy
    public void destroy() {
        LogUtils.info("######################## PreDestroyDemo destroy");
    }

}

方法四: 注册ShutdownHandler

  • Spring允许注册Runnable到Spring的shutdownHook,执行顺序最后,Logback就是用过这种方式去做最后的日志工作。

例:

@Component
public class ShutdownHandlerDemo implements ApplicationListener<ApplicationPreparedEvent> {

    @Override
    public void onApplicationEvent(ApplicationPreparedEvent event) {
        //注册至spring的ShutdownHandlers,关闭时会执行
        SpringApplication.getShutdownHandlers().add(new ShutdownHandler());
    }

    private static class ShutdownHandler implements Runnable {
        @Override
        public void run() {
            LogUtils.info("############ShutdownHandlerDemo.ShutdownHandler.run()");
        }
    }
}

方法五:注册至JVM的关闭钩子

  • 方法一至方法四本质是执行在Spring注册到JVM的shutdownHook中执行的,JVM可以挂载多个shutdownHook,调用顺序按照先进后出,但是多个shutdownHook并行执行.

例:

@Component
public class JavaShutdownHookDemo implements ApplicationListener<ApplicationPreparedEvent>, Runnable {

    @Override
    public void onApplicationEvent(ApplicationPreparedEvent event) {
        //向JVM注册shutdownHook
        Runtime.getRuntime().addShutdownHook(new Thread(this, "JavaShutdownHookDemo"));
    }

    @Override
    public void run() {
        LogUtils.info("######################## JavaShutdownHookDemo start");
    }
}