優雅管理Go Project生命周期

寫在前面 

最近和幾個小夥伴們在寫字節跳動第五屆青訓營後端組的大作業。

雖然昨天已經提交瞭項目,但有很多地方值得總結一下,比如這一篇,來看看我們是如何管理應用的生命周期的。

  • 項目地址
  • 項目文檔

一、什麼時候要註意管理應用的生命周期?

先來看一段代碼:(假設無 err 值)

func main() {
    // 1、啟動HTTP服務
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello, World!")
	})
	http.ListenAndServe(":8080", nil)
    // 2、啟動GRPC服務
    server := grpc.NewServer()
    listener, _ := net.Listen("tcp", ":1234")
	server.Serve(listener)    
}

這一段代碼,相信你一眼就能看出問題,因為在啟動HTTP後,進程會堵塞住,下面啟動GRPC服務的代碼,壓根就不會執行。

但是,如果想要同時啟動GRPC服務呢?該怎麼做呢?

自己沒有時間,那麼就請一個幫手咯,讓它來為我們啟動GRPC服務,而這個幫手,就是go的攜程。

  • 來看一段偽代碼,也就是調整成這樣,
func main() {
    // 1、將HTTP服務放在後臺啟動
    go start http
    // 2、將GRPC服務放在前臺啟動
    start grpc  
}

但是調整成這樣之後,理想的情況就是,HTTP成功啟動後、GRPC也要啟動成功。HTTP意外退出後,GRPC也需要退出服務,他們倆需要共存亡。

但若出現瞭 HTTP 意外退出、GRPC還未退出,那麼就會浪費資源。還可能出現其他的問題。比如接口異常。這樣會很危險。那我們該利用什麼方式,讓同一服務內,啟動多個線程。並且讓他們共同存亡的呢?

瞭解瞭上面的問題,我們再來重新描述總結一下出現的問題。

一個服務,可能會啟動多個進程,比如說 HTTP API、GRPC API、服務的註冊,這些模塊都是獨立的,都是需要在程序啟動的時候進行啟動。

而且如果需要關閉掉這個應用,還需要處理很多關閉的問題。比如說

  • HTTP、GRPC 的優雅關閉
  • 關閉數據庫鏈接
  • 完成註冊中心的註銷操作

而且,啟動的多個進程間,該如何通信呢? 某些服務意外退出瞭,按理來說要關閉整個應用,該如何監聽到呢?

二、我們是如何做的

(1)利用面向對象的方式來管理應用的生命周期

定義一個管理者對象,來管理我們應用所需要啟動的所有服務,比如這裡需要被我們啟動的服務有:HTTP、GRPC

這個管理者核心有兩個方法:start、stop

// 用於管理服務的開啟、和關閉
type manager struct {
	http *protocol.HttpService // HTTP生命周期的結構體[自定義]
	grpc *protocol.GRPCService // GRPC生命周期的結構體[自定義]
	l    logger.Logger		   // 日志對象
}

不用關心這裡依賴的 http、grpc結構體是什麼,我們在後面的章節,會詳細解釋。隻需要知道,我們用manager這個結構體,用於管理http、grpc服務即可。

(2)處理start

start這個函數,核心隻做瞭兩件事,分別啟動HTTP、GRPC服務。

func (m *manager) start() error {
	// 打印加載好的服務
	m.l.Infof("已加載的 [Internal] 服務: %s", ioc.ExistingInternalDependencies())
	m.l.Infof("已加載的 [GRPC] 服務: %s", ioc.ExistingGrpcDependencies())
	m.l.Infof("已加載的 [HTTP] 服務: %s", ioc.ExistingGinDependencies())
	// 如果不需要啟動HTTP服務,需要才啟動HTTP服務
	if m.http != nil {
		// 將HTTP放在後臺跑
		go func() {
			// 註:這屬於正常關閉:"http: Server closed"
			if err := m.http.Start(); err != nil && err.Error() != "http: Server closed" {
				return
			}
		}()
	}
    // 將GRPC放入前臺啟動
	m.grpc.Start()
	return nil
}

又因為開頭說過瞭,啟動這兩任一服務,都會將進程堵塞住。

所以我們找瞭一個幫手(攜程)來啟動HTTP服務,然後將GRPC服務放在前臺運行。

那為什麼我要將GRPC服務放在前臺運行呢?其實理論上放誰都行,但由於我們的架構原因。我們有的服務不需要啟動HTTP服務,而每一個服務都會啟動GRPC服務。所以,將GRPC放置在前臺,會更合適。

至於裡面如何使用HTTP、GRPC的服務對象啟動它們的服務。在這一節就不多贅述瞭。在之後的章節會有詳細的介紹~

看完瞭統一管理啟動的start方法,那我們來看看如何停止服務吧

(3)處理stop

1、什麼時候才去Stop?

我們開啟瞭多個服務,並且有的還是放在後臺運行的。這就涉及到瞭多個攜程的間通信的問題瞭

用什麼來通信吶?我怎麼知道HTTP服務掛沒掛?是意外掛的還是主動掛的?我們怎麼能夠優雅的統一關閉所有服務呢?

其實這一切的問題,Go都為我們想好瞭:那就是使用Channels。一個channel是一個通信機制,它可以讓一個攜程通過它給另一個攜程發送值信息。每個channel都有一個特殊的類型,也就是channels可發送數據的類型。

我們把一個go程當作一個人的化,那麼main 方法啟動的主go程就是你自己。在你的程序中使用到的其他go程,都是你的好幫手,你的好朋友,它們有給你去處理耗時邏輯的、有給你去執行業務無關的切面邏輯的。而且是你的好幫手,按理來說最好是由你自己去決定,要不要請一個好幫手。

