【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

 

完。

祝玩的開心~

Tags: