安全優(yōu)雅的關閉SpringBoot應用程序

什么叫優(yōu)雅停機?簡單說就是,在對應用進程發(fā)送停止指令之后,能保證正在執(zhí)行的業(yè)務操作不受影響。應用接收到停止指令之后的步驟應該是,停止接收訪問請求,等待已經接收到的請求處理完成,并能成功返回。對于內部執(zhí)行的其他定時任務,也要等當前正在執(zhí)行的定時任務執(zhí)行完畢,并且不再啟動新的定時任務。這時才真正停止應用。

如果暴力的關閉應用程序,即應用收到停止指令后,立即終止,則可能會導致進程持有的全局資源得不到釋放,而其他進程也因無法獲取資源而不能處理業(yè)務。比如如果某個任務處理需要首先獲取一個redis鎖,而鎖又沒有設置過期時間,如果任務獲取鎖后還未釋放鎖就終止了,會導致資源被鎖,無法再進行處理。

本文主要針對以下兩種情形進行應用的安全優(yōu)雅關閉:

  • 對于web接口請求,應用收到終止指令后,不再接受新的web請求,對于已經接收到的請求繼續(xù)正常處理,處理完畢后再終止應用。
  • 對于應用內部執(zhí)行的定時任務(Quartz實現),不再啟動新的定時任務,并等待當前正在執(zhí)行的所有定時任務執(zhí)行完畢,然后才終止。

核心方法

核心方法就是獲取對應的線程池,通過調用Executor的shutdown來通知線程池停止接收新的任務,并等待當前已經執(zhí)行的任務執(zhí)行完畢。

kill命令的正確使用姿勢

正常關閉應用應該使用kill -15,而不是kill -9,-9是暴力終止,直接在操作系統底層將應用殺死,是應用程序被動終止。而-15是通知應用終止,應用收到-15終止信號后會主動執(zhí)行一些善后操作,最終主動終止,是安全終止的方式。

安全優(yōu)雅地關閉web請求

這里主要針對SpringBoot內置的Tomcat容器,不過思路都是一樣的。

主要思路就是獲取Tomcat的Connector連接器,然后通過Connector獲取其連接線程池,最終通過操作線程池安全終止來達到web請求也安全終止的目的。

實現代碼如下

@Configuration
public class CbShutdownConfig {

    public static final int waitTime = 30;

    @Bean
    public GracefulShutdown gracefulShutdown() {
        return new GracefulShutdown();
    }

    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
        tomcat.addConnectorCustomizers(gracefulShutdown());
        return tomcat;
    }

    @Slf4j
    private static class GracefulShutdown implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {

        private volatile Connector connector;

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

        @Override
        public void onApplicationEvent(ContextClosedEvent event) {
            log.info("application is going to stop. try to stop tomcat gracefully");
            this.connector.pause();
            Executor executor = this.connector.getProtocolHandler().getExecutor();
            if (executor instanceof ThreadPoolExecutor) {
                ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
                threadPoolExecutor.shutdown();
                try {
                    if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
                        log.info("Tomcat did not terminate in the specified time.");
                        threadPoolExecutor.shutdownNow();
                    }
                } catch (Exception ex) {
                    log.error("awaitTermination failed.", ex);
                    threadPoolExecutor.shutdownNow();
                }
            }
        }
    }
}

安全優(yōu)雅地關閉Quartz定時任務

這里的主要思路還是獲取Quartz的線程池,通過操作線程池安全終止來達到安全終止定時任務的目的。

首先手動指定定時任務的線程池

  private static final ExecutorService executorService = Executors.newFixedThreadPool(14);

  SchedulerJobFactory jobFactory = new SchedulerJobFactory();
  jobFactory.setApplicationContext(applicationContext);

  SchedulerFactoryBean factory = new SchedulerFactoryBean();
  factory.setGlobalJobListeners(quartzExceptionListener);
  factory.setJobFactory(jobFactory);
  factory.setTaskExecutor(executorService);

指定安全關閉此線程池的方法

