Spring——項目優雅停機


 

前言

最近,公司項目要做灰度發布,則要先實現項目無縫上下線,如絲般順滑,我們給應用添加優雅停機功能。

什么是優雅停機:

  • 就是對應用進程發送停止指令之后,執行的一系列保證應用正常關閉的操作。這些操作往往包括等待已有請求執行完成、關閉線程、關閉連接和釋放資源等
  • 就是對應用進程發送停止指令之后,能保證正在執行的業務操作不受影響,可以繼續完成已有請求的處理,但是停止接受新請求
  • 本質上是JVM即將關閉前執行的一些額外的處理代碼
  • 可以避免非正常關閉程序可能造成數據異常或丟失,應用異常等問題

優雅停機主要處理:

  • 池化資源的釋放:數據庫連接池,HTTP 連接池,線程池
  • 在處理線程的釋放:已經被連接的HTTP請求
  • mq消費者的處理:正在處理的消息
  • 隱形受影響的資源的處理:Zookeeper、Nacos實例下線等

未優雅停機:

當我們停止正在運行的應用程序或進程時,底層操作系統會向進程發送終止信號。在沒有啟用任何優雅關閉機制的情況下(如:kill -9),Spring Boot 應用程序將在收到信號后立即終止。

此時一些沒有執行完的程序就會直接退出,可能導致業務邏輯執行失敗,在一些業務場景下:會出現數據不一致的情況,事務邏輯不會回滾。

優雅停機使用場景: 

  • 一個基於springboot的服務,服務從網絡接收請求,再把請求任務放入隊列里交給線程池取異步消費請求任務。怎么樣停止服務才能保證任務隊列里的請求都處理完成了呢?

 

JVM 中的實現

編程語言都會提供監聽當前線程終結的函數,比如在Java中,我們可以通過如下操作監聽我們的退出事件:

public class Main {
    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new ExitHook());
        System.out.println("Do something, will exit");
    }
}

class ExitHook extends Thread{
    public void run() {
        System.out.println("exiting. clear resources...");
    }
}

我們會得到如下的結果:

Do something, will exit
exiting. clear resources...

聰明的你一定發現了,我們可以在 ExitHook 去處理那些資源的釋放。那么,在實際應用中是如何體現優雅停機呢? 

kill -15 pid

通過以上命令發送一個關閉信號給到jvm, 然后就開始執行 Shutdown Hook 了。但是值得注意的是不能夠使用以下命令:

kill -9 pid

如果這么干的話,相當於從OS方面直接將其所有的資源回收,類比一下好比強行斷電,就沒有任何進行優雅停機的機會了。

不過這里是最簡單的實現,在實際的工作中,我們會遇見各種情況:在清理退出的時候出現異常怎么處理?清理的時間過長怎么處理?等等問題。

因此在Spring中,為了簡化這樣的操作已經幫助我們封裝了一些。

 

Spring 的模式

Spring一個IOC容器,他能夠管理的是收到其托管的對象,因此我們也可以很合理的想到我們需要定義托管對象的解構函數才能夠被Spring在退出時釋放,我們將問題簡單化點,有三個事情是Spring需要解決的:

  • Spring 需要知道 Runtime 在退出
  • Spring 知道需要釋放哪些資源
  • Spring 需要知道如何釋放資源

因為 Spring 版本繁雜,以 org.springframework.boot:2.1.14.RELEASE 版本分析為例。

Spring  需要知道 Runtime 在退出

畢竟 Spring 也是 JVM 上的實現,這一切勢必也依賴於 JVM 的 Shuthook,秘密就在 org.springframework.context.support.AbstractApplicationContext#registerShutdownHook 處:

@Override
public void registerShutdownHook() {
    if (this.shutdownHook == null) {
        // No shutdown hook registered yet.
        this.shutdownHook = new Thread() {            
            public void run() {
                synchronized (startupShutdownMonitor) {
                    doClose();
                }
            }
        };
        Runtime.getRuntime().addShutdownHook(this.shutdownHook);
    }
}

Spring 知道需要釋放哪些資源

回憶一下 Spring Bean 生命周期。 

Bean的生命周期銷毀:ContextClosedEvent、@PreDestroy、DisposableBean

當 Spring Context 銷毀的時候,會調用 destroy() 函數:org.springframework.context.support.AbstractApplicationContext#destroy

@Deprecated //Spring 5 即將廢棄
public void destroy() {
    close();
}

