引言
平滑重啟(Graceful Restart)技術作為一種常用的解決方案,通過允許新進程接管而不中斷現有的請求,確保了系統的穩定運行和業務連續性。同時目前公司的服務重啟絕大部分也都適用的 go 的平滑重啟技術。
本部分將對平滑重啟的概念、應用場景以及實現方式進行詳細的介紹,幫助開發者理解如何在實際應用中實現平滑重啟,保障服務的高可用性。
定義
- 平滑重啟:平滑重啟是指在不中斷現有服務的前提下,使用新進程替代現有進程的操作。具體來說,平滑重啟包括以下步驟:
- 啟動新的進程(通常是同一個服務的升級版)。
- 新進程接管當前的請求和連接。
- 舊進程完成當前任務后,優雅地退出,避免未處理的請求丟失。
- 重啟信號:在類 Unix 操作系統中,信號是用來通知進程發生某些事件的一種機制。常用的重啟信號包括:
- SIGHUP:通常用于通知進程重新加載配置或進行平滑重啟。在很多應用中,SIGHUP 被用來觸發進程的平滑重啟。
- SIGTERM:表示請求程序終止進程,通常由操作系統或用戶發起,用于平滑關閉進程。
- SIGINT:通常是用戶在終端輸入
Ctrl+C時發送的信號,用于終止進程。
- **PID 文件:**PID 文件是用來存儲正在運行的進程的進程 ID(PID)的文件。在平滑重啟中,PID 文件非常重要,它允許新進程在啟動時找到并與舊進程進行交互。新進程通常會讀取 PID 文件,獲取舊進程的 PID,并通過發送信號來請求舊進程退出。
平滑重啟
示例代碼
package mainimport ("fmt""net/http""os""os/signal""syscall""github.com/cloudflare/tableflip"
)// 首頁 handler
func homeHandler(w http.ResponseWriter, r *http.Request) {w.Header().Set("Content-Type", "text/plain")fmt.Fprintln(w, "Welcome to the Home Page!")
}func main() {// 創建 tableflip 管理器upg, err := tableflip.New(tableflip.Options{PIDFile: "/Users/wepie/Downloads/testGin.pid"})if err != nil {fmt.Printf("Error creating tableflip manager: %v\n", err)os.Exit(1)}defer upg.Stop()// 捕獲 SIGHUP 信號并觸發升級go func() {sig := make(chan os.Signal, 1)signal.Notify(sig, syscall.SIGHUP)for range sig {if err := upg.Upgrade(); err != nil {fmt.Printf("Error during upgrade: %v\n", err)} else {fmt.Println("Upgrade triggered: New process started!")}}}()// 創建一個新的 ServeMux 來管理多個路由mux := http.NewServeMux()mux.HandleFunc("/", homeHandler) // 首頁// 監聽端口并啟動 HTTP 服務ln, err := upg.Listen("tcp", "localhost:8081")if err != nil {fmt.Printf("Error starting listener: %v\n", err)os.Exit(1)}defer ln.Close()// 啟動 HTTP 服務go func() {if err := http.Serve(ln, mux); err != nil {fmt.Printf("HTTP server error: %v\n", err)}}()// 等待進程準備好if err := upg.Ready(); err != nil {fmt.Printf("Error marking process as ready: %v\n", err)os.Exit(1)}fmt.Println("服務啟動完成 pid: ", os.Getpid())// 等待退出信號<-upg.Exit()fmt.Println("服務退出")
}
上述是一個簡單的平滑重啟的案例,有想試驗的同學可以直接用這段代碼實現。
在命令行 kill -SIGHUP 操作該進程(PID 可以通過定義的 PID 文件位置查閱),可以看到最終的平滑重啟
過程分析
創建 tableflip 管理器 (upg,upgrader)
upg, err := tableflip.New(tableflip.Options{PIDFile: "/Users/wepie/Downloads/testGin.pid"})
if err != nil {fmt.Printf("Error creating tableflip manager: %v\n", err)os.Exit(1)
}
defer upg.Stop()
做了什么?
- 創建了一個 tableflip 管理器 upg,它負責控制進程的平滑重啟。
- 使用 tableflip.Options 配置選項來指定 PID 文件,它會記錄當前進程的 PID,通常用于在后續重啟中找到進程。
- 如果 tableflip.New 返回錯誤,程序會打印錯誤信息并退出。
- 使用 defer 確保當程序退出時,調用 upg.Stop() 來停止管理器,釋放資源。
完成了什么?
- 管理器 upg 被創建并準備好,后續的重啟操作將通過它來進行。
信號捕獲和進程升級
go func() {sig := make(chan os.Signal, 1)signal.Notify(sig, syscall.SIGHUP)for range sig {if err := upg.Upgrade(); err != nil {fmt.Printf("Error during upgrade: %v\n", err)} else {fmt.Println("Upgrade triggered: New process started!")}}
}()
做了什么?
- 啟動了一個新的 goroutine,負責監聽 SIGHUP 信號(通常用于平滑重啟)。當該信號到達時,觸發進程的重啟。
- signal.Notify(sig, syscall.SIGHUP) 告訴程序捕獲 SIGHUP 信號,并將其放入 sig 通道。(任何信號都可以作為放入通道的內容,取決于怎么設計)
- 當 sig 通道接收到 SIGHUP 信號時,程序通過 upg.Upgrade() 來觸發進程重啟。
- Upgrade() 會啟動一個新的進程,并優雅地停止舊進程。Upgrade 方法會啟動一個新的進程,新進程會從程序的入口點(即 main() 函數)重新開始執行。
- 舊進程在調用 upg.Upgrade() 后并不會立即退出。它會繼續運行,處理現有的請求,直到新進程完全準備就緒
完成了什么?
- 新的進程會在接收到 SIGHUP 信號時被啟動,而舊進程會在新的進程啟動后退出,完成平滑重啟。
創建 HTTP 路由和處理函數
這里只是此處服務的示例,期間想加任何其他邏輯都是可行的
mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler) // 首頁
做了什么?
- 創建了一個新的 HTTP 路由器 mux,并注冊了 / 路徑的處理函數 homeHandler,它會響應根路徑的請求。
完成了什么?
- 創建了基本的 HTTP 路由和處理器,為后續的服務啟動做準備。
啟動 HTTP 服務并監聽端口
ln, err := upg.Listen("tcp", "localhost:8081")
if err != nil {fmt.Printf("Error starting listener: %v\n", err)os.Exit(1)
}
defer ln.Close()
做了什么?
- 使用 upg.Listen() 方法來啟動一個 TCP 監聽器,監聽 localhost:8081 端口。
- upg.Listen() 會創建一個 listener,并確保即使進程重啟,新的進程會繼續監聽該端口,確保平滑重啟后服務不中斷。
- 如果發生錯誤,程序會打印錯誤并退出。
- 使用 defer 來確保程序退出時關閉監聽器,避免資源泄漏。
完成了什么?
- 監聽 localhost:8081 端口,并準備好接收 HTTP 請求。
啟動 HTTP 服務的 goroutine
go func() {if err := http.Serve(ln, mux); err != nil {fmt.Printf("HTTP server error: %v\n", err)}
}()
做了什么?
- 在一個新的 goroutine 中啟動 HTTP 服務。http.Serve() 使用前面創建的 ln(TCP 監聽器)和 mux(路由器)來啟動 HTTP 服務。
- Serve() 會持續運行,直到出現錯誤或者服務器被停止。
完成了什么?
- 啟動了一個 HTTP 服務器,監聽
localhost:8081端口并處理請求。
等待新進程準備
if err := upg.Ready(); err != nil {fmt.Printf("Error marking process as ready: %v\n", err)os.Exit(1)
}
fmt.Println("服務啟動完成 pid: ", os.Getpid())
做了什么?
- 調用 upg.Ready(),告訴 tableflip 管理器進程已經準備好,可以開始接受請求。
- 如果 Ready() 返回錯誤,程序會打印錯誤并退出。
完成了什么?
- 程序向 tableflip 發出通知,表明服務已經啟動并準備好接收請求。此時會通知舊進程在完成當前其他任務后關閉。
老進程等待退出信號
<-upg.Exit()
fmt.Println("服務退出")
做了什么?
- upg.Exit() 返回一個只讀通道,<-upg.Exit() 會阻塞,直到進程退出信號到來。
- 進程會一直等待,直到 tableflip 通知進程可以退出(即新進程啟動并完成任務,會在 ready 后通過進程間通信找到舊進程的 PID,發送系統信號通知它退出)。
- 當接收到退出信號時,程序會繼續執行并打印 “服務退出”。
完成了什么?
- 進程阻塞在這里,等待退出信號。upg.Exit() 會在新進程啟動后通過 tableflip 觸發進程退出。
總結
平滑重啟適用于需要精確控制進程重啟時機、避免中斷服務的場景。相比容器化環境的重啟機制,平滑重啟提供了更高的控制性和靈活性。在 CICD 流程中,也是一個不錯的選擇。
引用
https://github.com/cloudflare/tableflip