public static void stopJobs() {
        log.info("start to stop all message jobs...");

        executorService.shutdown();
        try {
            if (!executorService.awaitTermination(CbShutdownConfig.waitTime, TimeUnit.SECONDS)) {
                log.warn("Executor did not terminate in the specified time.");
                List<Runnable> droppedTasks = executorService.shutdownNow();
                log.warn("Executor was abruptly shut down. " + droppedTasks.size() + " tasks will not be executed.");
            }
        } catch (Exception e) {
            log.error("stop service awaitTermination failed.", e);
            executorService.shutdownNow();
        }

        log.info("end of stopping all message jobs...");
    }

到這里,我們完成了stopJobs函數的編寫,但是這個函數應該在哪里調用呢?
一種調用方式是使用Java的鉤子函數addShutdownHook,在Java程序收到終止指令后回調stopJobs(kill -9不會回調addShutdownHook)。但是實際測試發(fā)現,如果任務中有有關數據庫的操作,會報異常:

druid datasource already closed

此異常說明數據庫連接池在任務完成之前關閉了!所以我們的目標又變成了在數據庫連接池關閉之前完成未完成的任務。

經過搜索得知,數據庫連接池之所以會提前關閉,是因為其對應的Bean被銷毀了,所以目標是在數據庫連接池Bean銷毀之前完成任務??梢允褂胹pringboot的事件監(jiān)聽。

springboot的事件監(jiān)聽:為bean之間的消息通信提供支持。當一個bean做完一件事以后,通知另一個bean知曉并做出相應處理。這時,我們需要另一個bean,監(jiān)聽當前bean所發(fā)生的事件。

  • Spring提供5種標準的事件監(jiān)聽:

上下文更新事件(ContextRefreshedEvent):該事件會在ApplicationContext被初始化或者更新時發(fā)布。也可以在調用ConfigurableApplicationContext接口中的refresh()方法時被觸發(fā)。
上下文開始事件(ContextStartedEvent):當容器ConfigurableApplicationContext的Start()方法開始/重新開始容器時觸發(fā)該事件。
上下文停止事件(ContextStoppedEvent):當容ConfigurableApplicationContext的Stop()方法停止容器時觸發(fā)該事件。
上下文關閉事件(ContextClosedEvent):當ApplicationContext被關閉時觸發(fā)該事件。容器被關閉時,其管理的所有單例Bean都被銷毀。
請求處理事件(RequestHandledEvent):在Web應用中,當一個http請求(request)結束觸發(fā)該事件。

其中ContextClosedEvent事件是通知應用即將銷毀容器中的Bean的消息。所以我們可以監(jiān)聽SpringBoot的ContextClosedEvent,在這個事件中調用任務終止的方法:

@Service
    @Slf4j
    public static class CbJobStopListener implements ApplicationListener {

        @Override
        public void onApplicationEvent(ApplicationEvent event) {
            // 在spring bean容器銷毀之前執(zhí)行的事件,防止數據庫連接池在任務終止前銷毀
            if (event instanceof ContextClosedEvent) {
                log.info("event ContextClosedEvent");
                MessageConfig.stopJobs();
            }
        }
    }

最后測試,通過日志可以發(fā)現,在應用收到終止信號后,會等待當前已經啟動的定時任務終止,并且拒絕執(zhí)行新的定時任務。完美完成目標。

2019-07-04 16:25:40 [schedulerFactoryBean_QuartzSchedulerThread] ERROR org.quartz.core.QuartzSchedulerThread - ThreadPool.runInThread() return false!
2019-07-04 16:25:40 [schedulerFactoryBean_QuartzSchedulerThread] INFO  org.quartz.simpl.RAMJobStore - All triggers of Job DEFAULT.asyncFileTaskJob set to ERROR state.
2019-07-04 16:25:40 [schedulerFactoryBean_QuartzSchedulerThread] ERROR org.springframework.scheduling.quartz.LocalTaskExecutorThreadPool - Task has been rejected by TaskExecutor
java.util.concurrent.RejectedExecutionException: Task org.quartz.core.JobRunShell@2bc6c064 rejected from java.util.concurrent.ThreadPoolExecutor@64c55739[Shutting down, pool size = 6, active threads = 6, queued tasks = 0, completed tasks = 96]
    at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
    at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
    a

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容