優(yōu)雅管理Go?Project生命周期
寫在前面
最近和幾個(gè)小伙伴們?cè)趯懽止?jié)跳動(dòng)第五屆青訓(xùn)營(yíng)后端組的大作業(yè)。
雖然昨天已經(jīng)提交了項(xiàng)目,但有很多地方值得總結(jié)一下,比如這一篇,來(lái)看看我們是如何管理應(yīng)用的生命周期的。
一、什么時(shí)候要注意管理應(yīng)用的生命周期?
先來(lái)看一段代碼:(假設(shè)無(wú) err 值)
func main() {
// 1、啟動(dòng)HTTP服務(wù)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
})
http.ListenAndServe(":8080", nil)
// 2、啟動(dòng)GRPC服務(wù)
server := grpc.NewServer()
listener, _ := net.Listen("tcp", ":1234")
server.Serve(listener)
}
這一段代碼,相信你一眼就能看出問(wèn)題,因?yàn)樵趩?dòng)HTTP后,進(jìn)程會(huì)堵塞住,下面啟動(dòng)GRPC服務(wù)的代碼,壓根就不會(huì)執(zhí)行。
但是,如果想要同時(shí)啟動(dòng)GRPC服務(wù)呢?該怎么做呢?
自己沒(méi)有時(shí)間,那么就請(qǐng)一個(gè)幫手咯,讓它來(lái)為我們啟動(dòng)GRPC服務(wù),而這個(gè)幫手,就是go的攜程。
- 來(lái)看一段偽代碼,也就是調(diào)整成這樣,
func main() {
// 1、將HTTP服務(wù)放在后臺(tái)啟動(dòng)
go start http
// 2、將GRPC服務(wù)放在前臺(tái)啟動(dòng)
start grpc
}
但是調(diào)整成這樣之后,理想的情況就是,HTTP成功啟動(dòng)后、GRPC也要啟動(dòng)成功。HTTP意外退出后,GRPC也需要退出服務(wù),他們倆需要共存亡。
但若出現(xiàn)了 HTTP 意外退出、GRPC還未退出,那么就會(huì)浪費(fèi)資源。還可能出現(xiàn)其他的問(wèn)題。比如接口異常。這樣會(huì)很危險(xiǎn)。那我們?cè)摾檬裁捶绞?,讓同一服?wù)內(nèi),啟動(dòng)多個(gè)線程。并且讓他們共同存亡的呢?
了解了上面的問(wèn)題,我們?cè)賮?lái)重新描述總結(jié)一下出現(xiàn)的問(wèn)題。
一個(gè)服務(wù),可能會(huì)啟動(dòng)多個(gè)進(jìn)程,比如說(shuō) HTTP API、GRPC API、服務(wù)的注冊(cè),這些模塊都是獨(dú)立的,都是需要在程序啟動(dòng)的時(shí)候進(jìn)行啟動(dòng)。
而且如果需要關(guān)閉掉這個(gè)應(yīng)用,還需要處理很多關(guān)閉的問(wèn)題。比如說(shuō)
- HTTP、GRPC 的優(yōu)雅關(guān)閉
- 關(guān)閉數(shù)據(jù)庫(kù)鏈接
- 完成注冊(cè)中心的注銷操作
- ...
而且,啟動(dòng)的多個(gè)進(jìn)程間,該如何通信呢? 某些服務(wù)意外退出了,按理來(lái)說(shuō)要關(guān)閉整個(gè)應(yīng)用,該如何監(jiān)聽(tīng)到呢?
二、我們是如何做的
(1)利用面向?qū)ο蟮姆绞絹?lái)管理應(yīng)用的生命周期
定義一個(gè)管理者對(duì)象,來(lái)管理我們應(yīng)用所需要啟動(dòng)的所有服務(wù),比如這里需要被我們啟動(dòng)的服務(wù)有:HTTP、GRPC
這個(gè)管理者核心有兩個(gè)方法:start、stop
// 用于管理服務(wù)的開(kāi)啟、和關(guān)閉
type manager struct {
http *protocol.HttpService // HTTP生命周期的結(jié)構(gòu)體[自定義]
grpc *protocol.GRPCService // GRPC生命周期的結(jié)構(gòu)體[自定義]
l logger.Logger // 日志對(duì)象
}
不用關(guān)心這里依賴的 http、grpc結(jié)構(gòu)體是什么,我們?cè)诤竺娴恼鹿?jié),會(huì)詳細(xì)解釋。只需要知道,我們用manager這個(gè)結(jié)構(gòu)體,用于管理http、grpc服務(wù)即可。
(2)處理start
start這個(gè)函數(shù),核心只做了兩件事,分別啟動(dòng)HTTP、GRPC服務(wù)。
func (m *manager) start() error {
// 打印加載好的服務(wù)
m.l.Infof("已加載的 [Internal] 服務(wù): %s", ioc.ExistingInternalDependencies())
m.l.Infof("已加載的 [GRPC] 服務(wù): %s", ioc.ExistingGrpcDependencies())
m.l.Infof("已加載的 [HTTP] 服務(wù): %s", ioc.ExistingGinDependencies())
// 如果不需要啟動(dòng)HTTP服務(wù),需要才啟動(dòng)HTTP服務(wù)
if m.http != nil {
// 將HTTP放在后臺(tái)跑
go func() {
// 注:這屬于正常關(guān)閉:"http: Server closed"
if err := m.http.Start(); err != nil && err.Error() != "http: Server closed" {
return
}
}()
}
// 將GRPC放入前臺(tái)啟動(dòng)
m.grpc.Start()
return nil
}
又因?yàn)殚_(kāi)頭說(shuō)過(guò)了,啟動(dòng)這兩任一服務(wù),都會(huì)將進(jìn)程堵塞住。
所以我們找了一個(gè)幫手(攜程)來(lái)啟動(dòng)HTTP服務(wù),然后將GRPC服務(wù)放在前臺(tái)運(yùn)行。
那為什么我要將GRPC服務(wù)放在前臺(tái)運(yùn)行呢?其實(shí)理論上放誰(shuí)都行,但由于我們的架構(gòu)原因。我們有的服務(wù)不需要啟動(dòng)HTTP服務(wù),而每一個(gè)服務(wù)都會(huì)啟動(dòng)GRPC服務(wù)。所以,將GRPC放置在前臺(tái),會(huì)更合適。
至于里面如何使用HTTP、GRPC的服務(wù)對(duì)象啟動(dòng)它們的服務(wù)。在這一節(jié)就不多贅述了。在之后的章節(jié)會(huì)有詳細(xì)的介紹~
看完了統(tǒng)一管理啟動(dòng)的start方法,那我們來(lái)看看如何停止服務(wù)吧
(3)處理stop
1、什么時(shí)候才去Stop?
我們開(kāi)啟了多個(gè)服務(wù),并且有的還是放在后臺(tái)運(yùn)行的。這就涉及到了多個(gè)攜程的間通信的問(wèn)題了
用什么來(lái)通信吶?我怎么知道HTTP服務(wù)掛沒(méi)掛?是意外掛的還是主動(dòng)掛的?我們?cè)趺茨軌騼?yōu)雅的統(tǒng)一關(guān)閉所有服務(wù)呢?
其實(shí)這一切的問(wèn)題,Go都為我們想好了:那就是使用Channels。一個(gè)channel是一個(gè)通信機(jī)制,它可以讓一個(gè)攜程通過(guò)它給另一個(gè)攜程發(fā)送值信息。每個(gè)channel都有一個(gè)特殊的類型,也就是channels可發(fā)送數(shù)據(jù)的類型。
我們把一個(gè)go程當(dāng)作一個(gè)人的化,那么main 方法啟動(dòng)的主go程就是你自己。在你的程序中使用到的其他go程,都是你的好幫手,你的好朋友,它們有給你去處理耗時(shí)邏輯的、有給你去執(zhí)行業(yè)務(wù)無(wú)關(guān)的切面邏輯的。而且是你的好幫手,按理來(lái)說(shuō)最好是由你自己去決定,要不要請(qǐng)一個(gè)好幫手。
當(dāng)你請(qǐng)來(lái)了一個(gè)好幫手后,它們會(huì)在你的背后為你做你讓他們做的事情。那么多個(gè)人之間的通信,比較現(xiàn)代的方法,那可以是:打個(gè)電話?發(fā)個(gè)消息?所以用到了一個(gè)溝通的信道:Channel
好了,當(dāng)你了解了這些后,也就是接收到一些電話后,我們才需要去stop。我們?cè)倩氐紻ousheng使用的情景:
2、Dousheng的應(yīng)用場(chǎng)景
主攜程是GRPC服務(wù)這個(gè)人,我們請(qǐng)了一個(gè)幫手,給我啟動(dòng)HTTP服務(wù)。這個(gè)時(shí)候,如果HTTP服務(wù)這個(gè)幫手意外出事了。既然是幫我么你做事,那我們肯定得對(duì)別人負(fù)責(zé)是吧。但是我們也不知道它出不出意外啊,怎么辦呢?這時(shí)候你想了兩個(gè)方法:
- 跟你的幫手HTTP,發(fā)了如下消息

這就需要HTTP自己告訴我們,按理來(lái)說(shuō),應(yīng)該是可以的。但是如果HTTP遇到了重大問(wèn)題,根本來(lái)不及告訴我們呢?咱們又是一個(gè)負(fù)責(zé)的男人。為了避免這種情況發(fā)生,又請(qǐng)一個(gè)人,專門給我們看HTTP有沒(méi)有遇到重大問(wèn)題。于是有了第二種方式:
- 在請(qǐng)一個(gè)幫手
signal.Notify,幫助我們監(jiān)聽(tīng)HTTP可能會(huì)遇到的重大問(wèn)題

當(dāng)我們收到HTTP出事的信號(hào)后,那我們就可以統(tǒng)一的去優(yōu)雅關(guān)閉服務(wù)了。就這樣,我們做了一個(gè)負(fù)責(zé)的人~
相信你已經(jīng)了解了核心的思想,我們來(lái)看看,用代碼該如何實(shí)現(xiàn)
3、代碼實(shí)現(xiàn)
- 啟動(dòng)
signal.Notify,用于監(jiān)聽(tīng)系統(tǒng)信號(hào)
我們已經(jīng)分析過(guò)了,我們需要再請(qǐng)一個(gè)幫手,來(lái)給我們處理HTTP可能會(huì)遇到的重大事故:(syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP, syscall.SIGINT)
// WaitSign 等待退出的信號(hào),實(shí)現(xiàn)優(yōu)雅退出
func (m *manager) waitSign() {
// 用于接收信號(hào)的信道
ch := make(chan os.Signal, 1)
// 接收這幾種信號(hào)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP, syscall.SIGINT)
// 需要在后臺(tái)等待關(guān)閉
go m.waitStop(ch)
}
當(dāng)signal.Notify收到上面所列舉的信號(hào)后,那么就可以去做關(guān)閉的事情了,那如何關(guān)閉呢?
- 讀取信號(hào),執(zhí)行優(yōu)雅關(guān)閉邏輯
// WaitStop 中斷信號(hào),比如Terminal [關(guān)閉服務(wù)的方法]
func (m *manager) waitStop(ch <-chan os.Signal) {
// 等待信號(hào),若收到了,我們進(jìn)行服務(wù)統(tǒng)一的關(guān)閉
for v := range ch {
switch v {
default:
m.l.Infof("接受到信號(hào):%s", v)
// 優(yōu)雅關(guān)閉HTTP服務(wù)
if m.http != nil {
if err := m.http.Stop(); err != nil {
m.l.Errorf("優(yōu)雅關(guān)閉 [HTTP] 服務(wù)出錯(cuò):%s", err.Error())
}
}
// 優(yōu)雅關(guān)閉GRPC服務(wù)
if err := m.grpc.Stop(); err != nil {
m.l.Errorf("優(yōu)雅關(guān)閉 [GRPC] 服務(wù)出錯(cuò):%s", err.Error())
}
}
}
}
這里的邏輯比較簡(jiǎn)單,就是當(dāng)接收到信號(hào)的時(shí)候,對(duì)HTTP、GRPC做優(yōu)雅關(guān)閉的邏輯。至于為什么要進(jìn)行優(yōu)雅關(guān)閉,而不是直接os.Exit()?我們?cè)谙乱还?jié)講~
這里值得一提的是,我們從chanel里獲取數(shù)據(jù),因?yàn)槲覀冞@里只和單個(gè)攜程間進(jìn)行通信了,使用的是 for range,并沒(méi)有使用for select
好了,這樣我們應(yīng)用的生命周期算是被我們優(yōu)雅的拿捏了。我們一直在講優(yōu)雅關(guān)閉這個(gè)詞,我們來(lái)解釋一下什么是優(yōu)雅關(guān)閉?為什么需要優(yōu)雅關(guān)閉?
三、什么是優(yōu)雅關(guān)閉
既然HTTP服務(wù)和GRPC服務(wù)都需要優(yōu)雅關(guān)閉,我們這里用HTTP服務(wù)來(lái)舉例。
先來(lái)看這張圖,假設(shè)有三個(gè)并行的請(qǐng)求至我們的HTTP服務(wù)。它們都期望得到服務(wù)器的response。HTTP服務(wù)器正常運(yùn)行的情況下,多半是沒(méi)問(wèn)題的。

請(qǐng)求已發(fā)出,若提供的HTTP服務(wù)突然異常關(guān)閉了呢?我們繼續(xù)來(lái)把HTTP服務(wù)比作一個(gè)人??纯此欠駜?yōu)雅呢?
(1)沒(méi)有優(yōu)雅關(guān)閉
如果HTTP這個(gè)人不太優(yōu)雅,是一個(gè)做事不怎么負(fù)責(zé)的渣男。當(dāng)自己異常over了之后,也不解決完自己的事情,就讓別人(request),找不到資源了。真的很不負(fù)責(zé)啊。
大致用一幅圖表示:

這個(gè)不優(yōu)雅的HTTP服務(wù),當(dāng)有還未處理的請(qǐng)求時(shí),自己就異常關(guān)閉了,那么它根本不會(huì)理會(huì)原先的請(qǐng)求是否完成了。它只管自己退出程序。
(2)有了優(yōu)雅關(guān)閉
看完了那個(gè)渣男HTTP(沒(méi)有優(yōu)雅關(guān)閉),我們簡(jiǎn)直想罵它了。那我們來(lái)看,當(dāng)一個(gè)優(yōu)雅的謙謙君子(有優(yōu)雅關(guān)閉),又是如何看待這個(gè)問(wèn)題的。

