go實(shí)現(xiàn)服務(wù)優(yōu)雅關(guān)閉的示例
為什么需要優(yōu)雅關(guān)閉
什么叫優(yōu)雅關(guān)閉?先說不優(yōu)雅關(guān)閉,就是什么都不管,強(qiáng)制關(guān)閉進(jìn)程,這會(huì)導(dǎo)致有些正在處理中的請求被強(qiáng)行中斷
這樣做有什么問題?
- 用戶本次請求會(huì)失敗,降低用戶體驗(yàn)
- 沒有事務(wù)的數(shù)據(jù)庫操作,會(huì)產(chǎn)生部分成功的問題,破壞原子性
- 某些緩服務(wù)需要定期將本地緩存刷到遠(yuǎn)程db,強(qiáng)制關(guān)閉會(huì)導(dǎo)致數(shù)據(jù)丟失
優(yōu)雅關(guān)閉的核心是以下功能:
- 如何監(jiān)聽退出信號(hào)
- 如何拒絕新請求
- 如何等待進(jìn)行中的請求處理完畢
監(jiān)控服務(wù)退出信號(hào)
在go中使用下面的代碼監(jiān)聽退出信號(hào),如果c返回,說明監(jiān)聽到信號(hào)
不同的操作系統(tǒng)監(jiān)聽不同的退出信號(hào)
c := make(chan os.Signal, 1)
signals := []os.Signal{
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT,
}
signal.Notify(c, signals...)
<-c
拒絕新請求
go在1.8后增加了shutdown方法來,我們看看它怎么實(shí)現(xiàn)優(yōu)雅關(guān)閉:
srv.inShutdown.setTrue() lnerr := srv.closeListenersLocked() srv.closeDoneChanLocked()
- 設(shè)置inShutdown標(biāo)志位
- 關(guān)閉所有的listener
- 關(guān)閉doneChan
這一段對(duì)應(yīng)到http服務(wù)接收請求的流程:
for {
rw, err := l.Accept()
if err != nil {
select {
case <-srv.getDoneChan():
return ErrServerClosed
// ...
}
一旦關(guān)閉listener,關(guān)閉doneChan后,http服務(wù)就不會(huì)再接收新的請求,直接返回
執(zhí)行關(guān)閉之前的回調(diào)
for _, f := range srv.onShutdown {
go f()
}
這里的回調(diào)實(shí)現(xiàn)得比較粗糙:
- 沒有優(yōu)先級(jí)的概念,所有回調(diào)并發(fā)執(zhí)行,因此需要保證回調(diào)之間沒有依賴
- 雖然回調(diào)不適合長時(shí)間運(yùn)行,但Go http沒有提供機(jī)制來保證這些回調(diào)一定能執(zhí)行完畢,若想做到這點(diǎn)需要自己處理
等待處理中的請求執(zhí)行完畢
設(shè)置標(biāo)識(shí)位可以拒絕新的請求,但依舊在執(zhí)行的請求還在處理中,需要等這些請求執(zhí)行完畢
等待處理中的請求執(zhí)行完畢有兩種思路:
- 等待一段固定的時(shí)間
- 實(shí)時(shí)維護(hù)請求的計(jì)數(shù)
go選擇了兩種方式結(jié)合的模式,通過ctx設(shè)置一個(gè)最大的等待時(shí)間,同時(shí)不斷輪詢正在請求中的計(jì)數(shù)
ctx超時(shí)或者計(jì)數(shù)變?yōu)?,都會(huì)返回
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())
}
}
這里每隔一定時(shí)間檢查已有請求是否執(zhí)行完畢,如果執(zhí)行完畢,或者外部通過ctx設(shè)置的超時(shí)到期就會(huì)返回
檢查間隔是多少?
- 從1ms開始,每輪檢查后倍增,最大500ms
怎么判斷是否執(zhí)行完畢?
- 所有的連接都關(guān)閉
- 所有的listener都關(guān)閉
服務(wù)收到監(jiān)聽信號(hào)返回之前,關(guān)閉連接和listener,會(huì)被這里檢查到

實(shí)戰(zhàn)
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
}
該代碼做了下面的事:
- 注冊一個(gè)10s才返回的路由處理函數(shù)
- 開子協(xié)程監(jiān)聽OS的退出信號(hào),如果監(jiān)聽到了開始進(jìn)行優(yōu)雅關(guān)閉,雖多等待30s
- 主協(xié)程調(diào)用 server.ListenAndServe(),開始監(jiān)聽請求
需要注意的是,一定要在子協(xié)程中優(yōu)雅關(guān)閉結(jié)束后,主協(xié)程才能退出,這里用channel控制
因?yàn)橹鲄f(xié)程發(fā)現(xiàn)doneChan被關(guān)閉時(shí)會(huì)馬上返回,但此時(shí)主協(xié)程開的業(yè)務(wù)處理協(xié)程還在進(jìn)行中,如果主協(xié)程此時(shí)退出,無法達(dá)到優(yōu)雅關(guān)閉的效果
按照以下流程測試:
- 啟動(dòng) Web 服務(wù)
- 在瀏覽器請求http://localhost:8080/aaa
- 過5秒后在控制臺(tái)按下ctrl+c
- 觀察控制臺(tái)程序是否不會(huì)立刻結(jié)束,而是在 10s 后結(jié)束
支持強(qiáng)制退出
既然有優(yōu)雅退出,那就有強(qiáng)制退出,我們假設(shè)如果按下兩次ctrl+c,代表用戶希望服務(wù)強(qiáng)制退出:
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)
}()
// ...
}()
做法很簡單,收到第一個(gè)退出信號(hào)后,再開一個(gè)子協(xié)程,如果再收到退出信號(hào),就調(diào)用os.Exit退出進(jìn)程
并且close channel的容量需要為2,避免當(dāng)兩次退出信號(hào)過短時(shí)丟失信號(hào)
到此這篇關(guān)于go實(shí)現(xiàn)服務(wù)優(yōu)雅關(guān)閉的示例的文章就介紹到這了,更多相關(guān)go 服務(wù)關(guān)閉內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang優(yōu)化目錄遍歷的實(shí)現(xiàn)方法
對(duì)于go1.16的新變化,大家印象最深的可能是io包的大規(guī)模重構(gòu),但這個(gè)重構(gòu)實(shí)際上還引進(jìn)了一個(gè)優(yōu)化,這篇文章要說的就是這個(gè)優(yōu)化,所以本將給大家介紹golang是如何優(yōu)化目錄遍歷的,需要的朋友可以參考下2024-08-08
Golang兩行代碼實(shí)現(xiàn)發(fā)送釘釘機(jī)器人消息
創(chuàng)建一個(gè)釘釘機(jī)器人必須使用加簽,本文通過Golang兩行代碼實(shí)現(xiàn)發(fā)送釘釘機(jī)器人消息,本文給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2021-12-12

