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