golang中的优雅中止
簡介
按照一般的設計原則, 每個 HTTP 請求都是無狀態的,因此大多情況下 Web 應用都很容易做水平擴展。“無狀態”也意味著 HTTP 請求發起重試的成本是很低的,從而使得 Web 接口的開發很少關注優雅中止(一部分也因為 Web 框架做了這部分的考慮)。
不過,業務中 ① 總會存在對中止比較敏感的接口(比如支付相關),并且 ② 總會存在一些帶狀態的服務,此時優雅中止就顯得比較重要了。
本文通過一個Go 定時任務示例來簡單介紹 Go 技術棧中優雅中止的處理思路。
k8s中pod的終止機制
作為高可靠的服務平臺,k8s 定義了終止 Pod (業務進程在 Pod 中運行)的基本步驟:當主動刪除 pod 時,系統會在強制終止 Pod 之前將 TERM 信號發送到每個容器中的主進程,過一段時間后(默認為 30 秒),再把 KILL 信號發送到這些進程。除此之外, k8s 還通過鉤子方法提供了對 容器生命周期 的管理能力,允許用戶通過自定義的方式配置容器啟動后或終止前執行的操作。
當打包進鏡像的應用運行在 k8s 中的時候,如果應用實現了優雅中止的機制,就可以充分利用上面提到的 k8s 的能力,在升級應用(發新版本)和管理 Pod (宿主機維護時把 Pod 漂移到另一個宿主機,或者在閑時動態地收縮 Pod 數量從而把資源省出來另作他用)的過程中實現服務的零中斷。
優雅中止的 Go 代碼示例
下面的代碼定義了兩個定時任務:mySecondJobs 每秒鐘會觸發一次,每次持續約 1 秒鐘;myMinuteJobs 每分鐘會觸發一次,每次持續約 2 秒鐘。具體地可以閱讀下面的代碼(可以直接復制下面的代碼到自己的環境中運行):
package mainimport ("fmt""os""os/signal""syscall""time" )func main() {c := make(chan os.Signal)// Go 不允許監聽 SIGKILL/SIGSTOP 信號// 參考 https://github.com/golang/go/issues/9463signal.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 Acase <-second.C:go mySecondJobs()case <-minute.C:go myMinuteJobs()}}fmt.Println("graceful ending")// 做一些操作讓異步任務正常結束,這里偷懶地采取簡單等待的方式 😆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 語句中同一時刻有多個分支滿足條件時會隨機取一個執行,從而導致信息丟失,不過本文的代碼不會觸發這個問題,大家可以思考一下原因。
http的shutdown
如何優雅的關閉http服務在Go Web開發中一直被提及和討論的話題,今天Go 1.8的發布終于為我們帶來了這個特性。
文檔中是這樣介紹的:
func (srv *Server) Shutdown(ctx context.Context) errorShutdown將無中斷的關閉正在活躍的連接,然后平滑的停止服務。處理流程如下:
-
首先關閉所有的監聽
-
然后關閉所有的空閑連接
-
然后無限期等待連接處理完畢轉為空閑,并關閉
-
如果提供了 帶有超時的Context,將在服務關閉前返回Context的超時錯誤
需要注意的是,Shutdown并不嘗試關閉或者等待hijacked連接,如WebSockets。如果需要的話調用者需要分別處理諸如長連接類型的等待和關閉。
其實,你只要調用Shutdown方法就好了。
// main.gopackage mainimport ("fmt""log""net/http""os""os/signal""syscall""time" )func main() {http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello World, %v\n", time.Now())})s := &http.Server{Addr: ":8080",Handler: http.DefaultServeMux,ReadTimeout: 10 * time.Second,WriteTimeout: 10 * time.Second,MaxHeaderBytes: 1 << 20,}go func() {log.Println(s.ListenAndServe())log.Println("server shutdown")}()// Handle SIGINT and SIGTERM.ch := make(chan os.Signal)signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)log.Println(<-ch)// Stop the service gracefully.log.Println(s.Shutdown(nil))// Wait gorotine print shutdown messagetime.Sleep(time.Second * 5)log.Println("done.") }運行程序:
go run main.go然后ctrl + c終止該程序,會打印出如下信息:
2017/02/17 11:36:28 interrupt 2017/02/17 11:36:28 <nil> 2017/02/17 11:36:28 http: Server closed 2017/02/17 11:36:28 server shutdown可以看到,服務被正確關閉了。
在沒有Shutdown方法之前,ctrl + c就硬生生的終止了,然后就沒有然后了
小結
默認情況下,Go 應用在接收到 TERM 信號后直接退出主進程,如果此時有過程沒處理完(比如 接收到外部請求后尚未返回響應,或者內部的異步任務尚未結束),則會導致過程的異常中斷,影響服務質量。通過在代碼中顯式地捕獲 TERM 信號及其他信號,感知操作系統對進程的處理,可以主動采取措施優雅地結束應用進程。
隨著 k8s 的普及,考慮到其對進程生命周期的規范化管理,應用支持代碼級的優雅中止(尤其是容器化的應用)有必要成為一種開發規范,值得引起每一位開發者的注意
總結
以上是生活随笔為你收集整理的golang中的优雅中止的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: golang中的httptest
- 下一篇: golang中的web服务平滑重启