進程的優雅退出

Kill -9 PID 帶來的問題

在 Linux 上通常會通過 kill -9 pid 的方式強制將某個進程殺掉,這種方式簡單高效,因此很多程序的停止腳本經常會選擇使用 kill -9 pid 的方式。

無論是 Linux 的 Kill -9 pid 還是 windows 的 taskkill /f /pid 強制進程退出, 都會帶來一些副作用:對應用軟體而言其效果等同於突然掉電,可能會導致如下一些問題:

  1. 緩存中的數據尚未持久化到磁碟中,導致數據丟失;
  2. 正在進行文件的 write 操作,沒有更新完成,突然退出,導致文件損壞;
  3. 線程的消息隊列中尚有接收到的請求消息還沒來得及處理,導致請求消息丟失;
  4. 資料庫操作已經完成,例如賬戶餘額更新,準備返回應答消息給客戶端時,消息尚在通信線程的發送隊列中排隊等待發送,進程強制退出導致應答消息沒有返回給客戶端,客戶端發起超時重試,會帶來重複更新問題;
  5. 其它問題等...

JAVA 優雅退出

Java 的優雅停機通常通過註冊 JDK 的 ShutdownHook 來實現,當系統接收到退出指令後,首先標記系統處於退出狀態,不再接收新的消息,然後將積壓的消息處理完,最後調用資源回收介面將資源銷毀,最後各線程退出執行。

通常優雅退出需要有超時控制機制,例如 30S,如果到達超時時間仍然沒有完成退出前的資源回收等操作,則由停機腳本直接調用 kill -9 pid,強制退出。

如何實現 Netty 的優雅退出

要實現 Netty 的優雅退出,首先需要了解通用 Java 進程的優雅退出如何實現。下面我們先講解下優雅退出的實現原理,並結合實際代碼進行講解。最後看下如何實現 Netty 的優雅退出。

信號簡介

信號是在軟體層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一個中斷請求可以說是一樣的,它是進程間一種非同步通信的機制。以 Linux 的 kill 命令為例,kill -s SIGKILL pid (即 kill -9 pid) 立即殺死指定 pid 的進程,SIGKILL 就是發送給 pid 進程的信號。

信號具有平臺相關性,Linux 平臺支持的一些終止進程信號如下所示:

Windows 平臺存在一些差異,它的一些信號舉例如下:SIGINT(Ctrl+C 中斷)、SIGILL、SIGTERM (kill 發出的軟體終止)、SIGBREAK (Ctrl+Break 中斷)。

信號選擇:為了不幹擾正常信號的運作,又能模擬 Java 非同步通知,在 Linux 上我們需要先選定一種特殊的信號。通過查看信號列表上的描述,發現 SIGUSR1 和 SIGUSR2 是允許用戶自定義的信號, 我們可以選擇 SIGUSR2,為了測試方便,在 Windows 上我們可以選擇 SIGINT。

Java 程序的優雅退出

首先看下通用的 Java 進程優雅退出的流程圖:

1, 應用進程啟動的時候,初始化 Signal 實例,它的代碼示例如下:

Signal sig = new Signal(getOSSignalType());

其中 Signal 構造函數的參數為 String 字元串,也就是 2.1.1 小節中介紹的信號量名稱。

2, 根據操作系統的名稱來獲取對應的信號名稱,代碼如下:

private String getOSSignalType()
{
return System.getProperties().getProperty("os.name").
toLowerCase().startsWith("win") ? "INT" : "USR2";
}

判斷是否是 windows 操作系統,如果是則選擇 SIGINT,接收 Ctrl+C 中斷的指令;否則選擇 USR2 信號,接收 SIGUSR2(等價於 kill -12 pid)指令。

3, 將實例化之後的 SignalHandler 註冊到 JDK 的 Signal,一旦 Java 進程接收到 kill -12 或者 Ctrl+C 則回調 handle 介面,代碼示例如下:

Signal.handle(sig, shutdownHandler);

其中 shutdownHandler 實現了 SignalHandler 介面的 handle(Signal sgin) 方法,代碼示例如下:

4, 在接收到信號回調的 handle 介面中,初始化 JDK 的 ShutdownHook 線程,並將其註冊到 Runtime 中,示例代碼如下:

private void invokeShutdownHook()
{
Thread t = new Thread(new ShutdownHook(), "ShutdownHook-Thread");
Runtime.getRuntime().addShutdownHook(t);
}

5,接收到進程退出信號後,在回調的 handle 介面中執行虛擬機的退出操作,示例代碼如下:

Runtime.getRuntime().exit(0);

