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方法

[圖片上傳失敗...(image-d6dc92-1603679419328)]

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

[圖片上傳失敗...(image-560504-1603679419328)]

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

[圖片上傳失敗...(image-dfade0-1603679419328)]

來看一下doShutdown的邏輯 org.springframework.boot.web.embedded.tomcat.GracefulShutdown#doShutdown private void doShutdown(GracefulShutdownCallback callback) {

<pre class="public-DraftStyleDefault-pre" data-offset-key="2c2ob-0-0">

<pre class="Editable-styled" data-block="true" data-editor="6l6lh" data-offset-key="2c2ob-0-0">

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);

</pre>

</pre>

}

先是關閉掉所有的連接,在網絡層停止接受請求,然後再等待所有請求處理完畢。 其中關於 spring.lifecycle.timeout-per-shutdown-phase 配置,是通過等待配置的時間後,再執行TomcatWebServer的stop方法,將其aborted成員置爲true,實現如果應用在寬限期之後仍然有待處理的請求,應用程序將拋出異常並繼續強制關閉,而不是一直等待下去。 @Override public void stop() throws WebServerException {

<pre class="public-DraftStyleDefault-pre" data-offset-key="1m0i4-0-0">

<pre class="Editable-styled" data-block="true" data-editor="6l6lh" data-offset-key="1m0i4-0-0">

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();
}
}
}

</pre>

</pre>

}

void abort() {

<pre class="public-DraftStyleDefault-pre" data-offset-key="a4ia6-0-0">

<pre class="Editable-styled" data-block="true" data-editor="6l6lh" data-offset-key="a4ia6-0-0">

this.aborted = true;

</pre>

</pre>

}

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

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

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

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

EDAS 3.0 無損下線

EDAS 3.0 通過Java Agent技術無侵入增強您的應用,使其具備無損下線能力。 • 您無需修改一行代碼與配置,天然具備無侵入特點 • 同時支持 ECS 、K8s 場景 • 全面兼容開源,支持開源Dubbo、Spring Cloud 以及開源微服務網關

[圖片上傳失敗...(image-4f9518-1603679419328)]

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

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

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

• 主動註銷 我們在應用服務下線前,主動通知註冊中心註銷該實例 • 通知下線信息 我們會在服務端實例下線前主動通知客戶端,該服務節點下線的信息 • 調用其他提供者 我們在客戶端增強其負載均衡能力,在服務端下線後,客戶端主動調用其他服務提供者節點 同時我們提供應用等待的邏輯,使要下線的服務端等待已經收到的請求處理完成再關閉 Spring 容器。

完整的解決方案

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 商業化產品開發相關工作,目前主要關注雲原生、微服務等技術方向。

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章