Spring Boot 1.X和2.X優雅重啟實戰

猿天地發表於2018-05-21

專案在重新發布的過程中,如果有的請求時間比較長,還沒執行完成,此時重啟的話就會導致請求中斷,影響業務功能,優雅重啟可以保證在停止的時候,不接收外部的新的請求,等待未完成的請求執行完成,這樣可以保證資料的完整性。

Spring Boot 1.X


import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.catalina.connector.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextClosedEvent;

/**
 * Spring Boot1.X Tomcat容器優雅停機
 * @author yinjihuan
 *
 */
@Configuration
public class ShutdownConfig {
	
	/**
     * 用於接受shutdown事件
     * @return
     */
    @Bean
    public GracefulShutdown gracefulShutdown() {
        return new GracefulShutdown();
    }

    /**
     * 用於注入 connector
     * @return
     */
    @Bean
    public EmbeddedServletContainerCustomizer tomcatCustomizer() {
        return new EmbeddedServletContainerCustomizer() {
            @Override
            public void customize(ConfigurableEmbeddedServletContainer container) {
                if (container instanceof TomcatEmbeddedServletContainerFactory) {
                    ((TomcatEmbeddedServletContainerFactory) container).addConnectorCustomizers(gracefulShutdown());
                }
            }
        };
    }
    
    private static class GracefulShutdown implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {

        private static final Logger log = LoggerFactory.getLogger(GracefulShutdown.class);
        private volatile Connector connector;
        private final int waitTime = 120;

        @Override
        public void customize(Connector connector) {
            this.connector = connector;
        }

