天天看點

Spring boot 2.3優雅下線,距離生産還有多遠?

前言

在生産環境中,随着雲原生架構的發展,自動的彈性伸縮、滾動更新、分批釋出等雲原生能力讓使用者享受到了資源、成本、穩定性的最優解。但是在應用的縮容、釋出等過程中,由于執行個體下線處理得不夠優雅,将會導緻短暫的服務不可用,短時間内業務監控會出現大量 io 異常報錯;如果業務沒做好事務,那麼還會引起資料不一緻的問題,那麼需要緊急手動訂正錯誤資料;甚至每次釋出,您需要發告示停機釋出,否則您的使用者會出現一段時間服務不可用。沒處理好服務執行個體下線,無論發生上述哪種情況,都會對您業務的連續性造成困擾。

對于任何一個線上應用,如何在服務更新部署過程中保證業務無感覺是開發者必須要解決的問題,即從應用停止到重新開機恢複服務這個階段不能影響正常的業務請求,這使得無損下線成為應用生命周期中必不可少的一個環節。

同時在多次 Dubbo Meetup 中,平滑上下線一直都是位居微服務開發痛點前 Top 3。

下面我們來了解一下 Spring Boot 2.3 中提供的新特性 Graceful Shutdown,來分析一下它對我們生産穩定性帶來什麼樣的幫助。

Spring Boot graceful shutdown

Graceful shutdown

Graceful shutdown is supported with all four embedded web servers (Jetty, Reactor Netty, Tomcat, and Undertow) and with both reactive and Servlet-based web applications. When enabled using server.shutdown=graceful, upon shutdown, the web server will no longer permit new requests and will wait for a grace period for active requests to complete. The grace period can be configured using spring.lifecycle.timeout-per-shutdown-phase. Please see the reference documentation for further details.

Spring Boot 2.3.0.RELEASE引入了Graceful Shutdown的功能。其中應用在等待下線期間對待新請求的方式,取決于我們所使用的 Server 類型。根據官方文檔Tomcat、Jetty 和 Reactor Netty将會在網絡層面停止接收新的請求。Undertow 會繼續接收新的請求,但立即會以 HTTP 503(服務不可用)來響應。

配置與使用

在Spring Boot 2.3.0中,優雅停機的使用非常簡單,可以通過在應用程式配置檔案中設定兩個屬性來進行。

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

  1. immediate 這是預設值,配置後伺服器立即關閉,無優雅停機邏輯。
  2. graceful 開啟優雅停機功能,并遵守 spring.lifecycle.timeout-per-shutdown-phase 屬性中給出的逾時來作為服務端等待的最大時間。

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

例如:Properties 檔案

1、#To enable graceful shutdown

2、server.shutdown=graceful

3、#To configure the timeout period

4、spring.lifecycle.timeout-per-shutdown-phase=20s

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

1、 此時如果應用中沒有正在進行的請求,應用程式将會直接關閉,而無需等待逾時時間結束後才關閉。

2、此時如果應用中有正在處理的請求,則應用程式将等待逾時時間結束後才會關閉。如果應用在逾時時間之後仍然有未處理完的請求,應用程式将抛出異常并繼續強制關閉。

源碼實作分析

我們以 Tomcat 為例看一下是SpringBoot 2.3如何實作graceful shutdown的

這裡注意下,Tomcat 9.0.33或更高版本,才具備graceful shutdown功能。

我們看一下 SpringBoot 的 TomcatWebServer 的實作,先看其中構造函數

1、org.springframework.boot.web.embedded.tomcat.TomcatWebServer

2、public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {

3、 Assert.notNull(tomcat, "Tomcat Server must not be null");

4、 this.tomcat = tomcat;

5、 this.autoStart = autoStart;

6、 this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;

7、 initialize();

8、}

可以看到當我們配置 server.shutdown=graceful 時,其中 gracefulShutdown 成員就不為null,而是被置為 GracefulShutdown 執行個體。

當我們關閉SpringBoot的應用容器時,會觸發其生命周期的 stop 方法,我們看到其中會執行webServer的shutDownGracefully方法

Spring boot 2.3優雅下線,距離生産還有多遠?

因為我們配置 了server.shutdown=graceful ,是以 gracefulShutdown 成員并不為null,而是會觸發 gracefulShutdown 的 shutDownGracefully 方法

Spring boot 2.3優雅下線,距離生産還有多遠?

我們看一下shutDownGracefully 方法是如何做到graceful shutdown的

Spring boot 2.3優雅下線,距離生産還有多遠?

來看一下doShutdown的邏輯

org.springframework.boot.web.embedded.tomcat.GracefulShutdown#doShutdown

private void doShutdown(GracefulShutdownCallback callback) {

List<Connector> connectors = getConnectors();
connectors.forEach(this::close);
try {
    for (Container host : this.tomcat.getEngine().findChildren()) {
        for (Container context : host.findChildren()) {
            while (isActive(context)) {
                if (this.aborted) {
                    logger.info("Graceful shutdown aborted with one or more requests still active");
                    callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
                    return;
                }
                Thread.sleep(50);
            }
        }
    }

}
catch (InterruptedException ex) {
    Thread.currentThread().interrupt();
}
logger.info("Graceful shutdown complete");
callback.shutdownComplete(GracefulShutdownResult.IDLE);           

}

先是關閉掉所有的連接配接,在網絡層停止接受請求,然後再等待所有請求處理完畢。

