Spring 及 Spring Boot 進程優雅停止方式

原文: Spring 及 Spring Boot 進程優雅停止方式

1. 背景

一個http 的請求處理是需要時間的,同時一個應用的關閉也是需要時間。那麼,我們該如何來關閉一個正在運行中的Spring 或者Sping Boot 項目呢?關閉應用時,我們需要思考如下問題:

  1. 內存中仍存在沒有處理完的數據,比如等待同步的List
  2. 對於Java 的任務處理ExecutorService 中仍然有任務在等待
  3. 對於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時,我們監聽該事件:

  1. 先禁止server 繼續接收請求。
  2. 然後判斷server 的工作線程是否已經全部完成。
  3. 執行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 提供的ThreadPoolTaskExecutorThreadPoolTaskScheduler。因爲,他們擴展自上述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來關閉系統。因此,在設計系統時,一定要考慮系統的退出策略。這樣才能開發出更加健壯的系統。
csdnblogjvmexit04

8. 參考

  1. Java 優雅地退出程序
  2. 研究優雅停機時的一點思考
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章