虛擬機退出時,底層會自動檢測用戶是否註冊了 ShutdownHook 任務,如果有,則會自動將 ShutdownHook 線程拉起,執行它的 Run 方法,用戶只需要在 ShutdownHook 中執行資源釋放操作即可,示例代碼如下:

class ShutdownHook implements Runnable
{
@Override
public void run() {
System.out.println("ShutdownHook execute start...");
System.out.print("Netty NioEventLoopGroup shutdownGracefully...");
try {
TimeUnit.SECONDS.sleep(10);// 模擬應用進程退出前的處理操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ShutdownHook execute end...");
System.out.println("Sytem shutdown over, the cost time is 10000MS");
}
}

下面我們在 Windows 環境中對通用的 Java 優雅退出程序進行測試,打開 CMD 控制檯,拉起待測試程序,如下所示:

啟動進程:

查看線程信息,發現註冊的 ShutdownHook 線程沒有啟動,符合預期:

在控制檯執行 Ctrl+C,使進程退出,示例如下:

如上圖所示,我們定義的 ShutdownHook 線程在 JVM 退出時被執行,作為測試程序,它休眠 10S 之後退出,控制檯列印的相關信息如下:

下面我們總結下通用的 Java 程序優雅退出的技術要點:

Netty 的優雅退出

在實際項目中,Netty 作為高性能的非同步 NIO 通信框架,往往用作基礎通信框架負責各種協議的接入、解析和調度等,例如在 RPC 和分散式服務框架中,往往會使用 Netty 作為內部私有協議的基礎通信框架。

當應用進程優雅退出時,作為通信框架的 Netty 也需要優雅退出,主要原因如下:

  1. 儘快的釋放 NIO 線程、句柄等資源;
  2. 如果使用 flush 做批量消息發送,需要將積攢在發送隊列中的待發送消息發送完成;
  3. 正在 write 或者 read 的消息,需要繼續處理;
  4. 設置在 NioEventLoop 線程調度器中的定時任務,需要執行或者清理。

下面我們看下 Netty 優雅退出涉及的主要操作和資源對象:

Netty 的優雅退出總結起來有三大步操作:

  1. 把 NIO 線程的狀態位設置成 ST_SHUTTING_DOWN 狀態,不再處理新的消息(不允許再對外發送消息);
  2. 退出前的預處理操作:把發送隊列中尚未發送或者正在發送的消息發送完、把已經到期或者在退出超時之前到期的定時任務執行完成、把用戶註冊到 NIO 線程的退出 Hook 任務執行完成;
  3. 資源的釋放操作:所有 Channel 的釋放、多路復用器的去註冊和關閉、所有隊列和定時任務的清空取消,最後是 NIO 線程的退出。

下面我們具體看下如何實現 Netty 的優雅退出:

Netty 優雅退出的介面和總入口在 EventLoopGroup,調用它的 shutdownGracefully 方法即可,相關代碼如下:

bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();

除了無參的 shutdownGracefully 方法,還可以指定退出的超時時間和週期,相關介面定義如下:

EventLoopGroup 的 shutdownGracefully 工作原理下個章節做詳細講解,結合 Java 通用的優雅退出機制,即可實現 Netty 的優雅退出,相關偽代碼如下:

// 統一定義 JVM 退出事件,並將 JVM 退出事件作為主題對進程內部發布
// 所有需要優雅退出的消費者訂閱 JVM 退出事件主題
// 監聽 JVM 退出的 ShutdownHook 被啟動之後,發布 JVM 退出事件
// 消費者監聽到 JVM 退出事件,開始執行自身的優雅退出
// 如果所有的非守護線程都成功完成優雅退出,進程主動退出
// 如果到了退出的超時時間仍然沒正常退出,則由停機腳本通過 kill -9 pid 強殺進程,強制退出

總結一下

JVM 的 ShutdownHook 被觸發之後,調用所有 EventLoopGroup 實例的 shutdownGracefully 方法進行優雅退出。由於 Netty 自身對優雅退出有較完善的支持,所以實現起來相對比較簡單。

我整理了一些互聯網公司java程序員在面試中涉及到的絕大部分架構面試題及答案做成了文檔和架構視頻資料免費分享給大家(包括Dubbo、Redis、Netty、zookeeper、Spring cloud、分散式、高並發等架構技術資料)也可以關注獲得更多的面試資料,節省大家收集的時間! 現在私信我【資料】免費獲取!!

Netty 優雅退出原理分析

Netty 優雅退出涉及到線程組、線程、鏈路、定時任務等,底層實現細節非常複雜,下面我們就層層分解,通過源碼來剖析它的實現原理。

NioEventLoopGroup

NioEventLoopGroup 實際是 NioEventLoop 的線程組,它的優雅退出比較簡單,直接遍歷 EventLoop 數組,循環調用它們的 shutdownGracefully 方法,源碼如下:

NioEventLoop

調用 NioEventLoop 的 shutdownGracefully 方法,首先就是要修改線程狀態為正在關閉狀態,它的實現在父類 SingleThreadEventExecutor 中,它們的繼承關係如下:

SingleThreadEventExecutor 的 shutdownGracefully 代碼比較簡單,就是修改線程的狀態位,需要注意的是修改時需要對並發調用做判斷,如果是由 NioEventLoop 自身調用,則不需要加鎖,否則需要加鎖,代碼如下:

解釋下為什麼要加鎖,因為 shutdownGracefully 是 public 的方法,任何能夠獲取到 NioEventLoop 的代碼都可以調用它,在 Netty 中,業務代碼通常不需要直接獲取 NioEventLoop 並操作它,但是 Netty 對 NioEventLoop 做了比較厚的封裝,它不僅僅只能讀寫消息,還能夠執行定時任務,並作為線程池執行用戶自定義 Task。因此在 Channel 中將獲取 NioEventLoop 的方法開放了出來,這就意味著用戶只要能夠獲取到 Channel,理論上就會存在並發執行 shutdownGracefully 的可能,因此在優雅退出的時候做了並發保護。

完成狀態修改之後,剩下的操作主要在 NioEventLoop 中進行,代碼如下:

我們繼續看下 closeAll 的實現,它的原理是把註冊在 selector 上的所有 Channel 都關閉,但是有些 Channel 正在發送消息,暫時還不能關,需要稍後再執行,核心代碼如下:

循環調用 Channel Unsafe 的 close 方法,下面我們跳轉到 Unsafe 中,對 close 方法進行分析。

AbstractUnsafe

AbstractUnsafe 的 close 方法主要做了如下幾件事:

