背景
项目里面包含了第三方支付功能。在用户提现过程中,应用程序重启。此时的提现操作的响应,应用程序无法拿到,导致进行用户错误更新过程显示了不该显示的内容。
专题理解
当我们以命令或脚本的方式停止服务时,一般使用了 kill -9命令把服务进程杀掉,这个命令是非常暴力的,类似于直接按了这个服务的电源,显然这种方式对进行中的服务是很不友善的,当在停机时,正在进行RPC调用、执行批处理、缓存入库等操作,会造成不可挽回的数据损失,增加后期维护成本。
达到要求:
让服务在收到停机指令时,从容的拒绝新请求的进入,并执行完当前任务,然后关闭服务
优雅停机达到的要求:
首先要确保不会再有新的请求进来,所以需要设置一个流量挡板
保证正常处理已进来的请求线程,可以通过计数方式记录项目中的请求数量
如果涉及到注册中心,则需要在第一步结束后注销注册中心
停止项目中的定时任务
停止线程池
关闭其他需要关闭资源等等等
用到的命令:
当应用接收到kill的信号(15 终止号令)时,会停止接收新的请求,并等待活跃的请求完成后,关闭服务,
参考案例(springboot+tomcat)
方式一:
spring-boot-starter-actuator 模块提供了一个 restful 接口 /actuator/shutdown (POST) 用于优雅停机。一般需要限制内网关IP访问权限,而且最好使用Secrety进行登录验证;
#### 使用endpoints方式需要在配置文件中添加如下配置
server.shutdown=graceful ## 开启优雅停机
spring.lifecycle.timeout-per-shutdown-phase=20s ##设置优雅停机关闭流量挡板后最多等待时间
management.server.port=9090 ## 指定endpoints的访问端口,最好不与server.port一致
management.endpoint.shutdown.enabled=true ## 开启/actuator/shutdown路由
management.endpoints.web.exposure.include=shutdown ## 暴露/actuator/shutdown路由
方式二:
使用 kill -15 pid 发送停机通知进行优雅停机;
kill -9 pid 可以理解为操作系统从内核级别强行杀死某个进程,直接模拟了一次系统宕机,系统断电,这对于应用来说太不友好.kill -15 pid 则可以理解为发送一个通知,告知应用主动关闭。
实战经验
代码层面
package com.tianzao.novel.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.Connector;
import org.apache.coyote.ProtocolHandler;
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.time.LocalDateTime;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class GracefulShutdownTomcat implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
private volatile Connector connector;
private final int waitTime = 5;
@Override
public void customize(Connector connector) {
this.connector = connector;
}
@Override
public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
if (this.connector == null) {
return;
}
log.info("GracefulShutdownTomcat close start waitTime {} curTime {}", waitTime, LocalDateTime.now());
this.connector.pause();
log.info("GracefulShutdownTomcat pause company curTime {}", LocalDateTime.now());
ProtocolHandler protocolHandler = this.connector.getProtocolHandler();
protocolHandler.closeServerSocketGraceful();
log.info("GracefulShutdownTomcat Socket Close company curTime {}", LocalDateTime.now());
Executor executor = this.connector.getProtocolHandler().getExecutor();
if (executor instanceof ThreadPoolExecutor) {
try {
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
threadPoolExecutor.shutdown();
log.info("GracefulShutdownTomcat Executor shutdown company curTime {}", LocalDateTime.now());
if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
log.info("Tomcat thread pool did not shut down gracefully within " + waitTime + " seconds. Proceeding with forceful shutdown");
}
log.info("GracefulShutdownTomcat Executor shutdown waitTime over curTime {}", LocalDateTime.now());
} catch (InterruptedException ex) {
log.info("GracefulShutdownTomcat close Exception ", ex);
Thread.currentThread().interrupt();
}
} else {
log.info("GracefulShutdownTomcat Executor is not ThreadPoolExecutor curTime {}", LocalDateTime.now());
}
}
}
启动脚本
kill -15 $PID