spring cloud 优雅停机部署 spring boot

it2022-05-05  123

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

 

 

 


最新回复(0)