        @Override
        public void onApplicationEvent(ContextClosedEvent event) {
            this.connector.pause();
            Executor executor = this.connector.getProtocolHandler().getExecutor();
            if (executor instanceof ThreadPoolExecutor) {
                try {
                    ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
                    log.info("shutdown start");
                    threadPoolExecutor.shutdown();
                    log.info("shutdown end");
                    if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
                    	log.info("Tomcat 程式在" + waitTime + "秒內無法結束,嘗試強制結束");
                    }
                    log.info("shutdown success");
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
}

複製程式碼

Spring Boot 2.X

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.catalina.connector.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextClosedEvent;

/**
 * Spring Boot2.X Tomcat容器優雅停機
 * @author yinjihuan
 *
 */
@Configuration
public class ShutdownConfig {
	
	/**
     * 用於接受shutdown事件
     * @return
     */
    @Bean
    public GracefulShutdown gracefulShutdown() {
        return new GracefulShutdown();
    }

    @Bean
    public ServletWebServerFactory servletContainer() {
      TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
      tomcat.addConnectorCustomizers(gracefulShutdown());
      return tomcat;
    }
    
    private static class GracefulShutdown implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {

        private static final Logger log = LoggerFactory.getLogger(GracefulShutdown.class);
        private volatile Connector connector;
        private final int waitTime = 120;

        @Override
        public void customize(Connector connector) {
            this.connector = connector;
        }

        @Override
        public void onApplicationEvent(ContextClosedEvent event) {
            this.connector.pause();
            Executor executor = this.connector.getProtocolHandler().getExecutor();
            if (executor instanceof ThreadPoolExecutor) {
                try {
                    ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
                    log.info("shutdown start");
                    threadPoolExecutor.shutdown();
                    log.info("shutdown end");
                    if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
                    	log.info("Tomcat 程式在" + waitTime + "秒內無法結束,嘗試強制結束");
                    }
                    log.info("shutdown success");
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
}

複製程式碼

重啟服務指令碼:

LANG="zh_CN.UTF-8"
pid=`ps ax | grep fangjia-youfang-web | grep java | head -1 | awk '{print $1}'`
echo $pid
#kill $pid
curl -X POST http://127.0.0.1:8086/shutdown?token=認證資訊
while [[ $pid != "" ]]; do
    echo '服務停止中...'
    sleep 1
    pid=`ps ax | grep fangjia-youfang-web | grep java | head -1 | awk '{print $1}'`
done
echo '服務停止成功,開始重啟服務...'

java -jar xxx.jar
複製程式碼

在重啟之前首先傳送重啟命令到endpoint,或者用kill 程式ID的方式,千萬不要用kill -9。

然後迴圈檢測程式是否存在,如果服務正常停止了,程式也就不存在了,如果程式還在,證明還有未處理完的請求,停止1秒,繼續檢測。

關於重啟服務,建議用kill方式,這樣就不用依賴spring-boot-starter-actuator,如果用endpoint方式,則需要控制好許可權,不然隨時都有可能被人重啟了,可以用security來控制許可權,我這邊是自己用過濾器來控制的。

如果用actuator方式重啟的話需要配置啟用重啟功能: 1.x配置如下:

endpoints.shutdown.enabled=true
複製程式碼

2.x配置就比較多了,預設只暴露了幾個常用的,而且訪問地址也有變化,比如health, 以前是直接訪問/health,現在需要 /actuator/health才能訪問。我們可以通過配置來相容之前的訪問地址。

shutdown預設是不暴露的,可以通過配置暴露並開始,配置如下:

#訪問路徑,配置後就和1.x版本路徑一樣
management.endpoints.web.base-path=/
# 暴露所有,也可以暴露單個或多個
management.endpoints.web.exposure.include=*
# 開啟shutdown
management.endpoint.shutdown.enabled=true
複製程式碼

文件請參考:https://docs.spring.io/spring-boot/docs/2.0.2.RELEASE/reference/htmlsingle/#production-ready

如何測試

測試的話我們可以寫一個簡單的介面,在介面中等待,然後執行指令碼停止專案,如果正常的話會輸出服務停止中,等到你的介面執行完成,程式才會消失掉,但是如果超過了你配置的等待時間就會強行退出。

@GetMapping("/hello")
public String hello() {
	System.out.println("req.........");
	try {
		Thread.sleep(1000 * 60 * 3);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	return "hello";
}
複製程式碼

需要注意的問題

如果你的專案中有用到其他的執行緒池,比如Spring的ThreadPoolTaskExecutor,不熟悉的同學可以參考我的這篇文章《Spring Boot Async非同步執行》

在傳送停止命令後如果ThreadPoolTaskExecutor有執行緒還沒處理完的話,這個時候程式是不會自動關閉的。這個時候我們需要對執行緒池進行關閉處理,增加程式碼如下:

AsyncTaskExecutePool asyncTaskExecutePool = event.getApplicationContext().getBean(AsyncTaskExecutePool.class);
Executor executors = asyncTaskExecutePool.getAsyncExecutor();
try {
      if (executors instanceof ThreadPoolTaskExecutor) {
           ThreadPoolTaskExecutor threadPoolExecutor = (ThreadPoolTaskExecutor) executors;
           log.info("Async shutdown start");
           threadPoolExecutor.setWaitForTasksToCompleteOnShutdown(true);
	       threadPoolExecutor.setAwaitTerminationSeconds(waitTime);
           threadPoolExecutor.shutdown();
      }
} catch (Exception ex) {
     Thread.currentThread().interrupt();
}
複製程式碼

ThreadPoolTaskExecutor只有shutdown方法,沒有awaitTermination方法,通過檢視原始碼,在shutdown之前設定setWaitForTasksToCompleteOnShutdown和setAwaitTerminationSeconds同樣能實現awaitTermination。

原始碼如下:

public void shutdown() {
		if (logger.isInfoEnabled()) {
			logger.info("Shutting down ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : ""));
		}
		if (this.executor != null) {
			if (this.waitForTasksToCompleteOnShutdown) {
				this.executor.shutdown();
			}
			else {
				for (Runnable remainingTask : this.executor.shutdownNow()) {
					cancelRemainingTask(remainingTask);
				}
			}
			awaitTerminationIfNecessary(this.executor);
		}
	}
複製程式碼

當waitForTasksToCompleteOnShutdown為true的時候就直接呼叫executor.shutdown();,最後執行awaitTerminationIfNecessary方法。

private void awaitTerminationIfNecessary(ExecutorService executor) {
		if (this.awaitTerminationSeconds > 0) {
			try {
				if (!executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS)) {
					if (logger.isWarnEnabled()) {
						logger.warn("Timed out while waiting for executor" +
								(this.beanName != null ? " '" + this.beanName + "'" : "") + " to terminate");
					}
				}
			}
			catch (InterruptedException ex) {
				if (logger.isWarnEnabled()) {
					logger.warn("Interrupted while waiting for executor" +
							(this.beanName != null ? " '" + this.beanName + "'" : "") + " to terminate");
				}
				Thread.currentThread().interrupt();
			}
		}
	}
複製程式碼

awaitTerminationIfNecessary中會判斷屬性awaitTerminationSeconds 如果與值的話就執行關閉等待檢測邏輯,跟我們處理tomcat關閉的程式碼是一樣的。

發現這樣做之後好像沒什麼效果,於是我換了一種寫法,直接通過獲取ThreadPoolTaskExecutor中的ThreadPoolExecutor來執行關閉邏輯:

AsyncTaskExecutePool asyncTaskExecutePool = event.getApplicationContext().getBean(AsyncTaskExecutePool.class);
Executor executors = asyncTaskExecutePool.getAsyncExecutor();
 try {
      if (executors instanceof ThreadPoolTaskExecutor) {
            ThreadPoolTaskExecutor threadPoolExecutor = (ThreadPoolTaskExecutor) executors;
            log.info("Async shutdown start");
	        threadPoolExecutor.getThreadPoolExecutor().shutdown();

	        log.info("Async shutdown end"+threadPoolExecutor.getThreadPoolExecutor().isTerminated());
	        if (!threadPoolExecutor.getThreadPoolExecutor().awaitTermination(waitTime, TimeUnit.SECONDS)) {
                log.info("Tomcat 程式在" + waitTime + "秒內無法結束,嘗試強制結束");
            }
	        log.info("Async shutdown success");
       }
} catch (Exception ex) {
      Thread.currentThread().interrupt(); 
}
複製程式碼

這是方式也沒用達到我想要的效果,當我發出kill命令之後,直接就退出了,其實我有一個後臺執行緒在ThreadPoolTaskExecutor中執行,通過輸出的日誌看到,只要呼叫了shutdown,isTerminated方法返回的就是true,說已經關閉了,這塊還沒找到原因,有研究出來的小夥伴還請分享出來。

更多技術分享請關注微信公眾號:猿天地

image.png

相關文章