spring cloud 优雅停机部署 spring boot

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

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值