其中關于 spring.lifecycle.timeout-per-shutdown-phase 配置,是通過等待配置的時間後,再執行TomcatWebServer的stop方法,将其aborted成員置為true,實作如果應用在寬限期之後仍然有待處理的請求,應用程式将抛出異常并繼續強制關閉,而不是一直等待下去。

@Override

public void stop() throws WebServerException {

synchronized (this.monitor) {
    boolean wasStarted = this.started;
    try {
        this.started = false;
        try {
            if (this.gracefulShutdown != null) {
                this.gracefulShutdown.abort();
            }
            stopTomcat();
            this.tomcat.destroy();
        }
        catch (LifecycleException ex) {
            // swallow and continue
        }
    }
    catch (Exception ex) {
        throw new WebServerException("Unable to stop embedded Tomcat", ex);
    }
    finally {
        if (wasStarted) {
            containerCounter.decrementAndGet();
        }
    }
}           

void abort() {

this.aborted = true;           

在微服務場景下問題似乎依舊存在...

總結一下一個 Spring Cloud 應用正常分批釋出的流程

1、服務釋出前,消費者根據負載均衡規則調用服務提供者,業務正常。

2、服務提供者 B 需要釋出新版本,先對其中的一個節點進行操作,先是正常停止 Java 程序。

3、服務停止過程中,首先去注冊中心登出服務,然後等待服務端線程處理完成,再停止服務。

4、注冊中心則将通知消費者,其中的一個服務提供者節點已下線。這個過程包含推送和輪詢兩種方式,推送可以認為是準實時的,輪詢的耗時由服務消費者輪詢間隔決定,最差的情況下需要 1 分鐘。

5、服務消費者重新整理服務清單,感覺到服務提供者已經下線了一個節點,但是這個過程中Spring Cloud 的負載均衡元件 Ribbon 預設的重新整理時間是 30 秒 ,最差情況下需要耗時 30 秒。

6、服務消費者不再調用已經下線的節點

Spring boot 2.3優雅下線,距離生産還有多遠?

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

但是在服務端重新開機開始拒絕用戶端新的請求的時刻開始,即執行了Connectors.stop開始,到用戶端感覺到服務端該執行個體下線這段時間内,用戶端向該執行個體發起的所有請求都會被拒絕,進而引起服務調用異常。

Spring boot 2.3優雅下線,距離生産還有多遠?

如果用戶端考慮增加重試能力,這一定程度上可以緩解釋出過程中服務調用報錯的問題,但是無法根本上保證下線過程的無損,如果服務調用報錯期過程,或者分批釋出時候同一批次下線的節點數過多,無法保證僅僅增加多次重試就能夠調用到未下線的節點上。這不能根本解決問題!同時需要考慮配置重試帶來的業務上存在不幂等的風險。

EDAS 3.0 無損下線

EDAS 3.0 通過Java Agent技術無侵入增強您的應用,使其具備無損下線能力。

• 您無需修改一行代碼與配置,天然具備無侵入特點

• 同時支援 ECS 、K8s 場景

• 全面相容開源,支援開源Dubbo、Spring Cloud 以及開源微服務網關

Spring boot 2.3優雅下線,距離生産還有多遠?

EDAS的應用如何做到無損下線?

Spring boot 2.3優雅下線,距離生産還有多遠?

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

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

• 主動登出

我們在應用服務下線前,主動通知注冊中心登出該執行個體

• 通知下線資訊

我們會在服務端執行個體下線前主動通知用戶端,該服務節點下線的資訊

• 調用其他提供者

我們在用戶端增強其負載均衡能力,在服務端下線後,用戶端主動調用其他服務提供者節點

同時我們提供應用等待的邏輯,使要下線的服務端等待已經收到的請求處理完成再關閉 Spring 容器。

Spring boot 2.3優雅下線,距離生産還有多遠?

完整的解決方案

EDAS 3.0無損下線不僅僅支援 Spring Cloud 與 Dubbo 服務,我們還打通了消息、網關等微服務元件,讓您的應用在EDAS中做到全鍊路的下線無損。

EDAS 3.0支援端到端的無損下線

  • 雲上客戶存在多種微服務網關,支援主流開源微服務網關(Spring Cloud Gateway、Zuul等)的無損下線
  • 有些使用者的流量是通過 Ingress、SLB、Nginx 等方式打到服務端的場景
  • MQ消息等異步訂閱關系的微服務場景
  • K8s 使用 Service 服務發現的微服務場景

    為了做到全鍊路的無損下線,EDAS 3.0 通過無侵入的方式涵蓋多種場景的完整解決方案,確定您的釋出平滑無損。

即使面對白天大流量的場景,釋出依舊風輕雲淡。

招賢納士

我們 Dubbo / Spring Cloud 商業化團隊正在招人,除了 EDAS,我們還有 ARMS (應用實時監控服務)、MSE(微服務引擎)、ACM(應用配置管理)、SAE(Serverless 應用引擎)等獨立産品。我們在忙什麼?用心打磨這些産品,就是我們的工作。團隊的目标是将阿裡巴巴在服務治理上的最佳實踐通過産品化的形式輸出給阿裡雲上的企業客戶,幫助客戶實作業務永遠線上。

履歷投遞方式:[email protected]

作者資訊:

泮聖偉(花名:十眠)阿裡雲智能開發工程師,負責 Dubbo / Spring Cloud 商業化産品開發相關工作,目前主要關注雲原生、微服務等技術方向。