當你請來瞭一個好幫手後,它們會在你的背後為你做你讓他們做的事情。那麼多個人之間的通信,比較現代的方法,那可以是:打個電話?發個消息?所以用到瞭一個溝通的信道:Channel

好瞭,當你瞭解瞭這些後,也就是接收到一些電話後,我們才需要去stop。我們再回到Dousheng使用的情景:

2、Dousheng的應用場景

主攜程是GRPC服務這個人,我們請瞭一個幫手,給我啟動HTTP服務。這個時候,如果HTTP服務這個幫手意外出事瞭。既然是幫我麼你做事,那我們肯定得對別人負責是吧。但是我們也不知道它出不出意外啊,怎麼辦呢?這時候你想瞭兩個方法:

  • 跟你的幫手HTTP,發瞭如下消息

這就需要HTTP自己告訴我們,按理來說,應該是可以的。但是如果HTTP遇到瞭重大問題,根本來不及告訴我們呢?咱們又是一個負責的男人。為瞭避免這種情況發生,又請一個人,專門給我們看HTTP有沒有遇到重大問題。於是有瞭第二種方式:

  • 在請一個幫手signal.Notify,幫助我們監聽HTTP可能會遇到的重大問題

當我們收到HTTP出事的信號後,那我們就可以統一的去優雅關閉服務瞭。就這樣,我們做瞭一個負責的人~

相信你已經瞭解瞭核心的思想,我們來看看,用代碼該如何實現

3、代碼實現

  • 啟動signal.Notify,用於監聽系統信號

我們已經分析過瞭,我們需要再請一個幫手,來給我們處理HTTP可能會遇到的重大事故:(syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP, syscall.SIGINT)

// WaitSign 等待退出的信號,實現優雅退出
func (m *manager) waitSign() {
   // 用於接收信號的信道
   ch := make(chan os.Signal, 1)
   // 接收這幾種信號
   signal.Notify(ch, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP, syscall.SIGINT)
   // 需要在後臺等待關閉
   go m.waitStop(ch)
}

signal.Notify收到上面所列舉的信號後,那麼就可以去做關閉的事情瞭,那如何關閉呢?

  • 讀取信號,執行優雅關閉邏輯
// WaitStop 中斷信號,比如Terminal [關閉服務的方法]
func (m *manager) waitStop(ch <-chan os.Signal) {
   // 等待信號,若收到瞭,我們進行服務統一的關閉
   for v := range ch {
      switch v {
      default:
         m.l.Infof("接受到信號:%s", v)
         // 優雅關閉HTTP服務
         if m.http != nil {
            if err := m.http.Stop(); err != nil {
               m.l.Errorf("優雅關閉 [HTTP] 服務出錯:%s", err.Error())
            }
         }
		// 優雅關閉GRPC服務
         if err := m.grpc.Stop(); err != nil {
            m.l.Errorf("優雅關閉 [GRPC] 服務出錯:%s", err.Error())
         }
      }
   }
}

這裡的邏輯比較簡單,就是當接收到信號的時候,對HTTP、GRPC做優雅關閉的邏輯。至於為什麼要進行優雅關閉,而不是直接os.Exit()?我們在下一節講~

這裡值得一提的是,我們從chanel裡獲取數據,因為我們這裡隻和單個攜程間進行通信瞭,使用的是 for range,並沒有使用for select

好瞭,這樣我們應用的生命周期算是被我們優雅的拿捏瞭。我們一直在講優雅關閉這個詞,我們來解釋一下什麼是優雅關閉?為什麼需要優雅關閉?

三、什麼是優雅關閉

既然HTTP服務和GRPC服務都需要優雅關閉,我們這裡用HTTP服務來舉例。

先來看這張圖,假設有三個並行的請求至我們的HTTP服務。它們都期望得到服務器的response。HTTP服務器正常運行的情況下,多半是沒問題的。

請求已發出,若提供的HTTP服務突然異常關閉瞭呢?我們繼續來把HTTP服務比作一個人。看看它是否優雅呢?

(1)沒有優雅關閉

如果HTTP這個人不太優雅,是一個做事不怎麼負責的渣男。當自己異常over瞭之後,也不解決完自己的事情,就讓別人(request),找不到資源瞭。真的很不負責啊。

大致用一幅圖表示:

這個不優雅的HTTP服務,當有還未處理的請求時,自己就異常關閉瞭,那麼它根本不會理會原先的請求是否完成瞭。它隻管自己退出程序。

(2)有瞭優雅關閉

看完瞭那個渣男HTTP(沒有優雅關閉),我們簡直想罵它瞭。那我們來看,當一個優雅的謙謙君子(有優雅關閉),又是如何看待這個問題的。

這是一個負責人的人,為什麼說他負責人、說它優雅呢?因為當它自己接收到異常關閉的信號後。它不會隻顧自己關閉。它大概還會做兩件事:

  • 關閉建立連接的請求通道,防止還會接收到新的請求
  • 處理完以請求的,但是還未響應的請求。保證資源得到響應,哪怕是錯誤的response

正是因為它主要做瞭這兩件事,我們才說此時的HTTP服務,是一個優雅的謙謙君子。

而當有很多個請求到時候,我們怎麼知道是否會不會突然異常關閉呢?如果遇到瞭這種情況,我們應該處理完未完成的響應,拒絕新的請求建立連接,因為我們是一個優雅的人。

以上就是優雅管理Go Project生命周期的詳細內容,更多關於Go Project生命周期的資料請關註WalkonNet其它相關文章!

推薦閱讀: