spring boot 应用优雅关机
完美的停机步骤应该实现以下步骤:
第一步: 向Eureka Server Delete/Down 掉注册信息
第二步:查看spring boot应用是否还有用户相关的线程:即tomcat的用户线程是否都运行完毕,比如一个用户的查询已经进入改应用,应该等待其响应完毕。
第三步:如果没有正在运行的线程,则停掉应用,发布版本。如果有则等等待。
第四步:发完完毕完毕后,在启动/重置应用状态。
方案1:Runtime.getRuntime().addShutdownHook
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
ThreadGroup currentGroup=Thread.currentThread().getThreadGroup();
ThreadGroup topGroup = null; //找到顶层的ThreadGroup
while(currentGroup!=null) {
topGroup = currentGroup;
currentGroup = currentGroup.getParent();
}
if(topGroup==null) {
logger.debug("the root thread group is empty ...");
}else {
##自行补充,1.向EurekaServer注销 。2.查看进行中的进程。3.停机。
}
}
});
手动实现以上的步骤。有点难度似乎,因为应用与外部的所有连接都得检查,而且tomcate的http请求相关线程池的线程并不是在请求响应完毕之后就马上回收的。
方案2:
网上百度很多优雅停机的,但是最新的spring boot 2.1.X相关的还是很少。于是研究了一下:
spring boot版本 2.1.4
1.spring boot actuator 默认暴露的端点是:health , Info 其他要进行配置开启才行
management:
endpoints:
web:
exposure:
include:
- "*" #开启所有的暴露端点
endpoint:
shutdown:
enabled: true #停机端口
restart:
enabled: true #重启端口,包含pause与resume
2. http://yourip:9999/actuator/shutdown
在contoller加入方法:
@GetMapping("/")
public String root() throws Exception{
for(int i=0;i<100;i++) {
logger.debug(i+" .");
Thread.currentThread().sleep(1000);
}
return "root";
}
保证执行时间够长,测试优雅关机是否能够实现已进入应用的请求能够执行完。
使用postman用post方法请求http://yourip:9999/actuator/shutdown
可以看到请求http://yourip:9999/的请求在应用shutdown后,请求报错:连接失败。
3.http://yourip:9999/actuator/pause
使用postman用post方法请求http://yourip:9999/actuator/pause
可以看到请求http://yourip:9999/能够得到正常的响应,并且EurekaServer该服务的状态为:DOWN。
再次http://yourip:9999/时,服务已经变成不可用。说明这台应用已经不会接受到用户请求了,可以开始部署。
4.http://yourip:9999/actuator/resume 可以恢复应用。
5.当我们在linux上部署发布时,虽然pause可以让已请求到的请求响应完毕,但是如何知道所有请求都响应完毕了呢?
5.1 : 比较笨拙的方法是看应用的日志输出的最后时间。
5.2 :查看tomcat线程池的状态,只需看用户进程。
http://yourip:9999/actuator/threaddump 端点可以查到当前应用正在执行的所有的线程。
请求http://yourip:9999/前,调用一次把信息copy出来threaddump1.txt,请求http://yourip:9999/中调用一次,吧信息copy出来threaddump2.txt
调用http://yourip:9999/前的threaddump1.txt因为没有正在执行的用户线程信息,是搜索不到应用包名的。例如你的controller都在com.xxh.app1这个package里面。
调用中的threaddump2.txt应该会包含com.xxh.app1 package的包名:
示例:
{
"threadName": "http-nio-9999-exec-3",
"threadId": 38,
"blockedTime": -1,
"blockedCount": 0,
"waitedTime": -1,
"waitedCount": 122,
"lockName": null,
"lockOwnerId": -1,
"lockOwnerName": null,
"inNative": false,
"suspended": false,
"threadState": "TIMED_WAITING",
"stackTrace": [{
"methodName": "sleep",
"fileName": "Thread.java",
"lineNumber": -2,
"className": "java.lang.Thread",
"nativeMethod": true
},
{
"methodName": "root",
"fileName": "XxhServiceSecurityController.java",
"lineNumber": 71,
"className": "com.xxh.app1.XxhServiceSecurityController",
"nativeMethod": false
},
{
"methodName": "invoke0",
"fileName": "NativeMethodAccessorImpl.java",
"lineNumber": -2,
"className": "sun.reflect.NativeMethodAccessorImpl",
"nativeMethod": true
},
{
"methodName": "invoke",
"fileName": "NativeMethodAccessorImpl.java",
"lineNumber": 62,
"className": "sun.reflect.NativeMethodAccessorImpl",
"nativeMethod": false
},
{
"methodName": "invoke",
"fileName": "DelegatingMethodAccessorImpl.java",
"lineNumber": 43,
"className": "sun.reflect.DelegatingMethodAccessorImpl",
"nativeMethod": false
},
所以我们可以用这个写个脚本来检测是否有http请求尚未完成响应。threaddump是json格式的。
5.3:获取到spring boot应用的pause事件(严格来说这个pause是spring cloud的),找到正在运行的用户线程,当用户线程数为0时,就可以停止应用了。
查看org.springframework.cloud.context.restart.RestartEndpoint源码
private ConfigurableApplicationContext context;
/**
* Pause endpoint configuration.
*/
@Endpoint(id = "pause")
public class PauseEndpoint {
@WriteOperation
public Boolean pause() {
if (isRunning()) {
doPause();
return true;
}
return false;
}
}
public synchronized void doPause() {
if (this.context != null) {
this.context.stop();
}
}
最终调用的是org.springframework.context.support.AbstractApplicationContext
@Override
public void stop() {
getLifecycleProcessor().stop();
publishEvent(new ContextStoppedEvent(this));
}
所以我们只要监听到ContextStoppedEvent事件即可。
5.4 我们统一拦截controller,定义一个线程安全的atomicInteger对象,每次请求进来都加1,请求响应完毕减1.
public class XxhControllerInterceptor extends HandlerInterceptorAdapter
public static AtomicInteger requestFinishFlag=new AtomicInteger(0);
private static final Logger logger = LoggerFactory.getLogger(XxhControllerInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
logger.debug("***"+requestFinishFlag.incrementAndGet()+"***");
return super.preHandle(request, response, handler);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
logger.debug("***"+requestFinishFlag.decrementAndGet()+"***");
super.afterCompletion(request, response, handler, ex);
}
5.5 监听时间,轮训标志位是否为0,为0则表示可以退出。
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextStoppedEvent;
import org.springframework.web.bind.annotation.RestController;
import com.xxh.app1.common.core.interceptor.XxhControllerInterceptor;
@SpringBootApplication(scanBasePackages="com.xxh.app1.*")
@EnableDiscoveryClient
public class XxhServiceSecurityApplication {
private static final Logger logger = LoggerFactory.getLogger(XxhServiceSecurityApplication.class);
public static void main(String[] args) {
logger.debug("XxhServiceSecurityApplication:::main:::::starting..::::");
SpringApplication.run(XxhServiceSecurityApplication.class, args).addApplicationListener(new XxhServiceSecurityAppContextStopListener());
logger.debug("XxhServiceSecurityApplication:::main:::::started success..::::");
}
public static class XxhServiceSecurityAppContextStopListener implements ApplicationListener<ContextStoppedEvent>{
@Override
public void onApplicationEvent(ContextStoppedEvent event) {
int flag=XxhControllerInterceptor.requestFinishFlag.get();
while(flag>0) {
logger.debug("recieve ContextStoppedEvent ...controller Flag:"+flag);
flag=XxhControllerInterceptor.requestFinishFlag.get();
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
logger.error("ContextStoppedEvent Sleep Interrupt",e);
}
}
//可以时使用Kill -9 关停应用,然后部署重启了。
logger.debug("recieve ContextStoppedEvent ...controller Flag:"+flag+", we can access publish operation");
//Runtime.getRuntime().exec("你的发版脚本");
}
}
}