詳解如何在Go中實(shí)現(xiàn)優(yōu)雅停止
簡(jiǎn)介
什么是優(yōu)雅停止?在談優(yōu)雅停止前,我們可以說(shuō)說(shuō)什么是優(yōu)雅重啟,或者說(shuō)熱重啟。
簡(jiǎn)言之,優(yōu)雅重啟就是在服務(wù)升級(jí)、配置更新時(shí),要重新啟動(dòng)服務(wù),優(yōu)雅重啟就是在服務(wù)不中斷或連接不丟失的情況下,重啟服務(wù)。優(yōu)雅重啟的整個(gè)流程中,新的進(jìn)程將在舊的進(jìn)程停止前啟動(dòng),舊進(jìn)程會(huì)完成活動(dòng)中的請(qǐng)求后優(yōu)雅地關(guān)閉進(jìn)程。
優(yōu)雅重啟是服務(wù)開(kāi)發(fā)中一個(gè)非常重要的概念,它讓我們?cè)诓恢袛喾?wù)的情況下,更新代碼和修復(fù)問(wèn)題。它在維持高可用性的生產(chǎn)環(huán)境中尤其關(guān)鍵。
從上面的這段可知,優(yōu)雅重啟是由兩個(gè)部分組成,分別是優(yōu)雅停止和啟動(dòng)。
本文重點(diǎn)介紹優(yōu)雅停止,而優(yōu)雅啟動(dòng)的整個(gè)流程要借助于外部工具控制,如 k8s 的容器編排。
優(yōu)雅停止
優(yōu)雅停止,即要在停止服務(wù)的同時(shí),保證業(yè)務(wù)的完整性。從目標(biāo)上看,優(yōu)雅停止經(jīng)歷三個(gè)步驟:通知服務(wù)停止、服務(wù)啟動(dòng)清理,等待清理確認(rèn)退出。
要停止一個(gè)服務(wù),首先是通過(guò)一些機(jī)制告知服務(wù)要執(zhí)行退出前的工作,最常見(jiàn)的就是基于操作系統(tǒng)信號(hào),我們慣例監(jiān)聽(tīng)的信號(hào)主要是兩個(gè),分別是由 kill PID
發(fā)出的 SIGTERM 和 CTRL+C 發(fā)出的 SIGINT。 其他信號(hào)還有,CTRL+/ 發(fā)出的 SIGQUIT。
當(dāng)接收到指定信號(hào),服務(wù)就要停止接受新的請(qǐng)求,且等待當(dāng)前活動(dòng)中的請(qǐng)求全部完成后再完全停止服務(wù)。
接下來(lái),開(kāi)始具體的代碼實(shí)現(xiàn)部分吧。
從 HTTP 服務(wù)開(kāi)始
談優(yōu)雅重啟,最常被引用的案例就是 HTTP 服務(wù),我將通過(guò)代碼逐步演示這個(gè)過(guò)程。如下是一個(gè)常規(guī) HTTP 服務(wù):
func hello(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello World\n") } func main() { http.HandleFunc("/", hello) log.Println("Starting server on :8080") if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal("ListenAndServe: ", err) } }
我們通過(guò) time.Sleep
增加 hello 的耗時(shí),以便于調(diào)試。
func hello(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello World\n") time.Sleep(10 * time.Second) }
運(yùn)行:
$ go run main.go
通過(guò) curl 請(qǐng)求訪問(wèn) http://localhost:8080/
,它進(jìn)入到 10 秒的處理階段。假設(shè)這時(shí),我們 CTRL+C 請(qǐng)求退出,HTTP 服務(wù)會(huì)直接退出,我們的 curl 請(qǐng)求被直接中斷。
我們可以使用 Go 標(biāo)準(zhǔn)庫(kù)提供的 http.Server
有一個(gè) Shutdown
方法,可以安全地關(guān)閉服務(wù)器而不中斷任何活動(dòng)的連接。而我們要做的,只需在收到停止信號(hào)后,執(zhí)行 Shutdown
即可。
信號(hào)方面,我們通過(guò) Go 標(biāo)準(zhǔn)庫(kù) signal
實(shí)現(xiàn),它提供了一個(gè) Notify
函數(shù),可與 chan nnel 配合傳遞信號(hào)消息。我們監(jiān)聽(tīng)的目標(biāo)信號(hào)是 SIGINT
和 SIGTERM
。
重新修改 HTTP 服務(wù)入口,使用 http.Server
的 Shutdown
函數(shù)關(guān)閉 Server
。
func main() { mux := http.NewServeMux() mux.HandleFunc("/", hello) server := http.Server{Addr: ":8080", Handler: mux} go server.ListenAndServe() quit := make(chan os.Signal, 1) // 注冊(cè)接收信號(hào)的 channel signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit // 等待停止信號(hào) if err := server.Shutdown(context.Background()); err != nil { log.Fatal("Shutdown: ", err) } }
我們將 server.ListenAndServe
運(yùn)行于另一個(gè) goroutine 中同時(shí)忽略了它的返回錯(cuò)誤。
通過(guò) signal.Notify
注冊(cè)信號(hào)。當(dāng)收到如 CTRL+C 或 kill PID 發(fā)出的中斷信號(hào),執(zhí)行 serve.Shutdown
,它會(huì)通知到 server 停止接收新的請(qǐng)求,并等待活動(dòng)中的連接處理完成。
現(xiàn)在運(yùn)行 go run main.go
啟動(dòng)服務(wù),執(zhí)行 curl
命令測(cè)試接口,在請(qǐng)求還沒(méi)有返回之時(shí),我們可以通過(guò) CTRL+C 停止服務(wù),它會(huì)有一段時(shí)間等待,我們可以在這個(gè)過(guò)程中嘗試 curl
請(qǐng)求,看它是否還接收新的請(qǐng)求。
如果希望防止程序假死,或者其他問(wèn)題導(dǎo)致服務(wù)長(zhǎng)時(shí)間無(wú)法退出,可通過(guò) context.WithTimeout
方法包裝下傳遞給 Shutdown
方法的 ctx
變量。
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { log.Fatal("Shutdown: ", err) }
到這里,我們就介紹完了 Go 標(biāo)準(zhǔn)庫(kù) net/http
的優(yōu)雅停止的使用方案。
抽象出一個(gè)常規(guī)方案
如果開(kāi)發(fā)一個(gè)非 HTTP 的服務(wù),如何讓它支持優(yōu)雅停止呢?畢竟不是所有項(xiàng)目都是 HTTP 服務(wù),不是所有項(xiàng)目都有現(xiàn)成的框架。
本文開(kāi)頭提到的的三步驟,net/http
包的 Shutdown
把最核心的服務(wù)停止前的清理和等待都已經(jīng)在內(nèi)部實(shí)現(xiàn)了。我們可解讀下它的實(shí)現(xiàn)。
進(jìn)入到 Shutdown
的源碼中,重點(diǎn)是開(kāi)頭的第一句代碼,如下所示:
// future calls to methods such as Serve will return ErrServerClosed. func (srv *Server) Shutdown(ctx context.Context) error { srv.inShutdown.Store(true) // ...其他清理代碼 // ...等待活動(dòng)請(qǐng)求完成并將其關(guān)閉 }
inShutdown
是一個(gè)標(biāo)志位,用于標(biāo)識(shí)程序是否已停止。為了解決并發(fā)數(shù)據(jù)競(jìng)爭(zhēng),它的底層類型是 atomic.bool
,。
在 server.go
中的 Server.Serve
方法中,通過(guò)判斷 inShutdown
決定是否繼續(xù)接受新的請(qǐng)求。
func (srv *Server) Serve(l net.Listener) error { // ... for { rw, err := l.Accept() if err != nil { if srv.shuttingDown() { return ErrServerClosed } // ... }
我們可以從如上的分析中得知,要讓 HTTP 服務(wù)支持優(yōu)雅停止要啟動(dòng)兩個(gè) goroutine,Shutdown
運(yùn)行與 main goroutine 中,當(dāng)接收中停止信號(hào),通過(guò) inShutdown
標(biāo)志位通知運(yùn)行中的 goroutine。
用簡(jiǎn)化的代碼表示這個(gè)一般模式。
var inShutdown bool func Start() { for !inShutdown { // running time.Sleep(10 * time.Second) } } func Shutdown() { inShutdown = true } func main() { go Start() quit = make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <- quit Shutdown() }
大概看起來(lái)是那么回事,但這里的代碼少了一個(gè)步驟,即 Shutdown
沒(méi)有等待 Start
完成。
標(biāo)準(zhǔn)庫(kù) net/http
是通過(guò) for 循環(huán)不斷檢查是否有活動(dòng)中的連接,如果連接沒(méi)有進(jìn)行中請(qǐng)求會(huì)將其關(guān)閉,直到將所有連接關(guān)閉,便會(huì)退出 Shutdown
。
核心代碼如下:
func (srv *Server) Shutdown(ctx context.Context) { // ...之前的代碼 timer := time.NewTimer(nextPollInterval()) defer timer.Stop() for { if srv.closeIdleConns() { return lnerr } select { case <-ctx.Done(): return ctx.Err() case <-timer.C: timer.Reset(nextPollInterval()) } } }
重點(diǎn)就是那句 closeIdleConns
,它負(fù)責(zé)檢查是否還有執(zhí)行中的請(qǐng)求。我就不把這部分的源代碼貼出來(lái)了。而檢查頻率是通過(guò) timer 控制的。
現(xiàn)在讓簡(jiǎn)化版等待 Start
完成后才退出。我們引入一個(gè)名為 isStop
的標(biāo)志位以監(jiān)控停止?fàn)顟B(tài)。
var inShutdown bool var isStop bool func Start() { for !inShutdown { // running time.Sleep(10 * time.Second) } isStop = true } func Shutdown() { inShutdown = true timer := time.NewTimer(time.Millisecond) defer timer.Stop() for { if isStop { return } <- timer.C timer.Reset(time.Millisecond)) } }
如上的代碼中,Start
函數(shù)退出時(shí)會(huì)執(zhí)行 isStop = true
表明已退出,在 Shutdown
中,通過(guò)定期檢查 isStop
等待 Start
退出完成。
此外,net/http
的 Shutdown
方法還接收了一個(gè) context.Context
參數(shù),允許實(shí)現(xiàn)超時(shí)控制,從而防止程序假死或強(qiáng)制關(guān)閉。
需要特別指出的是,示例中用的 isStop
和 inShutdown
標(biāo)志位為非原子類型,在正式場(chǎng)景中,為避免數(shù)據(jù)競(jìng)爭(zhēng),要使用原子操作或其他同步機(jī)制。
除了可用共享內(nèi)存標(biāo)志位在不同協(xié)程間傳遞狀態(tài),也可以通過(guò) channel 實(shí)現(xiàn),或你看到過(guò)類似如下的形式。
var inShutdown bool func Start(stop chan struct{}) { for !inShutdown { // running time.Sleep(10 * time.Second) } stop <- struct{}{} } func Shutdown() { inShutdown = true } func main() { stop := make(chan struct{}) defer close(stop) go Start(stop) go func() { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit Shutdown() }() <-stop }
如上的代碼中,Start
通過(guò) channel 通知主 goroutine,當(dāng)觸發(fā)停止信號(hào),isShutdown
通知 Start
要停止退出,它成功退出后,通過(guò) stop <- struct{}
通知主函數(shù),結(jié)束等待。
總的來(lái)說(shuō),channel 的優(yōu)勢(shì)很明顯,避免了單獨(dú)管理一個(gè) isStop 標(biāo)志位來(lái)標(biāo)識(shí)服務(wù)狀態(tài),并且免去了基于定時(shí)器的定期輪詢檢查的過(guò)程,還更加實(shí)時(shí)和高效。當(dāng)然,net/http
使用輪詢檢查機(jī)制,是它的場(chǎng)景所決定,和我們這里不完全一樣。
一點(diǎn)思考
Go 語(yǔ)言支持多種方式在 Goroutine 間傳遞信息,這催生了多樣的優(yōu)雅停止實(shí)現(xiàn)方式。如果是在涉及多個(gè)嵌套 Goroutine 的場(chǎng)景中,我們可以引入 context 來(lái)實(shí)現(xiàn)多層級(jí)的狀態(tài)和信息傳遞,確保操作的連貫性和安全性。
然盡管實(shí)現(xiàn)方式眾多,但其核心思路是一致的,而底層目標(biāo)始終是我們要保證處理邏輯的完整性。
另外,通過(guò)將優(yōu)雅停止與容器編排技術(shù)結(jié)合,并為服務(wù)添加健康檢查,我們能夠確保服務(wù)總有實(shí)例在活躍狀態(tài),實(shí)現(xiàn)真正意義上的優(yōu)雅重啟。這不僅提高了服務(wù)的可靠性,也優(yōu)化了資源的利用效率。
總結(jié)
本文探索了 Go 語(yǔ)言中優(yōu)雅重啟的實(shí)現(xiàn)方法,展示了如何通過(guò) http.Server 的 Shutdown 方法安全地重啟服務(wù),以及使用 context 控制超時(shí)?;诖耍覀兂橄蟪隽艘话惴?wù)優(yōu)雅停止的核心思路。
以上就是詳解如何在Go中實(shí)現(xiàn)優(yōu)雅停止的詳細(xì)內(nèi)容,更多關(guān)于在Go中實(shí)現(xiàn)停止的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang 生成對(duì)應(yīng)的數(shù)據(jù)表struct定義操作
這篇文章主要介紹了golang 生成對(duì)應(yīng)的數(shù)據(jù)表struct定義操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04GoLang channel底層代碼實(shí)現(xiàn)詳解
Channel和goroutine的結(jié)合是Go并發(fā)編程的大殺器。而Channel的實(shí)際應(yīng)用也經(jīng)常讓人眼前一亮,通過(guò)與select,cancel,timer等結(jié)合,它能實(shí)現(xiàn)各種各樣的功能。接下來(lái),我們就要梳理一下GoLang channel底層代碼實(shí)現(xiàn)2022-10-10Mac下Vs code配置Go語(yǔ)言環(huán)境的詳細(xì)過(guò)程
這篇文章給大家介紹Mac下Vs code配置Go語(yǔ)言環(huán)境的詳細(xì)過(guò)程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2021-07-07golang?pprof?監(jiān)控系列?go?trace統(tǒng)計(jì)原理與使用解析
這篇文章主要為大家介紹了golang?pprof?監(jiān)控系列?go?trace統(tǒng)計(jì)原理與使用解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04Golang使用反射的動(dòng)態(tài)方法調(diào)用詳解
Go是一種靜態(tài)類型的語(yǔ)言,提供了大量的安全性和性能。這篇文章主要和大家介紹一下Golang使用反射的動(dòng)態(tài)方法調(diào)用,感興趣的小伙伴可以了解一下2023-03-03Golang實(shí)現(xiàn)超時(shí)機(jī)制讀取文件的方法示例
讀寫(xiě)文件是Go程序的基本任務(wù),包括使用程序查看文件內(nèi)容、創(chuàng)建或修改文件,Go提供了os,ioutil,io以及bufio包實(shí)現(xiàn)文件操作,本文介紹如果在讀文件過(guò)程中增加超時(shí)機(jī)制,避免文件太大一直占用資源,需要的朋友可以參考下2025-01-01用Go語(yǔ)言編寫(xiě)一個(gè)簡(jiǎn)單的分布式系統(tǒng)
這篇文章主要介紹了用Go語(yǔ)言編寫(xiě)一個(gè)簡(jiǎn)單的分布式系統(tǒng),文中的代碼示例講解的非常詳細(xì),對(duì)我們的學(xué)習(xí)或工作有一定的幫助,感興趣的小伙伴跟著小編一起來(lái)看看吧2023-08-08Go語(yǔ)言kube-scheduler深度剖析與開(kāi)發(fā)之pod調(diào)度
這篇文章主要為大家介紹了Go語(yǔ)言kube-scheduler深度剖析與開(kāi)發(fā),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04詳解Go語(yǔ)言中new和make關(guān)鍵字的區(qū)別
本篇文章來(lái)介紹一道非常常見(jiàn)的面試題,到底有多常見(jiàn)呢?可能很多面試的開(kāi)場(chǎng)白就是由此開(kāi)始的。那就是 new 和 make 這兩個(gè)內(nèi)置函數(shù)的區(qū)別,希望對(duì)大家有所幫助2023-03-03