@Override
public void close() {
    synchronized (this.startupShutdownMonitor) {
        doClose();
        // If we registered a JVM shutdown hook, we don't need it anymore now:
        // We've already explicitly closed the context.
        if (this.shutdownHook != null) {
            try {
                Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
            }
            catch (IllegalStateException ex) {
                // ignore - VM is already shutting down
            }
        }
    }
}

實則我們定位到最終的釋放資源處就是 org.springframework.context.support.AbstractApplicationContext#doClose 函數,我們在盡情的分析一下。

protected void doClose() {
    LiveBeansView.unregisterApplicationContext(this);
    try {
        // Publish shutdown event.
        publishEvent(new ContextClosedEvent(this)); ➀
    }
    // Stop all Lifecycle beans, to avoid delays during individual destruction.
    if (this.lifecycleProcessor != null) {
        try {
            this.lifecycleProcessor.onClose(); ➁
        }
        catch (Throwable ex) {
            logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
        }
    }

    // Destroy all cached singletons in the context's BeanFactory.
    destroyBeans(); ➂

    // Close the state of this context itself.
    closeBeanFactory(); ➃

    // Let subclasses do some final clean-up if they wish...
    onClose(); ➄

    // Reset local application listeners to pre-refresh state.
    if (this.earlyApplicationListeners != null) {
        this.applicationListeners.clear();
        this.applicationListeners.addAll(this.earlyApplicationListeners);
    }

    // Switch to inactive.
    this.active.set(false);
}

剪去那些無影響的代碼部分,我們可以發現對於 Spring 來說,真正關閉的順序是:

  1. 發布一個關閉事件
  2. 調用生命周期處理器
  3. 銷毀所有的Bean
  4. 關閉Bean工廠
  5. 調用子類的Close函數

對於 ➀➁ 是回調機制,不涉及到對象的銷毀,➄ 是對於繼承類的調用,所有的銷毀都在 ➂ 中。受到 Spring 托管的對象繁多,不一定所有的對象都需要銷毀行為。進一步定位一下,我們就發現了:

public void destroySingleton(String beanName) {
    this.removeSingleton(beanName);
    DisposableBean disposableBean;
    synchronized(this.disposableBeans) {
        disposableBean = (DisposableBean)this.disposableBeans.remove(beanName);
    }
    this.destroyBean(beanName, disposableBean);
}

實際上那些需要銷毀的對象都應該是 DisposableBean 對象。

那我們對於第二個問題也知道了,Spring會銷毀那些 DisposableBean 類型的 Bean對象。

Spring 需要知道如何釋放資源

其實這是一個不是問題的問題,對於 DisposableBean 來說僅僅需要實現一個接口

public interface DisposableBean {
    void destroy() throws Exception;
}

對於需要實現釋放資源的對象需要自行實現此接口。

組合在一起

我們已經知道我們最開始提出的 3 個問題,讓我們試着用這3個問題的答案拼湊處一個 Spring Web Server 是如何優雅的停機的。

那我們需要證實一件事情: Web Server內部的資源都是 DisposableBean,並且受 Spring 托管。

通過反向定位的辦法,可以快速的定位到比如 數據庫的資源 org.springframework.orm.jpa.AbstractEntityManagerFactoryBean#destroy 在銷毀的階段會將 Entity 對象進行銷毀。

對於收到 Spring 托管的對象的優雅停機的路徑是:

Runtine Shutdown Hook -> Context:destory() -> DisposableBean:destroy()

對於大部分的資源比如數據庫,服務發現,等等都是這樣的銷毀方式。

進階: Web 容器

一個疑問

對於普通的 Bean 的銷毀我們已經完全了解,但是對於動手做實驗的不知道有沒有發現,其實 Web Server 並不是在 destroySingleton 階段進行銷毀的,那他是在哪里銷毀的呢?

回憶一下,我們除了銷毀Beans 之外,是不是還有最后一個 close() 函數可以調用,沒有錯!對於 Tomcat ... 這些web容器來說,本身就是 ApplicationContext 的一個子類並非是 Bean 一部分,

因此他們的 close() 函數在 org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext

protected void onClose() {
    super.onClose();
    this.stopAndReleaseWebServer();
}

因此 stop webserver 是在 destory context 之后銷毀的,那我們豈不是會出現一邊在接受請求但是這些請求都是會失敗的嗎?如果是是這樣的,可真是太愚蠢的設計了。