這是一個(gè)負(fù)責(zé)人的人,為什么說(shuō)他負(fù)責(zé)人、說(shuō)它優(yōu)雅呢?因?yàn)楫?dāng)它自己接收到異常關(guān)閉的信號(hào)后。它不會(huì)只顧自己關(guān)閉。它大概還會(huì)做兩件事:
- 關(guān)閉建立連接的請(qǐng)求通道,防止還會(huì)接收到新的請(qǐng)求
- 處理完以請(qǐng)求的,但是還未響應(yīng)的請(qǐng)求。保證資源得到響應(yīng),哪怕是錯(cuò)誤的
response。
正是因?yàn)樗饕隽诉@兩件事,我們才說(shuō)此時(shí)的HTTP服務(wù),是一個(gè)優(yōu)雅的謙謙君子。
而當(dāng)有很多個(gè)請(qǐng)求到時(shí)候,我們?cè)趺粗朗欠駮?huì)不會(huì)突然異常關(guān)閉呢?如果遇到了這種情況,我們應(yīng)該處理完未完成的響應(yīng),拒絕新的請(qǐng)求建立連接,因?yàn)槲覀兪且粋€(gè)優(yōu)雅的人。
以上就是優(yōu)雅管理Go Project生命周期的詳細(xì)內(nèi)容,更多關(guān)于Go Project生命周期的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語(yǔ)言提升開(kāi)發(fā)效率的語(yǔ)法糖技巧分享
每門語(yǔ)言都有自己的語(yǔ)法糖,像java的語(yǔ)法糖就有方法變長(zhǎng)參數(shù)、拆箱與裝箱、枚舉、for-each等等,Go語(yǔ)言也不例外。本文就來(lái)介紹一些Go語(yǔ)言的語(yǔ)法糖,需要的可以參考一下2022-07-07
淺析go中Ticker,Timer和Tick的用法與區(qū)別
在go面試的時(shí)候,面試官經(jīng)常會(huì)問(wèn)time包的Ticker,Timer以及Tick的區(qū)別,一般在超時(shí)控制的時(shí)候用的比較多,今天就跟隨小編一起來(lái)詳細(xì)學(xué)一下這幾個(gè)的區(qū)別吧2023-10-10
一文理解Goland協(xié)程調(diào)度器scheduler的實(shí)現(xiàn)
本文主要介紹了Goland協(xié)程調(diào)度器scheduler的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06
Golang信號(hào)處理及如何實(shí)現(xiàn)進(jìn)程的優(yōu)雅退出詳解
這篇文章主要給大家介紹了關(guān)于Golang信號(hào)處理及如何實(shí)現(xiàn)進(jìn)程的優(yōu)雅退出的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-03-03

