原文: Spring 及 Spring Boot 進程優雅停止方式
1. 背景
一個http 的請求處理是需要時間的,同時一個應用的關閉也是需要時間。那麼,我們該如何來關閉一個正在運行中的Spring 或者Sping Boot 項目呢?關閉應用時,我們需要思考如下問題:
- 內存中仍存在沒有處理完的數據,比如等待同步的List
- 對於Java 的任務處理ExecutorService 中仍然有任務在等待
- 對於Web 項目,仍然有請求未處理完。關閉應用時,是否先禁止Web 服務器接收新的請求
如果我們在關閉系統時,存在上述問題,我們的應用能安全的關閉嗎?
2. Linux 下停止進程的方法
2.1 kill 命令
kill 命令與關閉進行相關的指令有如下三個:
- SIGINT 2 中斷(同 Ctrl + C)
- SIGTERM 15 正常終止
- SIGKILL 9 強制終止
經常使用的 ctrl + c
就是發送了 SIGINT(2) 信號給進程的。另外,整個信號中,最特殊的命令就是 SIGKILL(9), 它代表 無條件結束進程,也就是通常說的強制結束進程,這種方式結束進程有可能會導致進程內存中 數據丟失。而另外兩個信號對於進程來說是可以選擇性忽略的,但目前的絕大部分的進程都是可以通過這三個信號進行結束的。
2.2 正常結束與強制結束
Java 程序的後臺程序 正常 與 強制結束 方式對比。在 Java 中,強制結束代表 直接立即結束 進程中的 Main 線程和其他所有線程,這裏強調 直接和立即,也就是說通過強制方式,進程不會做任何收尾工作。而 正常結束 則非立即結束進程,而是先調用程序的 ShutdownHook 收尾線程,等收尾線程結束後再結束所有線程。
這裏出現了 收尾線程,實際上這個就是 Java 程序中通過 Runtime.getRuntime().addShutdownHook()
方式註冊的線程就是收尾線程。
3. Spring 項目停止的方法
Spring
項目也正是採用了ShutdownHook
的方法進行上下文的關閉操作。具體的關閉方法close
實現在類AbstractApplicationContext
中。實現如下:
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
public void close() {
synchronized(this.startupShutdownMonitor) {
// 調用實際關閉方法
this.doClose();
if (this.shutdownHook != null) {
try {
// 移除hook
Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
} catch (IllegalStateException var4) {
}
}
}
}
protected void doClose() {
if (this.active.get() && this.closed.compareAndSet(false, true)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Closing " + this);
}
LiveBeansView.unregisterApplicationContext(this);
try {
// 同步發佈上下文關閉事件,該事件是上下文關閉前進行一些操作的關鍵點
// 此時上下文環境還未被清理
this.publishEvent((ApplicationEvent)(new ContextClosedEvent(this)));
} catch (Throwable var3) {
this.logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", var3);
}
if (this.lifecycleProcessor != null) {
try {
this.lifecycleProcessor.onClose();
} catch (Throwable var2) {
this.logger.warn("Exception thrown from LifecycleProcessor on context close", var2);
}
}
// 清理單例的 Beans,會調用定義在Bean 中的destroy 方法
this.destroyBeans();
this.closeBeanFactory();
this.onClose();
if (this.earlyApplicationListeners != null) {
this.applicationListeners.clear();
this.applicationListeners.addAll(this.earlyApplicationListeners);
}
this.active.set(false);
}
}
protected void destroyBeans() {
this.getBeanFactory().destroySingletons();
}
// 後文介紹的ServletWebServerApplicationContext 實現該方法,這也是導致直接關閉應用會出現有未被處理的請求被關閉的原因
protected void onClose() {
}
public boolean isActive() {
return this.active.get();
}
}
由上述的流程可知,我們可以在Bean 的destroy
方法中對內存中的數據和未執行完的ExecutorService 任務進行處理。
4. Spring Boot 項目停止的方法
一個集成了Spring MVC
項目,我們在關閉時就需要思考未被處理完的請求,需要時間被處理。以及這段時間需要將請求處理邏輯關閉的問題。
ServletWebServerApplicationContext
類是使用基於Servlet的Web 應用的上下文類,該類重寫了AbstractApplicationContext
中的close
方法。代碼如下:
public class ServletWebServerApplicationContext extends GenericWebApplicationContext
implements ConfigurableWebServerApplicationContext {
// 重寫 colse 方法調用的onClose
@Override
protected void onClose() {
super.onClose();
// 調用具體的內置server 停止服務
stopAndReleaseWebServer();
}
private void stopAndReleaseWebServer() {
WebServer webServer = this.webServer;
if (webServer != null) {
try {
webServer.stop();
this.webServer = null;
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}
}
由以上實現我們可以,在採用kill -2
或者kill -15
關閉Spring Boot(MVC)項目時,我們如果不做特殊處理,關閉的過程中就會出現請求丟失的問題。因爲此時,應用的上下文先關閉,內置的Server 後關閉。
針對這個問題,我們該如何處理呢?可以想象的步驟是這樣的,在Spring 發出ContextClosedEvent
時,我們監聽該事件:
- 先禁止server 繼續接收請求。
- 然後判斷server 的工作線程是否已經全部完成。
- 執行spring 的關閉流程。
綜合Spring 的關閉流程,參考Spring 官方給予的方案Shutdown embedded servlet container gracefully如下:
@SpringBootApplication
@Slf4j
@RestController
public class StopJavaSpringApplication implements ApplicationContextAware {
@Autowired
static ApplicationContext context;
public static void main(String[] args) {
SpringApplication.run(StopJavaSpringApplication.class, args);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
log.info("begin to shutdown ... ");
/*try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
HomeController one = context.getBean(HomeController.class);
System.out.println(one + " -> started: " + one.started.get() + " ended:" + one.ended.get());
log.info("bye bye, app stopped ...");
}));
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
@RequestMapping("/pause")
public String pause() throws InterruptedException {
Thread.sleep(10000);
return "Pause complete";
}
@Bean
public GracefulShutdown gracefulShutdown() {
return new GracefulShutdown();
}
// 自定義Tomcat 的配置
@Bean
public WebServerFactoryCustomizer tomcatCustomizer() {
return factory -> {
if (factory instanceof TomcatServletWebServerFactory) {
((TomcatServletWebServerFactory) factory)
.addConnectorCustomizers(gracefulShutdown());
}
};
}
// 監聽 ContextClosedEvent,此處是同步邏輯,如果配置了事件異步處理,則不能實現想要的邏輯
private static class GracefulShutdown implements TomcatConnectorCustomizer,
ApplicationListener<ContextClosedEvent> {
private static final Logger log = LoggerFactory.getLogger(GracefulShutdown.class);
private volatile Connector connector;
@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 {
// 等待 30s 線程池org.apache.tomcat.util.threads.ThreadPoolExecutor 完成所有請求
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
threadPoolExecutor.shutdown();
if (!threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
log.warn("Tomcat thread pool did not shut down gracefully within "
+ "30 seconds. Proceeding with forceful shutdown");
}
}
catch (InterruptedException ex) {
// 30s 未完成 強制關閉 該邏輯也可以寫在bash 中
Thread.currentThread().interrupt();
}
}
}
}
}
}
上述增加的邏輯,直接監聽了ContextClosedEvent
事件,此時同步Server 處理邏輯。基本能解決關閉Web 服務器遇到的問題。
5. 成員變量線程池的銷燬
上文提到過ExecutorService
也就是ThreadPoolExecutor
類的關閉問題。根據其文檔來看,無論是調用shutdown
/**
* Initiates an orderly shutdown in which previously submitted
* tasks are executed, but no new tasks will be accepted.
* Invocation has no additional effect if already shut down.
* 不保證任務一定完成關閉
* <p>This method does not wait for previously submitted tasks to
* complete execution. Use {@link #awaitTermination awaitTermination}
* to do that.
*
* @throws SecurityException {@inheritDoc}
*/
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
還是shutduwnNow
/**
* Attempts to stop all actively executing tasks, halts the
* processing of waiting tasks, and returns a list of the tasks
* that were awaiting execution. These tasks are drained (removed)
* from the task queue upon return from this method.
* 不等待一定關閉
* <p>This method does not wait for actively executing tasks to
* terminate. Use {@link #awaitTermination awaitTermination} to
* do that.
* 未響應interrupts 的線程,可能併爲關閉
* <p>There are no guarantees beyond best-effort attempts to stop
* processing actively executing tasks. This implementation
* cancels tasks via {@link Thread#interrupt}, so any task that
* fails to respond to interrupts may never terminate.
*
* @throws SecurityException {@inheritDoc}
*/
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
ThreadPoolExecutor 在 shutdown 之後會變成 SHUTDOWN 狀態,無法接受新的任務,隨後等待正在執行的任務執行完成。意味着,shutdown 只是發出一個命令,至於有沒有關閉還是得看線程自己。
ThreadPoolExecutor 對於 shutdownNow 的處理則不太一樣,方法執行之後變成 STOP 狀態,並對執行中的線程調用 Thread.interrupt() 方法(但如果線程未處理中斷,則不會有任何事發生),所以並不代表“立刻關閉”。
因此,Spring 對線程池進行了自己的封裝,定義了ExecutorConfigurationSupport
來進行處理。
public abstract class ExecutorConfigurationSupport extends CustomizableThreadFactory implements BeanNameAware, InitializingBean, DisposableBean {
// 關閉的鉤子 DisposableBean
public void destroy() {
this.shutdown();
}
// 清理執行線程
public void shutdown() {
if (this.logger.isInfoEnabled()) {
this.logger.info("Shutting down ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : ""));
}
if (this.executor != null) {
// 根據waitForTasksToCompleteOnShutdown 判斷是否等待線程完成
if (this.waitForTasksToCompleteOnShutdown) {
this.executor.shutdown();
} else {
Iterator var1 = this.executor.shutdownNow().iterator();
while(var1.hasNext()) {
Runnable remainingTask = (Runnable)var1.next();
this.cancelRemainingTask(remainingTask);
}
}
this.awaitTerminationIfNecessary(this.executor);
}
}
// 調用awaitTermination 判斷
private void awaitTerminationIfNecessary(ExecutorService executor) {
if (this.awaitTerminationSeconds > 0) {
try {
if (!executor.awaitTermination((long)this.awaitTerminationSeconds, TimeUnit.SECONDS) && this.logger.isWarnEnabled()) {
this.logger.warn("Timed out while waiting for executor" + (this.beanName != null ? " '" + this.beanName + "'" : "") + " to terminate");
}
} catch (InterruptedException var3) {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Interrupted while waiting for executor" + (this.beanName != null ? " '" + this.beanName + "'" : "") + " to terminate");
}
Thread.currentThread().interrupt();
}
}
}
}
因此,對於基於Spring 的線程池,建議採用Spring 提供的ThreadPoolTaskExecutor
和 ThreadPoolTaskScheduler
。因爲,他們擴展自上述ExecutorConfigurationSupport
,關閉更加優雅。
6. Spring Boot actuator提供的/shutdown
Spring Boot 的actuator 提供的shutdown 接口也僅僅是調用了context.close() 方法,因此並不是安全和友好的。
@Endpoint(id = "shutdown", enableByDefault = false)
public class ShutdownEndpoint implements ApplicationContextAware {
private static final Map<String, String> NO_CONTEXT_MESSAGE = Collections
.unmodifiableMap(Collections.singletonMap("message", "No context to shutdown."));
private static final Map<String, String> SHUTDOWN_MESSAGE = Collections
.unmodifiableMap(Collections.singletonMap("message", "Shutting down, bye..."));
private ConfigurableApplicationContext context;
@WriteOperation
public Map<String, String> shutdown() {
if (this.context == null) {
return NO_CONTEXT_MESSAGE;
}
try {
return SHUTDOWN_MESSAGE;
}
finally {
Thread thread = new Thread(this::performShutdown);
thread.setContextClassLoader(getClass().getClassLoader());
thread.start();
}
}
private void performShutdown() {
try {
Thread.sleep(500L);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
// 調用close 方法
this.context.close();
}
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
if (context instanceof ConfigurableApplicationContext) {
this.context = (ConfigurableApplicationContext) context;
}
}
}
7. 思考
經由上述分析,得出如下結果。如果不是十分肯定系統kill -9
不會出現任何數據問題,那麼請採用kill -15
來關閉系統。因此,在設計系統時,一定要考慮系統的退出策略。這樣才能開發出更加健壯的系統。