另辟蹊徑

對於這樣的情況,我們在 shutdown 之前讓 Web server 停止接受任何的請求,但可惜的是在此版本的 tomcat 不支持此特效,需要待 9.0.33+ spring-boot-2-3-0-available-now

在早期的版本中(spring boot 2.3.0 之前),我們依然可以通過一些額外的方式將這件事情做到:請查閱下面【Spring Boot < 2.3.0:內置容器】章節內容

又一個疑問?

在 Spring-Boot-2.3.0 之前我們都需要這么處理嗎?我的天,很多寫了 Spring 已經超過十年了,難道 Spring 一直沒有解決這個問題嗎?答案也是否定的。

還記得我們在 Spring 的早期階段,我們通過 XML 來構造一個Spring項目的時候嗎?

那時候的方案是將我們的Web 程序作為一個 War 提供給 Tomcat 的 webapps中,此時tomcat會嘗試構造我們的 DispatcherServlet 將整個系統運作起來,

而在 Shutdown 階段,這樣的邏輯也是由 Tomcat 進行處理的。也就是說,對於 Embeded Web Server 的 Spring Boot 和 傳統的 Web Server 在 Destory Spring Applicaion Context 這一步的時間是不一樣的,

Spring Boot with Embeded Web Server 在 2.3.0 之前的版本都是先關閉 Context 上下文再關閉 Web容器,而傳統的 Web Server 是先關閉 Web容器 再去關閉 Context 上下文。

對於 傳統的 Web Server:

Runtime Shutdwon Hook -> org.apache.catalina.util.LifecycleBase#stop -> Spring Context Stop

因此出現優雅停機問題的集中在 Spring Boot < 2.3.0 的版本內。

 

如何優雅停機

傳統的 Tomcat 容器

我們執行 catalina.sh stop <WAITING SECONDS> 就可以執行 Tomcat 的優雅停機。

Spring Boot > 2.3.0:內置容器

Springboot2.3.0 之后默認完成了優雅停機。

  • 啟用正常停機

可以通過在應用程序配置文件中設置兩個屬性來進行:

# 開啟優雅停機
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s

1、 server.shutdown 屬性可以支持的值有兩種:

  • immediate 這是默認值,配置后服務器立即關閉,無優雅停機邏輯。
  • graceful 開啟優雅停機功能,並遵守 spring.lifecycle.timeout-per-shutdown-phase 屬性中給出的超時來作為服務端等待的最大時間。

2、spring.lifecycle.timeout-per-shutdown-phase 服務端等待最大超時時間,采用java.time.Duration格式的值,默認30s。

     當我們使用了如上配置開啟了優雅停機功能,當我們通過SIGTERM信號關閉 Spring Boot 應用時:

  • 此時如果應用中沒有正在進行的請求,應用程序將會直接關閉,而無需等待超時時間結束后才關閉。
  • 此時如果應用中有正在處理的請求,則應用程序將等待超時時間結束后才會關閉。如果應用在超時時間之后仍然有未處理完的請求,應用程序將拋出異常並繼續強制關閉。

Spring Boot 的所有嵌入式服務器都支持優雅終止。但是,拒絕新請求的方式可能會因各個服務器的實現而異(見下圖)。

web 容器名稱 行為說明
Tomcat 9.0.33+ 停止接受網絡層的請求,客戶端新請求等待超時。
Reactor Netty 停止接受網絡層的請求,客戶端新請求等待超時。
Undertow 接受請求,客戶端新請求直接返回 503。
Jetty 停止接受網絡層的請求,客戶端新請求等待超時。
  • 正常關機方法

1、執行 kill -2 或者 kill -15

kill -2或-15 相當於快捷鍵 Ctrl + C 會觸發 Java 的 ShutdownHook 事件處理,一定不要使用 kill -9,暴力美學強制殺死進程,不會執行 ShutdownHook

優雅停機或者一些后置處理可參考以下源碼:

public abstract class AbstractApplicationContext {
    ......
    public void registerShutdownHook() {
        if (this.shutdownHook == null) {
            this.shutdownHook = new Thread("SpringContextShutdownHook") {
                public void run() {
                    synchronized(AbstractApplicationContext.this.startupShutdownMonitor) {
                        AbstractApplicationContext.this.doClose();
                    }
                }
            };
            Runtime.getRuntime().addShutdownHook(this.shutdownHook);
        }
    }

