日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

go實現服務優雅關閉的示例_Golang

作者:亞洲第一中鋒_哈達迪 ? 更新時間: 2023-04-18 編程語言

為什么需要優雅關閉

什么叫優雅關閉?先說不優雅關閉,就是什么都不管,強制關閉進程,這會導致有些正在處理中的請求被強行中斷

這樣做有什么問題?

  • 用戶本次請求會失敗,降低用戶體驗
  • 沒有事務的數據庫操作,會產生部分成功的問題,破壞原子性
  • 某些緩服務需要定期將本地緩存刷到遠程db,強制關閉會導致數據丟失

優雅關閉的核心是以下功能:

  • 如何監聽退出信號
  • 如何拒絕新請求
  • 如何等待進行中的請求處理完畢

監控服務退出信號

在go中使用下面的代碼監聽退出信號,如果c返回,說明監聽到信號

不同的操作系統監聽不同的退出信號

c := make(chan os.Signal, 1)
signals := []os.Signal{
   syscall.SIGINT,
   syscall.SIGTERM,
   syscall.SIGQUIT,
}

signal.Notify(c, signals...)
<-c

拒絕新請求

go在1.8后增加了shutdown方法來,我們看看它怎么實現優雅關閉:

srv.inShutdown.setTrue()
lnerr := srv.closeListenersLocked()
srv.closeDoneChanLocked()
  • 設置inShutdown標志位
  • 關閉所有的listener
  • 關閉doneChan

這一段對應到http服務接收請求的流程:

for {
   rw, err := l.Accept()
   if err != nil {
      select {
      case <-srv.getDoneChan():
         return ErrServerClosed
   // ...       
}

一旦關閉listener,關閉doneChan后,http服務就不會再接收新的請求,直接返回

執行關閉之前的回調

for _, f := range srv.onShutdown {
   go f()
}

這里的回調實現得比較粗糙:

  • 沒有優先級的概念,所有回調并發執行,因此需要保證回調之間沒有依賴
  • 雖然回調不適合長時間運行,但Go http沒有提供機制來保證這些回調一定能執行完畢,若想做到這點需要自己處理

等待處理中的請求執行完畢

設置標識位可以拒絕新的請求,但依舊在執行的請求還在處理中,需要等這些請求執行完畢

等待處理中的請求執行完畢有兩種思路:

  • 等待一段固定的時間
  • 實時維護請求的計數

go選擇了兩種方式結合的模式,通過ctx設置一個最大的等待時間,同時不斷輪詢正在請求中的計數

ctx超時或者計數變為0,都會返回

timer := time.NewTimer(nextPollInterval())
defer timer.Stop()
for {
   if srv.closeIdleConns() && srv.numListeners() == 0 {
      return lnerr
   }
   select {
   case <-ctx.Done():
      return ctx.Err()
   case <-timer.C:
      timer.Reset(nextPollInterval())
   }
}

這里每隔一定時間檢查已有請求是否執行完畢,如果執行完畢,或者外部通過ctx設置的超時到期就會返回

檢查間隔是多少?

  • 從1ms開始,每輪檢查后倍增,最大500ms

怎么判斷是否執行完畢?

  • 所有的連接都關閉
  • 所有的listener都關閉

服務收到監聽信號返回之前,關閉連接和listener,會被這里檢查到

在這里插入圖片描述

實戰

func main() {
   // 注冊路由
   http.Handle("/aaa", http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {
      time.Sleep(time.Second * 10)
      fmt.Println(111)
   }))
   server := http.Server{
      Addr:    "localhost:8080",
      Handler: http.DefaultServeMux,
   }

   close := make(chan int)
   go func() {
      quit := make(chan os.Signal)
      signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
      <-quit

      ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
      defer cancel()
      err := server.Shutdown(ctx)
      log.Print(err)
      
      // 控制外層退出
      close <- 1
   }()

   err := server.ListenAndServe()
   fmt.Println(err)

   <-close
}

該代碼做了下面的事:

  • 注冊一個10s才返回的路由處理函數
  • 開子協程監聽OS的退出信號,如果監聽到了開始進行優雅關閉,雖多等待30s
  • 主協程調用 server.ListenAndServe(),開始監聽請求

需要注意的是,一定要在子協程中優雅關閉結束后,主協程才能退出,這里用channel控制

因為主協程發現doneChan被關閉時會馬上返回,但此時主協程開的業務處理協程還在進行中,如果主協程此時退出,無法達到優雅關閉的效果

按照以下流程測試:

  • 啟動 Web 服務
  • 在瀏覽器請求http://localhost:8080/aaa
  • 過5秒后在控制臺按下ctrl+c
  • 觀察控制臺程序是否不會立刻結束,而是在 10s 后結束

支持強制退出

既然有優雅退出,那就有強制退出,我們假設如果按下兩次ctrl+c,代表用戶希望服務強制退出:

close := make(chan int, 2)
go func() {
   quit := make(chan os.Signal)
   signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
   <-quit
   go func() {
      <-quit
      os.Exit(1)
   }()

   // ...
}()

做法很簡單,收到第一個退出信號后,再開一個子協程,如果再收到退出信號,就調用os.Exit退出進程

并且close channel的容量需要為2,避免當兩次退出信號過短時丟失信號

原文鏈接:https://blog.csdn.net/qq_39383767/article/details/128886199

欄目分類
最近更新