SOFA
Scalable Open Financial Architecture
是螞蟻金服自主研發的金融級分散式中介軟體,包含了構建金融級雲原生架構所需的各個元件,是在金融場景裡錘鍊出來的最佳實踐。
本文為《剖析 | SOFARPC 框架》第九篇,作者米麒麟,目前就職於陸金所。
前言
眾所周知,在微服務架構下面,當應用需要進行新功能升級釋出,或者異常關閉重啟的時候,我們會對應用的程式進行關閉,而在關閉之前,我們希望做一些諸如關閉資料庫連線,等待處理任務完成等操作,這個就涉及到我們本文中的優雅關閉功能。假如應用沒有支援優雅停機,則會帶來譬如資料丟失,交易中斷、檔案損壞以及服務未下線等情況。
微服務的優雅停機需要遵循”登出釋出服務 → 通知登出服務 → 更新服務清單 → 開啟請求遮蔽 → 呼叫銷燬業務服務 → 檢查所有請求是否完成 → 超時強制停機”應用服務停機流程。
SOFARPC 提供服務端/客戶端優雅關閉功能特性,用來解決 kill PID,應用意外自動退出譬如 System.exit()
退出 JVM,使用指令碼或命令方式停止應用等使用場景,避免服務版本迭代上線人工干預的工作量,提高微服務架構的服務高可靠性。
本文將從程式的優雅關閉,SOFARPC 應用服務優雅關閉流程,Netty 的優雅停機等方面出發詳細剖析 。
程式優雅關閉
Kill 結束程式
在 Linux上,kill 命令傳送指定的訊號到相應程式,不指定訊號則預設傳送 SIGTERM(15) 終止指定程式。如果無法終止,可以傳送 SIGKILL(9) 來強制結束程式。kill 命令訊號共有64個訊號值,其中常用的是:
2 (SIGINT:中斷,Ctrl+C)。
15 (SIGTERM:終止,預設值)。
9 (SIGKILL:強制終止)。
這裡我們重點說一下 15 和 9 的情況。
kill PID/kill -15 PID
命令系統傳送 SIGTERM 程式訊號給響應的應用程式,當應用程式接收到 SIGTERM 訊號,可以進行釋放相應資源後再停止,此時程式可能仍然繼續執行。
而kill -9 PID
命令沒有給程式遺留善後處理的條件。應用程式將會被直接終止。
對微服務應用而言其效果等同於突然斷電,強行終止可能會導致如下幾方面問題:
-
快取資料尚未持久化到磁碟,導致資料丟失;
-
檔案寫操作正在進行未更新完成,突然退出程式導致檔案損壞;
-
執行緒訊息佇列尚有接收到的請求訊息,未能及時處理,導致請求訊息丟失;
-
資料庫事務提交,服務端提供給客戶端請求響應,訊息尚在通訊執行緒傳送佇列,程式強制退出導致客戶端無法接收到響應,此時發起超時重試帶來重複更新。
所以支援優雅關閉的前提是關閉的時候,不能被直接 通過傳送訊號為 9 的 Kill 來強制結束。當然,其實我們也可以對外統一暴露應用程式管理的 API 來進行控制。本文暫時不做討論。
Java 優雅關閉
當應用程式收到訊號為15的關閉命令時,可以進行相應的響應,Java 程式的優雅停機通常通過註冊 JDK 的 ShutdownHook 來實現,當應用系統接收到退出指令,首先 JVM 標記系統當前處於退出狀態,不再接收新的訊息,然後逐步處理推積的訊息,接著呼叫資源回收介面進行資源銷燬,例如記憶體清理、物件銷燬等,最後各執行緒退出業務邏輯執行。
優雅停機需要超時控制機制,即到達超時時間仍然尚未完成退出前資源回收等操作,則通過停機指令碼呼叫kill-9 PID
命令強制退出程式。
其中 JVM 優雅關閉的 流程主要的階段如下圖所示:
如圖所示,Java程式優雅退出流程包括如下五個步驟:
-
應用程式啟動,初始化 Signal 例項;
-
根據作業系統型別,獲取指定程式訊號;
-
實現 SignalHandler 介面,例項化並註冊到 Signal,當 Java 程式接收到譬如 kill -12 或者 Ctrl+C 命令訊號回撥其 handle() 方法;
-
SignalHandler 的 handle 回撥介面初始化 ShutdownHook 執行緒,並將其註冊到 Runtime 的 ShutdownHook。
-
Java 程式接收到終止程式訊號,呼叫 Runtime 的
exit()
方法退出 JVM 虛擬機器,自動檢測使用者是否註冊ShutdownHook 任務,如果有則觸發 ShutdownHook 執行緒執行自定義資源釋放等操作。
SOFARPC 優雅關閉
在程式可以進行優雅關閉後,SOFARPC 如何實現優雅關閉呢?首先 SOFARPC 對於所有可以被優雅關閉的資源設計com.alipay.sofa.rpc.base.Destroyable
介面,通過向 JVM 的 ShutdownHook 註冊來對這些可被銷燬的資源進行優雅關閉,支援銷燬前和銷燬後操作。
這裡包括兩部分:
-
作為服務端註冊 JDK 的 ShutdownHook 執行取消服務註冊、關閉服務埠等動作實現;
-
作為客戶端通過實現 DestroyHook 介面逐步處理正在呼叫的請求關閉服務呼叫。
總體設計
執行時上下文註冊 JDK 的 ShutdownHook 執行銷燬 SOFARPC 執行相關環境實現類似釋出平臺/使用者執行kill PID
優雅停機。執行時上下文 RpcRuntimeContext 靜態初始化塊註冊 ShutdownHook 函式:
static {
...
// 增加jvm關閉事件
if (RpcConfigs.getOrDefaultValue(RpcOptions.JVM_SHUTDOWN_HOOK, true)) {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("SOFA RPC Framework catch JVM shutdown event, Run shutdown hook now.");
}
destroy(false);
}
}, "SOFA-RPC-ShutdownHook"));
}
}複製程式碼
註冊本身很簡單,重要的是 destroy 方法實際上做的事情非常多。按照先後順序,大致包含如下幾個部分。
RpcRuntimeContext 銷燬服務優雅關閉完整流程:
-
設定 RPC 框架執行狀態修改為正在關閉,表示當前執行緒不再處理 RPC 服務請求;
-
遍歷執行時上下文關閉資源的銷燬鉤子,進行註冊銷燬器清理資源前期準備工作;
-
獲取釋出的服務配置反註冊服務提供者,向指定註冊中心批量執行取消服務註冊;
-
檢查當前服務端連線和佇列任務,先把佇列任務處理完畢,再緩慢關閉啟動埠;
-
關閉釋出的服務,到註冊中心取消服務釋出,取消將處理器註冊到服務端,清除服務釋出配置快取狀態;
-
關閉呼叫的服務,斷開客戶端連線取消服務呼叫,清除服務訂閱配置快取,到註冊中心取消服務訂閱;
-
遍歷註冊中心配置逐一關閉註冊中心,移除指定註冊中心配置快取;
-
不可複用長連線管理器銷燬連線,關閉客戶端的公共連線資源,清理不可複用連線快取;
-
遍歷 RPC 框架上下文已載入的模組,逐步解除安裝模組譬如負載均衡、鏈路追蹤等;
-
遍歷執行時上下文關閉資源的解除安裝鉤子,進行註冊銷燬器清理資源後期收尾工作;
-
清理全部快取例如應用類載入器快取、服務類載入器快取以及方法物件快取等;
-
調整 RPC 框架執行狀態更新為關閉完畢,執行時上下文釋放資源關閉服務程式。
作為服務端
總體設計包含非常多的優雅關閉步驟,這裡我們再單獨介紹一下作為服務端的時候,幾個核心步驟的原理和流程,作為服務端,SOFARPC 關閉服務程式不能直接暴力關閉,而是逐步進行關閉。需要進行如下幾個步驟:
-
反註冊服務:註冊中心工廠獲取全部註冊中心例項呼叫 batchUnRegister 方法批量取消服務註冊,通知服務消費者監聽器更新其服務提供者列表,避免服務消費者繼續引用下線服務造成服務呼叫異常不可用現象。
-
關閉埠:服務端工廠檢查執行緒池 bizThreadPool 佇列是否有正在執行的請求或者佇列裡有請求,執行緒組呼叫 shutdownGracefully 方法緩慢關閉遠端服務埠,保證業務執行緒池佇列請求先處理完畢再關閉執行緒池以及埠。
-
銷燬服務物件:根據釋出/訂閱服務配置關閉提供/呼叫的服務,呼叫 unExport/unRefer 方法進行取消服務釋出/訂閱,註冊中心刪除釋出/訂閱服務配置,清理髮布/訂閱服務配置快取,防止產生 RPC 服務釋出/訂閱服務物件。
RpcRuntimeContext 銷燬服務配置資源核心實現入口:
com.alipay.sofa.rpc.context.RpcRuntimeContext#destroy()複製程式碼
作為客戶端
作為客戶端,SOFARPC 通過實現 DestroyHook 銷燬鉤子介面提供優雅關閉的鉤子,把 GracefulDestroyHook 關閉鉤子註冊到長連線管理器銷燬客戶端連線方法。客戶端優雅關閉連線實際上是 Cluster 的關閉,關閉呼叫的服務實現入口:
com.alipay.sofa.rpc.client.AbstractCluster#destroy()複製程式碼
GracefulDestroyHook 鉤子優雅關閉連線整體流程:
-
銷燬前準備斷連:獲取當前 Client 正在傳送的呼叫數量和服務消費方斷連超時時間配置,檢查是否有正在呼叫的服務請求並且當前執行時上下文時間未到達指定超時時間,滿足準備條件則當前執行緒睡眠10秒;
-
銷燬時釋放資源:關閉重連執行緒 reconThread,關閉客戶端長連線,清空當前存活+重試的客戶端列表,多執行緒執行銷燬已經建立的客戶端長連線,逐步處理正在呼叫的服務請求並且下線服務呼叫請求操作。
其中 GracefulDestroyHook 優雅關閉鉤子銷燬前準備斷連操作:
是一個自旋檢查的操作。
Netty 優雅關閉
SOFARPC 在關閉自身 RpcServer 的時候,也會關閉啟動的 Netty 服務端。這時候就涉及到 Netty 的優雅關閉。
Netty 作為高效能的非同步 NIO 通訊框架,負責各種通訊協議的接入,解析和排程,SOFABolt 是基於 Netty 最佳實踐的輕量、易用、高效能、易擴充套件的通訊框架。當微服務應用程式優雅停機,作為基礎通訊框架的 Netty 需要考慮優雅停機控制,主要原因包括以下幾方面因素:
-
儘快釋放 NIO 執行緒,清理物件控制程式碼資源;
-
使用 flush 批量傳送訊息,需要傳送積攢在通訊佇列等待傳送的訊息;
-
正在進行 read 和 write 的訊息需要繼續處理;
-
NioEventLoop 執行緒排程器配置的定時任務需要執行或者清理。
這裡是 Netty底層的實現邏輯,我們只要知道在關閉 Server的時候,需要進行相應的方法呼叫即可。
可以看到
-
設定 NioEventLoop 執行緒狀態修改為 ST_SHUTTING_DOWN,表示當前執行緒不再處理請求訊息;
-
確認關閉操作:將通訊佇列等待傳送或者正在傳送的訊息傳送完畢,把已經到期或者關閉超時之前到期的定時任務執行結束,把使用者自定義註冊到 NioEventLoop 執行緒的 ShutdownHook 關閉鉤子執行完成;
-
清理資源操作:把註冊到多路複用器 Selector 的 Channel 釋放,持有多路複用器 Selector 去註冊和關閉,通訊佇列和定時任務清空取消,修改 NioEventLoop 執行緒狀態為 ST_TERMINATED 關閉執行緒。
其中,Netty 的優雅停機核心實現入口:
io.netty.channel.EventLoopGroup#shutdownGracefully()複製程式碼
SOFABoot 優雅關閉
一個完整的微服務可能不僅僅包括SOFARPC,還可能會用到各種各樣的中介軟體,也涉及到各種流量排程等行為,所以優雅關閉是需要和釋出平臺聯動的。如果強制 kill, 那麼目前的這些優雅關閉的方案都不會生效。
所以在後續的 SOFABoot 版本中我們會增加接收一套完整的運維API,方便釋出管控平臺進行呼叫。SOFABoot 接收通過接收「關閉運維指令」而不是單純依賴 ShutdownHook 邏輯,然後觸發各個中介軟體的優雅關閉行為,其中就包括SOFAPRC的主動反註冊服務釋出和服務呼叫等關閉動作,各個中介軟體的優雅關閉執行完成後,SOFABoot 程式再退出。
總結
本文從程式的優雅關閉,到 SOFARPC 的優雅關閉支援,並詳細介紹 Netty 優雅關閉的原理。在設計優雅關閉的時候,可以考慮按照如下幾個約定來進行實現。
(1) 應用能夠支援優雅停機
(2) 優先登出註冊中心註冊的服務例項
(3) 待停機的服務應用的接入點標記拒絕服務
(4) 上游服務支援故障轉移因優雅停機而拒絕的服務
(5) 根據實際業務場景提供適當的停機介面。
相關連結
SOFA 文件: http://www.sofastack.tech/
SOFA: https://github.com/alipay
SOFARPC: https://github.com/alipay/sofa-rpc
SOFABolt: https://github.com/alipay/sofa-bolt
《剖析 | SOFARPC 框架》系列歷史文章
參與有獎調研,幫助 SOFA 成長
歡迎大家共同打造 SOFAStack https://github.com/alipay