    /** @deprecated */
    @Deprecated
    public void destroy() {
        this.close();
    }

    public void close() {
        Object var1 = this.startupShutdownMonitor;
        synchronized(this.startupShutdownMonitor) {
            this.doClose(); //重點:銷毀bean if (this.shutdownHook != null) {
                try {
                    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);
                }
            }

            this.destroyBeans();
            this.closeBeanFactory();
            this.onClose();
            if (this.earlyApplicationListeners != null) {
                this.applicationListeners.clear();
                this.applicationListeners.addAll(this.earlyApplicationListeners);
            }
            this.active.set(false);
        }
    }
    ......
}

2、通過 actuate 端點實現優雅停機

POST 請求 /actuator/shutdown 即可執行優雅關機。

pom.xml 需引入依賴如下:

  <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

application.properties 需配置如下:

management.endpoint.shutdown.enabled=true
management.endpoints.web.exposure.include=shutdown

優雅停機或者一些后置處理可參考以下源碼:

@Endpoint(id = "shutdown", enableByDefault = false)
public class ShutdownEndpoint implements ApplicationContextAware {

    @WriteOperation
    public Map<String, String> shutdown() {
        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 邏輯和上邊 shutdownhook 的處理一樣,其實就是調用AbstractApplicationContext的close()方法
        this.context.close();
    }
}

Spring Boot < 2.3.0:內置容器

Springboot2.3.0 之前需要自己實現優雅停機。

  • 方法 1:自己實現優雅停機

創建SafetyShutDownConfig類實現TomcatConnectorCustomizer,ApplicationListener<ContextClosedEvent>接口即可,kill -2和kill -15就可以進行測試。

實現 TomcatConnectorCustomizer 接口,定制 Connector 的行為,實現 ApplicationListener<ContextClosedEvent> 接口,監聽 Spring 容器的關閉事件,即當前的 ApplicationContext 執行 close() 方法,

這樣我們就可以在請求處理完畢后進行 Tomcat 線程池的關閉,具體的實現代碼如下:

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

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 {
                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) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

有了定制的 Connector 回調,還需要在啟動過程中添加到內嵌的 Tomcat 容器中,然后等待監聽到關閉指令時執行,addConnectorCustomizers 方法可以把定制的 Connector 行為添加到內嵌的 Tomcat 中,具體代碼如下:

@Bean
public ConfigurableServletWebServerFactory tomcatCustomizer() {
    TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
    factory.addConnectorCustomizers(gracefulShutdown());
    return factory;
}
  • 方法 2:基於平台實現

現在的很多應用都跑在 kubernetes 這樣的容器平台上,此時我們 POD 在進行 terminated 操作的時候,會首先向運行的進程發送一個 SIGTERM 指令,然后等待 30秒,

在30后沒有終結的話,會再次發送一個 SIGKILL 進行強制終結,因此對於容器平台,我們要注意的這一個等待時間是否足夠進行資源的回收。

但是我們還有一個問題,就是正在請求的流量問題,對於這個問題我們需要進行組合拳,還記得 kubernetes 中有 readinessProbe 的概念嗎?

當我們的Pod啟動的時候,如果 Readiness 未就緒, kubernetes 也不會將我們的 POD 作為 SVC 的可選地址(采用SVC負債均衡的情況下)。

因此我們的應用在接受到 SIGTERM 的第一時刻就將Readiness 的地址進行失敗行為,並且等待一定時間之后再進行 Spring Context Shutdown 操作。

但是對於超長時間的請求依然會有失敗的可能,不過對於大部分的應用來說,優雅停機本身也只是等待固定時間,因此對於超長持續的請求讓其失敗也是可選的方案。

 

微服務優雅停機 

前面說的,是基於單機版本的優雅停機,在關閉時,只是保證了服務端內部線程執行完畢,調用方的狀態是沒關注的。

不論是Dubbo還是Spring Cloud 的分布式服務框架,需要關注的是怎么能在服務停止前,先將提供者在注冊中心進行反注冊,然后在停止服務提供者,這樣才能保證業務系統不會產生各種503、timeout等現象。

背景

在生產環境中,隨着雲原生架構的發展,自動的彈性伸縮、滾動升級、分批發布等雲原生能力讓用戶享受到了資源、成本、穩定性的最優解。

