【Golang】程式如何優雅的退出?
1. 背景
項目開發過程中,隨著需求的迭代,程式碼的發布會頻繁進行,在發布過程中,如何讓程式做到優雅的退出?
為什麼需要優雅的退出?
- 你的 http 服務,監聽埠沒有關閉,客戶的請求發過來了,但處理了一半,可能造成臟數據。
- 你的協程 worker 的一個任務運行了一半,程式退出了,結果不符合預期。
如下我們以 http 服務,gRPC 服務,單獨的 woker 協程為例子,一步步說明平滑關閉的寫法。
2. 常見的幾種平滑關閉
為了解決退出可能出現的潛在問題,平滑關閉一般做如下一些事情
- 關閉對外的監聽埠,拒絕新的連接
- 關閉非同步運行的協程
- 關閉依賴的資源
- 等待如上資源關閉
- 然後平滑關閉
2.1 http server 平滑關閉
原來的寫法
// startHttpServer start http server func startHttpServer() { mux := http.NewServeMux() // mux.Handle("/metrics", promhttp.Handler()) if err := http.ListenAndServe(":1608", mux); err != nil { log.Fatal("startHttpServer ListenAndServe error: " + err.Error()) } }
帶平滑關閉的寫法
// startHttpServer start http server func startHttpServer() { mux := http.NewServeMux() // mux.Handle("/metrics", promhttp.Handler()) srv := &http.Server{ Addr: ":1608", Handler: mux, } // 註冊平滑關閉,退出時會調用 srv.Shutdown(ctx) quit.GetQuitEvent().RegisterQuitCloser(srv) if err := srv.ListenAndServe(); err != nil { log.Fatal("startHttpServer ListenAndServe error: " + err.Error()) } }
把平滑關閉註冊到http.Server的關閉函數中
// startHttpServer start http server func startHttpServer() { mux := http.NewServeMux() // mux.Handle("/metrics", promhttp.Handler()) srv := &http.Server{ Addr: ":1608", Handler: mux, } // 把平滑退出註冊到http.Server中 srv.RegisterOnShutdown(quit.GetQuitEvent().GracefulStop) if err := srv.ListenAndServe(); err != nil { log.Fatal("startHttpServer ListenAndServe error: " + err.Error()) } }
2.2 gRPC server 平滑關閉
原來的寫法
// startGrpcServer start grpc server func startGrpcServer() { listen, err := net.Listen("tcp", "0.0.0.0:9999") if err != nil { log.Fatalf("Failed to listen: %v", err) return } grpcServer := grpc.NewServer() // helloBoot.GrpcRegister(grpcServer) go grpcServer.Serve(listen) defer grpcServer.GracefulStop() // ... }
帶平滑關閉的寫法
// startGrpcServer start grpc server func startGrpcServer() { listen, err := net.Listen("tcp", "0.0.0.0:9999") if err != nil { log.Fatalf("Failed to listen: %v", err) return } grpcServer := grpc.NewServer() // helloBoot.GrpcRegister(grpcServer) go grpcServer.Serve(listen) // 把 grpc 的GracefulStop註冊到退出事件中 quit.GetQuitEvent().RegisterStopFunc(grpcServer.GracefulStop) quit.WaitSignal() }
2.3 worker 協程平滑關閉
單獨的協程啟停,可以通過計數的方式註冊到退出事件處理器中。
- 啟動協程 增加計數
- quit.GetQuitEvent().AddGoroutine()
- 停止協程 減計數
- quit.GetQuitEvent().DoneGoroutine()
- 常駐後台運行的協程退出的條件改成退出事件是否結束的條件
- !quit.GetQuitEvent().HasFired()
- 常駐後台運行的協程若通過 select 處理 chan,同時增加退出事件的chan
- case <-quit.GetQuitEvent().Done()
// myWorker my worker type myWorker struct { } // RunWorkerWithChan run Goroutine worker func (m *myWorker) RunWorkerWithChan() { // 啟動一個Goroutine時,增加Goroutine數 quit.GetQuitEvent().AddGoroutine() defer func() { // 一個Goroutine退出時,減少Goroutine數 quit.GetQuitEvent().DoneGoroutine() }() // 退出時,此次退出 for !quit.GetQuitEvent().HasFired() { select { // 退出時,收到退出訊號 case <-quit.GetQuitEvent().Done(): break //case msg := <- m.YouChan: // handle msg } } } // RunWorker run Goroutine worker func (m *myWorker) RunWorker() { // 啟動一個Goroutine時,增加Goroutine數 quit.GetQuitEvent().AddGoroutine() defer func() { // 一個Goroutine退出時,減少Goroutine數 quit.GetQuitEvent().DoneGoroutine() }() // 退出時,此次退出 for !quit.GetQuitEvent().HasFired() { // ... } }
2.4 實現 io.Closer 介面的自定義服務平滑關閉
實現 io.Closer 介面的結構體,增加到退出事件處理器中
// startMyService start my service func startMyService() { srv := NewMyService() // 註冊平滑關閉,退出時會調用 srv.Close() quit.GetQuitEvent().RegisterCloser(srv) srv.Run() } // myService my service type myService struct { isStop bool } // NewMyService new func NewMyService() *myService { return &myService{} } // Close my service func (m *myService) Close() error { m.isStop = true return nil } // Run my service func (m *myService) Run() { for !m.isStop { // .... } }
2.5 集成其他框架怎麼做
退出訊號處理由某一框架接管,尋找框架如何註冊退出函數,優秀的框架一般都會實現安全實現退出的機制。
如下將退出事件註冊到某一框架的平滑關閉函數中
func startMyServer() { // ... // xxx框架退出函數註冊退出事件 xxx.RegisterQuitter(func() { quit.GetQuitEvent().GracefulStop() }) }
參考:
//github.com/mygityf/go-library/blob/main/quit/quit.go
完。
祝玩的開心~