一個示例闡述 Go 應用的優雅中止
寫在前面
按照一般的設計原則, 每個 HTTP 請求都是無狀態的,因此大多情況下 Web 應用都很容易做水平擴充套件。“無狀態”也意味著 HTTP 請求發起重試的成本是很低的,從而使得 Web 介面的開發很少關注優雅中止(一部分也因為 Web 框架做了這部分的考慮)。
不過,業務中 ① 總會存在對中止 比較敏感的介面(比如支付相關),並且 ② 總會存在一些帶狀態的服務,此時優雅中止就顯得比較重要了。
本文通過一個Go 定時任務示例 來簡單介紹 Go 技術棧中優雅中止的處理思路。
適用人群
入門——初級√ ——中級——高階;本文適應初級及以上。
程式碼級支援優雅中止是必要的
優雅中止的含義
所謂“優雅中止”,是指應用接收到特定的中止訊號(比如INT
、TERM
)後,不再接受外部的新請求,也不再建立內部的新任務,保持應用程序執行直到舊需求和舊任務執行完成後再終止退出。
Kubernetes 中 Pod 的終止機制
作為高可靠的服務平臺,k8s 定義了終止Pod
(業務程序在 Pod 中執行)的基本步驟:當主動刪除 pod 時,系統會在強制終止 Pod 之前將TERM
訊號傳送到每個容器中的主程序,過一段時間後(預設為 30 秒),再把KILL
訊號傳送到這些程序。除此之外, k8s 還通過鉤子方法提供了對容器生命週期
的管理能力,允許使用者通過自定義的方式配置容器啟動後或終止前執行的操作。
當打包進映象的應用執行在 k8s 中的時候,如果應用實現了優雅中止的機制,就可以充分利用上面提到的 k8s 的能力,在升級應用(發新版本)和管理 Pod (宿主機維護時把 Pod 漂移到另一個宿主機,或者在閒時動態地收縮 Pod 數量從而把資源省出來另作他用)的過程中實現服務的零中斷。
優雅中止的 Go 程式碼示例
下面的程式碼定義了兩個定時任務:mySecondJobs
每秒鐘會觸發一次,每次持續約 1 秒鐘;myMinuteJobs
每分鐘會觸發一次,每次持續約 2 秒鐘。具體地可以閱讀下面的程式碼(可以直接複製下面的程式碼到自己的環境中執行):
package main import ( "fmt" "os" "os/signal" "syscall" "time" ) func main() { c := make(chan os.Signal) // Go 不允許監聽 SIGKILL/SIGSTOP 訊號 // 參考 https://github.com/golang/go/issues/9463 signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) second := time.NewTicker(time.Second) minute := time.NewTicker(time.Minute) A: // 由於 for-select 巢狀使用,設定跳出 for 迴圈的標記 for { select { case s := <-c: // 收到 SIGTERM/SIGINT 訊號,跳出 for 迴圈結束程序 fmt.Printf("get signal %s, graceful ending...\n", s) break A case <-second.C: go mySecondJobs() case <-minute.C: go myMinuteJobs() } } fmt.Println("graceful ending") // 做一些操作讓非同步任務正常結束,這裡偷懶地採取簡單等待的方式 :laughing: time.Sleep(time.Second * 10) fmt.Println("graceful ended.") } func mySecondJobs() { tS := time.Now().String() fmt.Printf("starting second job: %s \n", tS) time.Sleep(time.Second * 1) // 假設每個任務消耗 1 秒時間 fmt.Printf("second job %s are done. \n", tS) } func myMinuteJobs() { tS := time.Now().String() fmt.Printf("starting minute job: %s \n", tS) time.Sleep(time.Second * 2) // 假設每個任務消耗 2 秒時間 fmt.Printf("minute job %s are done. \n", tS) }
原始碼解讀-優雅中止的處理思路
signal.Notify for + select for
原始碼解讀-值得關注的幾個點
-
程式碼中採用了
go mySecondJobs()
和go myMinuteJobs()
非同步任務的方式;如果採用同步的方式將無法捕獲訊號,因為此時主執行緒在處理業務邏輯,沒有空閒處理訊號捕獲邏輯。 - 原始碼中偷懶地採取簡單等待的方式來保證非同步任務正常結束,非普適方法,實際開發中需要根據情況做定製。
-
time.Ticker
的使用是有注意事項的,當select
語句中同一時刻有多個分支滿足條件時會隨機取一個執行,從而導致資訊丟失(參考文獻中最後一篇有講到),不過本文的程式碼不會觸發這個問題,大家可以思考一下原因。
小結
預設情況下,Go 應用在接收到TERM
訊號後直接退出主程序,如果此時有過程沒處理完(比如 接收到外部請求後尚未返回響應,或者內部的非同步任務尚未結束),則會導致過程的異常中斷,影響服務質量。通過在程式碼中顯式地捕獲TERM
訊號及其他訊號,感知作業系統對程序的處理,可以主動採取措施優雅地結束應用程序。
隨著 k8s 的普及,考慮到其對程序生命週期的規範化管理,應用支援程式碼級的優雅中止 (尤其是容器化的應用)有必要成為一種開發規範,值得引起每一位開發者的注意。
參考
- 訊號(LINUX訊號機制)_百度百科 介紹 Linux 中的訊號(比如 SIGINT、SIGTERM 等)
- Pods - Kubernetes Kubernetes 官網對 pod 的介紹,包含對 pod 生命週期的介紹
- Container Lifecycle Hooks - Kubernetes Kubernetes 官方對容器生命週期中的鉤子的介紹
- 優雅地關閉kubernetes中的nginx - 簡書 介紹了訊號、 k8s 中 pod 的終止流程,以及 nginx 的優雅終止
- golang訊號signal的處理 - 快樂程式設計
- Golang的 signal - 蟈蟈俊 - 部落格園
- os/signal: Prevent developers from catching SIGKILL and SIGSTOP · Issue #9463 · golang/go · GitHub Go 中不允許捕獲 SIGKILL 和 SIGSTOP 訊號
- go裡面select-case和time.Ticker的使用注意事項 - CSDN部落格 雖然本文的示例不會觸發,不過 time.Ticker 使用時還是要注意一下這個小坑
- how to get a graceful shutdown of an server. · Issue #1329 · gin-gonic/gin · GitHub Go 的 Web 框架 Gin 對優雅中止的支援示例