Kill -9 PID 帶來的問題
在 Linux 上通常會通過 kill -9 pid 的方式強制將某個進程殺掉,這種方式簡單高效,因此很多程序的停止腳本經常會選擇使用 kill -9 pid 的方式。
無論是 Linux 的 Kill -9 pid 還是 windows 的 taskkill /f /pid 強制進程退出, 都會帶來一些副作用:對應用軟體而言其效果等同於突然掉電,可能會導致如下一些問題:
Java 的優雅停機通常通過註冊 JDK 的 ShutdownHook 來實現,當系統接收到退出指令後,首先標記系統處於退出狀態,不再接收新的消息,然後將積壓的消息處理完,最後調用資源回收介面將資源銷毀,最後各線程退出執行。
通常優雅退出需要有超時控制機制,例如 30S,如果到達超時時間仍然沒有完成退出前的資源回收等操作,則由停機腳本直接調用 kill -9 pid,強制退出。
要實現 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 進程優雅退出的流程圖:
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 作為高性能的非同步 NIO 通信框架,往往用作基礎通信框架負責各種協議的接入、解析和調度等,例如在 RPC 和分散式服務框架中,往往會使用 Netty 作為內部私有協議的基礎通信框架。
當應用進程優雅退出時,作為通信框架的 Netty 也需要優雅退出,主要原因如下:
下面我們看下 Netty 優雅退出涉及的主要操作和資源對象:
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 優雅退出涉及到線程組、線程、鏈路、定時任務等,底層實現細節非常複雜,下面我們就層層分解,通過源碼來剖析它的實現原理。
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 方法主要做了如下幾件事:
至此,優雅退出流程已經完成,這是否意味著 NioEventLoop 線程可以退出了,其實並非如此。
在此處,只是做了 Channel 的關閉和從 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等架構視頻資料!