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("你的发版脚本"); } } }