但是在應用的縮容、發布等過程中,由於實例下線處理得不夠優雅,將會導致短暫的服務不可用,短時間內業務監控會出現大量 io 異常報錯;

如果業務沒做好事務,那么還會引起數據不一致的問題,那么需要緊急手動訂正錯誤數據;甚至每次發布,您需要發告示停機發布,否則您的用戶會出現一段時間服務不可用。

沒處理好服務實例下線,無論發生上述哪種情況,都會對您業務的連續性造成困擾。

對於任何一個線上應用,如何在服務更新部署過程中保證業務無感知是開發者必須要解決的問題,即從應用停止到重啟恢復服務這個階段不能影響正常的業務請求,

這使得無損下線成為應用生命周期中必不可少的一個環節。

微服務下的問題

一個 Spring Cloud 應用正常分批發布的流程:

  1. 服務發布前,消費者根據負載均衡規則調用服務提供者,業務正常。
  2. 服務提供者 B 需要發布新版本,先對其中的一個節點進行操作,先是正常停止 Java 進程。
  3. 服務停止過程中,首先去注冊中心注銷服務,然后等待服務端線程處理完成,再停止服務。
  4. 注冊中心則將通知消費者,其中的一個服務提供者節點已下線。這個過程包含推送和輪詢兩種方式,推送可以認為是准實時的,輪詢的耗時由服務消費者輪詢間隔決定,最差的情況下需要 1 分鍾。
  5. 服務消費者刷新服務列表,感知到服務提供者已經下線了一個節點,但是這個過程中Spring Cloud 的負載均衡組件 Ribbon 默認的刷新時間是 30 秒 ,最差情況下需要耗時 30 秒。
  6. 服務消費者不再調用已經下線的節點。

我們看到,當一個Spring Cloud服務端通過SpringBoot提供的graceful shutdown下線時,它會拒絕客戶端新的請求,並且等待已經在處理的線程處理完成后,或者在配置的應用最長等待時間到了之后進行下線。

但是在服務端重啟開始拒絕客戶端新的請求的時刻開始,即執行了Connectors.stop開始,到客戶端感知到服務端該實例下線這段時間內,客戶端向該實例發起的所有請求都會被拒絕,從而引起服務調用異常。

如果客戶端考慮增加重試能力,這一定程度上可以緩解發布過程中服務調用報錯的問題,但是無法根本上保證下線過程的無損,

如果服務調用報錯期過程,或者分批發布時候同一批次下線的節點數過多,無法保證僅僅增加多次重試就能夠調用到未下線的節點上。

這不能根本解決問題!同時需要考慮配置重試帶來的業務上存在不冪等的風險。 

阿里雲EDAS無損下線

阿里雲EDAS應用無損下線的設計:

如圖看到,我們通過3個步驟的增強,主動注銷、服務提供者通知下線信息、服務消費者調用其他服務提供者。

可以看到,真正做到無損下線能力是需要客戶端增強一起聯動的

  • 主動注銷:我們在應用服務下線前,主動通知注冊中心注銷該實例
  • 通知下線信息:我們會在服務端實例下線前主動通知客戶端,該服務節點下線的信息
  • 調用其他提供者:我們在客戶端增強其負載均衡能力,在服務端下線后,客戶端主動調用其他服務提供者節點

本人生產環境無損下線

項目說明:

非容器環境,我們應用服務之間使用openFeign進行通信,使用ribbon進行負載均衡,ribbon會緩存服務列表(每30s刷新一次),默認情況下,nacos服務下線不會即時刷新ribbon緩存服務列表。

借鑒了阿里雲EDAS應用無損下線的設計思想,在自己公司的微服務中采用以下方法實現無損下線:

  1. 實現單個springboot應用的優雅停機;
  2. 配置客戶端重試機制,服務端接口支持冪等操作;
  3. springboot應用中暴露actuator的service-registry端點(即暴露nacos服務下線的http請求地址);
  4. 編寫停止應用的腳本:首先調用服務下線的請求地址讓服務下線,然后休眠一段時間(ribbon服務列表刷新時間),最后再停止應用;
#!/bin/bash
APP_NAME="masl"
APP_PORT="8080"
jar_name="$APP_NAME.jar"
pid=`ps -ef | grep $jar_name | grep -v grep | awk '{print $2}'`
if [ -n "$pid" ]
then
   echo "begin call nacos offline."
   curl -X "POST" "http://localhost:$APP_PORT/$APP_NAME/actuator/service-registry?status=DOWN" -H "Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8"
   echo "begin sleep 10s, wait ribbon server list refresh."
   sleep 30
   echo "begin stop app, kill pid:" $pid
   kill -15 $pid
