詳解如何在Go中實現(xiàn)優(yōu)雅停止
簡介
什么是優(yōu)雅停止?在談優(yōu)雅停止前,我們可以說說什么是優(yōu)雅重啟,或者說熱重啟。
簡言之,優(yōu)雅重啟就是在服務(wù)升級、配置更新時,要重新啟動服務(wù),優(yōu)雅重啟就是在服務(wù)不中斷或連接不丟失的情況下,重啟服務(wù)。優(yōu)雅重啟的整個流程中,新的進程將在舊的進程停止前啟動,舊進程會完成活動中的請求后優(yōu)雅地關(guān)閉進程。
優(yōu)雅重啟是服務(wù)開發(fā)中一個非常重要的概念,它讓我們在不中斷服務(wù)的情況下,更新代碼和修復(fù)問題。它在維持高可用性的生產(chǎn)環(huán)境中尤其關(guān)鍵。
從上面的這段可知,優(yōu)雅重啟是由兩個部分組成,分別是優(yōu)雅停止和啟動。
本文重點介紹優(yōu)雅停止,而優(yōu)雅啟動的整個流程要借助于外部工具控制,如 k8s 的容器編排。
優(yōu)雅停止
優(yōu)雅停止,即要在停止服務(wù)的同時,保證業(yè)務(wù)的完整性。從目標上看,優(yōu)雅停止經(jīng)歷三個步驟:通知服務(wù)停止、服務(wù)啟動清理,等待清理確認退出。
要停止一個服務(wù),首先是通過一些機制告知服務(wù)要執(zhí)行退出前的工作,最常見的就是基于操作系統(tǒng)信號,我們慣例監(jiān)聽的信號主要是兩個,分別是由 kill PID 發(fā)出的 SIGTERM 和 CTRL+C 發(fā)出的 SIGINT。 其他信號還有,CTRL+/ 發(fā)出的 SIGQUIT。
當接收到指定信號,服務(wù)就要停止接受新的請求,且等待當前活動中的請求全部完成后再完全停止服務(wù)。
接下來,開始具體的代碼實現(xiàn)部分吧。
從 HTTP 服務(wù)開始
談優(yōu)雅重啟,最常被引用的案例就是 HTTP 服務(wù),我將通過代碼逐步演示這個過程。如下是一個常規(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)
}
}
我們通過 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 秒的處理階段。假設(shè)這時,我們 CTRL+C 請求退出,HTTP 服務(wù)會直接退出,我們的 curl 請求被直接中斷。
我們可以使用 Go 標準庫提供的 http.Server 有一個 Shutdown 方法,可以安全地關(guān)閉服務(wù)器而不中斷任何活動的連接。而我們要做的,只需在收到停止信號后,執(zhí)行 Shutdown 即可。
信號方面,我們通過 Go 標準庫 signal 實現(xiàn),它提供了一個 Notify 函數(shù),可與 chan nnel 配合傳遞信號消息。我們監(jiān)聽的目標信號是 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)
// 注冊接收信號的 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 啟動服務(wù),執(zhí)行 curl 命令測試接口,在請求還沒有返回之時,我們可以通過 CTRL+C 停止服務(wù),它會有一段時間等待,我們可以在這個過程中嘗試 curl 請求,看它是否還接收新的請求。
如果希望防止程序假死,或者其他問題導(dǎo)致服務(wù)長時間無法退出,可通過 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 的服務(wù),如何讓它支持優(yōu)雅停止呢?畢竟不是所有項目都是 HTTP 服務(wù),不是所有項目都有現(xiàn)成的框架。
本文開頭提到的的三步驟,net/http 包的 Shutdown 把最核心的服務(wù)停止前的清理和等待都已經(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 服務(wù)支持優(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,它負責(zé)檢查是否還有執(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 標志位來標識服務(wù)狀態(tài),并且免去了基于定時器的定期輪詢檢查的過程,還更加實時和高效。當然,net/http 使用輪詢檢查機制,是它的場景所決定,和我們這里不完全一樣。
一點思考
Go 語言支持多種方式在 Goroutine 間傳遞信息,這催生了多樣的優(yōu)雅停止實現(xiàn)方式。如果是在涉及多個嵌套 Goroutine 的場景中,我們可以引入 context 來實現(xiàn)多層級的狀態(tài)和信息傳遞,確保操作的連貫性和安全性。
然盡管實現(xiàn)方式眾多,但其核心思路是一致的,而底層目標始終是我們要保證處理邏輯的完整性。
另外,通過將優(yōu)雅停止與容器編排技術(shù)結(jié)合,并為服務(wù)添加健康檢查,我們能夠確保服務(wù)總有實例在活躍狀態(tài),實現(xiàn)真正意義上的優(yōu)雅重啟。這不僅提高了服務(wù)的可靠性,也優(yōu)化了資源的利用效率。
總結(jié)
本文探索了 Go 語言中優(yōu)雅重啟的實現(xiàn)方法,展示了如何通過 http.Server 的 Shutdown 方法安全地重啟服務(wù),以及使用 context 控制超時?;诖耍覀兂橄蟪隽艘话惴?wù)優(yōu)雅停止的核心思路。
以上就是詳解如何在Go中實現(xiàn)優(yōu)雅停止的詳細內(nèi)容,更多關(guān)于在Go中實現(xiàn)停止的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang 生成對應(yīng)的數(shù)據(jù)表struct定義操作
這篇文章主要介紹了golang 生成對應(yīng)的數(shù)據(jù)表struct定義操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04
Mac下Vs code配置Go語言環(huán)境的詳細過程
這篇文章給大家介紹Mac下Vs code配置Go語言環(huán)境的詳細過程,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2021-07-07
golang?pprof?監(jiān)控系列?go?trace統(tǒng)計原理與使用解析
這篇文章主要為大家介紹了golang?pprof?監(jiān)控系列?go?trace統(tǒng)計原理與使用解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-04-04
Golang使用反射的動態(tài)方法調(diào)用詳解
Go是一種靜態(tài)類型的語言,提供了大量的安全性和性能。這篇文章主要和大家介紹一下Golang使用反射的動態(tài)方法調(diào)用,感興趣的小伙伴可以了解一下2023-03-03
Go語言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