  • 判斷當前該鏈路是否有消息正在發送,如果有則將關閉操作封裝成 Task 放到 eventLoop 中稍後再執行:
  • 將發送隊列清空,不再允許發送新的消息:
  • 調用 SocketChannel 的 close 方法,關閉鏈路:

  • 調用 pipeline 的 fireChannelInactive,觸發鏈路關閉通知事件:
  • 最後是調用 deregister,從多路復用器上取消 SelectionKey:

至此,優雅退出流程已經完成,這是否意味著 NioEventLoop 線程可以退出了,其實並非如此。

在此處,只是做了 Channel 的關閉和從 Selector 上的去註冊,總結如下:

  1. 通過 inFlush0 來判斷當前是否正在發送消息,如果是,則不執行 Channel 關閉動作,放入 NIO 線程的任務隊列中稍後再執行 close() 操作;
  2. 因為已經不允許新的發送消息加入,一旦發送操作完成,就執行鏈路關閉、觸發鏈路關閉事件和從 Selector 上取消註冊操作。

之前已經說了,NioEventLoop 除了 I/O 讀寫之外,還兼具定時任務執行、關閉 ShutdownHook 的執行等,如果此時有到期的定時任務,即使 Chanel 已經關閉,但是仍然需要繼續執行,線程不能退出。下面我們具體分析下 TaskQueue 的處理流程。

TaskQueue

NioEventLoop 執行完 closeAll()操作之後,需要調用 confirmShutdown 看是否真的能夠退出,它的處理邏輯如下:

執行 TaskQueue 中排隊的 Task,代碼如下:

執行註冊到 NioEventLoop 中的 ShutdownHook,代碼如下:

判斷是否到達優雅退出的指定超時時間,如果達到或者過了超時時間,則立即退出,代碼如下:

如果沒到達指定的超時時間,暫時不退出,每隔 100MS 檢測下是否有新的任務加入,有則繼續執行:

在 confirmShutdown 方法中,夾雜了一些對已經廢棄的 shutdown()方法的處理,例如:

調用新的 shutdownGracefully 系列方法,該判斷條件是永遠都不會成立的,因此對於已經廢棄的 shutdown 相關的處理邏輯,不再詳細分析。

到此為止,confirmShutdown 方法講解完畢,confirmShutdown 返回 true,則 NioEventLoop 線程正式退出,Netty 的優雅退出完成,代碼如下:

本文到此結束!喜歡的朋友幫忙轉發下文章和關注一下,感謝支持!!

現在私信我【資料】免費獲取面試題及答案和更多關於分散式架構、高可擴展、高性能、高並發、性能優化、Spring boot、Redis等架構視頻資料!


推薦閱讀:
相關文章