fi

注:因nacos服務下線不會即時刷新ribbon緩存服務列表,所以步驟3要休眠一段時間,這種方法不夠優雅,可以有更好的方法,但需要擴展實現:NamingService通過subscribe方法如何感知到實例的上下線。

 

優雅停機其它

dubbo優雅下線

dubbo默認開啟了優雅停機。下線分為從注冊中心下線,關閉協議。2.7之后源碼如下:ShutdownHookListener,繼承 Spring ApplicationListener 接口,用以監聽 Spring 相關事件。

這里 ShutdownHookListener 僅僅監聽 Spring 關閉事件,當 Spring 開始關閉,將會觸發 ShutdownHookListener 內部邏輯。

public class SpringExtensionFactory implements ExtensionFactory {
    private static final Logger logger = LoggerFactory.getLogger(SpringExtensionFactory.class);

    private static final Set<ApplicationContext> CONTEXTS = new ConcurrentHashSet<ApplicationContext>();
    private static final ApplicationListener SHUTDOWN_HOOK_LISTENER = new ShutdownHookListener();

    public static void addApplicationContext(ApplicationContext context) {
        CONTEXTS.add(context);
        if (context instanceof ConfigurableApplicationContext) {
            // 注冊 ShutdownHook
            ((ConfigurableApplicationContext) context).registerShutdownHook();
            // 取消 AbstractConfig 注冊的 ShutdownHook 事件
            DubboShutdownHook.getDubboShutdownHook().unregister();
        }
        BeanFactoryUtils.addApplicationListener(context, SHUTDOWN_HOOK_LISTENER);
    }
    // 繼承 ApplicationListener,這個監聽器將會監聽容器關閉事件
    private static class ShutdownHookListener implements ApplicationListener {
        @Override
        public void onApplicationEvent(ApplicationEvent event) {
            if (event instanceof ContextClosedEvent) {
                DubboShutdownHook shutdownHook = DubboShutdownHook.getDubboShutdownHook();
                shutdownHook.doDestroy();
            }
        }
    }
}

注:通過配置dubbo.application.shutwait=30s可以設置dubbo等待時間。

線程池優雅關閉

Spring托管的線程池默認完成了優雅關閉。自定義的線程池優雅關閉的方法如下:

private ThreadPoolExecutor executor;

    @Bean
    @Primary
    public ThreadPoolExecutor asyncServiceExecutor() {
        executor = new ThreadPoolExecutor(5, 20, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000), new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }

    @PreDestroy
    public void destroyThreadPool() {
        if (!executor.isTerminated()){
            executor.shutdown();
            try {
                executor.awaitTermination(10, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
                executor.shutdownNow();
            }
        }
        log.info("ThreadPoolExecutor destroyed !");
    }

mq消費者優雅關閉

Spring托管的MQ消費者默認完成了優雅關閉。 

添加ShutdownHook

自定義添加ShutdownHook,有幾種簡單的方式。執行順序:contextCloseEvent > disposableBean.destroy() > @PreDestroy

  • 實現ApplicationListener接口,實現contextClosedEvent事件
public class APIService implements ApplicationListener<ContextClosedEvent>
{
    @Override
    public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
        //Do shutdown work.
    }
}
  • 實現DisposableBean接口,實現destroy方法
@Slf4j
@Service
public class DefaultDataStore implements DisposableBean {

    private final ExecutorService executorService = new ThreadPoolExecutor(
      OSUtil.getAvailableProcessors(), OSUtil.getAvailableProcessors() + 1, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(200), new DefaultThreadFactory("UploadVideo")); @Override public void destroy() throws Exception { log.info("准備優雅停止應用使用 DisposableBean"); executorService.shutdown(); } }
  • 使用@PreDestroy注解
@Slf4j
@Service
public class DefaultDataStore {

    private final ExecutorService executorService = new ThreadPoolExecutor(
      OSUtil.getAvailableProcessors(), OSUtil.getAvailableProcessors() + 1, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(200), new DefaultThreadFactory("UploadVideo")); @PreDestroy public void shutdown() { log.info("准備優雅停止應用 @PreDestroy"); executorService.shutdown(); } }

 

 

引用:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM