背景
- 为避免服务在停机过程中请求(业务)丢失,包含停机过程中继续有请求进来,或正在运行中的任务被强制中断。需规范各子系统的服务关机流程,实现优雅关机,避免业务中断。
需解决的问题
- 解决停机时还有流量打到被停机实例上
- 解决其他线程例如定时任务停机导致线程中断,任务未执行完成的问题。
优雅停机指南
- 进行优雅停机需使用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");
}
}