Go標(biāo)準(zhǔn)庫http?server的優(yōu)雅關(guān)閉深入理解
引言
本篇為【深入理解Go標(biāo)準(zhǔn)庫】系列第三篇
第一篇:http server的啟動
第二篇:ServeMux的使用與模式匹配
第三篇:http server的優(yōu)雅關(guān)閉??
本系列將持續(xù)更新,歡迎關(guān)注 ?? 獲取實時通知
還記得怎么啟動一個HTTP Server么?
package main import ( "net" "net/http" ) func main() { // 方式1 err := http.ListenAndServe(":8080", nil) if err != nil { panic(err) } // 方式2 // server := &http.Server{Addr: ":8080"} // err := server.ListenAndServe() // if err != nil { // panic(err) // } }
ListenAndServe
在不出錯的情況下,會一直阻塞在這個位置,如何停止這樣的一個HTTP Server呢?
CTRL+C
是結(jié)束一個進(jìn)程常用的方式,它和kill pid
或者kill -l 15 pid
命令本質(zhì)上沒有任何區(qū)別,他們都是向進(jìn)程發(fā)送了SIGTERM
信號。因為程序沒有設(shè)置對SIGTERM
信號的處理程序,所以系統(tǒng)默認(rèn)的信號處理程序結(jié)束了我們的進(jìn)程
這會帶來什么問題?
在服務(wù)器的進(jìn)程被殺死時,我們的服務(wù)器可能正在處理請求并未完成。因此對于客戶端產(chǎn)生了一個預(yù)期外的錯誤
curl -v --max-time 4 127.0.0.1:8009/foo * Connection #0 to host 127.0.0.1 left intact * Trying 127.0.0.1:8009... * Connected to 127.0.0.1 (127.0.0.1) port 8009 (#0) > GET /foo HTTP/1.1 > Host: 127.0.0.1:8009 > User-Agent: curl/7.86.0 > Accept: */* > * Empty reply from server * Closing connection 0 curl: (52) Empty reply from server
如果有nginx代理,因為upstream的中斷,nginx會產(chǎn)生502的響應(yīng)
curl -v --max-time 11 127.0.0.1:8010/foo * Trying 127.0.0.1:8010... * Connected to 127.0.0.1 (127.0.0.1) port 8010 (#0) > GET /foo HTTP/1.1 > Host: 127.0.0.1:8010 > User-Agent: curl/7.86.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 502 Bad Gateway < Server: nginx/1.25.3 < Date: Sat, 02 Dec 2023 10:14:33 GMT < Content-Type: text/html < Content-Length: 497 < Connection: keep-alive < ETag: "6537cac7-1f1"
優(yōu)雅關(guān)閉的初步實現(xiàn)
優(yōu)雅關(guān)閉(graceful shutdown)指的是我們的HTTP Server關(guān)閉前既拒絕新來的請求,又正確的處理完正在進(jìn)行中的請求,隨后進(jìn)程退出。如何實現(xiàn)?
?? 異步啟動HTTP server
因為ListenAndServe
會阻塞goroutine,如果還需要讓代碼繼續(xù)執(zhí)行,我們需要把它放到一個異步的goroutine中
go func() { if err := srv.ListenAndServe(); err != nil { panic(err) } }()
?? 第二步:設(shè)置SIGTERM信號處理程序
操作系統(tǒng)默認(rèn)的信號處理程序是直接結(jié)束進(jìn)程,因此要實現(xiàn)graceful shutdown,要設(shè)置程序自己的信號處理程序。
Go中可以使用如下的方式來處理信號
signal.Notify
來設(shè)置我們要監(jiān)聽的信號,一旦有程序設(shè)定的信號發(fā)生時,信號會被寫入channel中signalCh chan os.Signal
我們定義的是一個帶緩沖的channel,當(dāng)channel中沒有數(shù)據(jù)時讀操作會阻塞
signalCh := make(chan os.Signal, 1) signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM) sig := <-signalCh log.Printf("Received signal: %v\n", sig)
?? 第三步:平滑的關(guān)閉HTTP Server
在自定義的信號處理程序中處理什么呢?
1、首先需要關(guān)閉端口的監(jiān)聽,此時新的請求就無法建立連接
2、對空閑的連接進(jìn)行關(guān)閉
3、對進(jìn)行中的連接等待處理完成,變成空閑連接后進(jìn)行關(guān)閉
在Go 1.8以前實現(xiàn)上述操作需要編寫大量的代碼,也有一些第三方的庫(tylerstillwate/graceful、facebookarchive/grace等)可供使用。但Go1.8之后標(biāo)準(zhǔn)庫提供了 Shutdown()
方法
?? 實現(xiàn):綜合上面三步有如下實現(xiàn)
func main() { mx := http.NewServeMux() mx.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { time.Sleep(time.Duration(rand.Intn(10)) * time.Second) w.Write([]byte("Receive path foo\n")) }) srv := http.Server{ Addr: ":8009", Handler: mx, } go func() { if err := srv.ListenAndServe(); err != nil { panic(err) } }() signalCh := make(chan os.Signal, 1) signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM) sig := <-signalCh log.Printf("Received signal: %v\n", sig) if err := srv.Shutdown(context.Background()); err != nil { log.Fatalf("Server shutdown failed: %v\n", err) } log.Println("Server shutdown gracefully") }
沒有收到SIGINT
、SIGTERM
信號前,main goroutine被signalCh
的讀阻塞
一旦收到信號,signalCh
的阻塞被解除會往下執(zhí)行server的Shutdown()
,Shutdown()
函數(shù)會處理好活躍和非活躍的連接,并返回結(jié)果
上述代碼有什么問題么?
優(yōu)雅關(guān)閉實現(xiàn)的細(xì)節(jié)
?? 當(dāng)Shutdown
被調(diào)用時ListenAndServe
會立刻返回http.ErrServerClosed
的錯誤
go func() { if err := srv.ListenAndServe(); err != nil { panic(err) } }()
對于上文的代碼,Shutdown()
剛被調(diào)用,ListenAndServe
所在的goroutine就拋出了panic,因而也導(dǎo)致main goroutine被退出,并沒有達(dá)到運行Shutdown()
預(yù)期的效果
如果依舊想對ListenAndServe
的錯誤拋出painc,需要忽略http.ErrServerClosed
的錯誤
go func() { err := srv.ListenAndServe() if err != nil && err != http.ErrServerClosed { panic(err) } }()
?? 在有限的時間內(nèi)關(guān)閉服務(wù)器
優(yōu)雅關(guān)閉過程中會等待進(jìn)行中的請求完成。但請求處理的過程可能非常耗時,或者請求本身已經(jīng)陷入了無法結(jié)束的狀態(tài),我們不可能無限的等待下去,因此設(shè)定一個關(guān)閉的上限時間會更穩(wěn)妥。
Shutdown()
接受一個context.Context
類型的參數(shù),我們可以用來設(shè)定超時時間
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Fatalf("Server shutdown failed: %v\n", err) } log.Println("Server shutdown gracefully")
通過ctx.Done()
可以區(qū)分是否因為超時導(dǎo)致的服務(wù)器關(guān)閉,因而可以對不同的退出原因進(jìn)行區(qū)分
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { select { case <-ctx.Done(): // 由于達(dá)到超時時間服務(wù)器關(guān)閉,未完成優(yōu)雅關(guān)閉 log.Println("timeout of 5 seconds.") default: // 其他原因?qū)е碌姆?wù)關(guān)閉異常,未完成優(yōu)雅關(guān)閉 log.Fatalf("Server shutdown failed: %v\n", err) } return } // 正確執(zhí)行優(yōu)雅關(guān)閉服務(wù)器 log.Println("Server shutdown gracefully")
?? 釋放其他資源
除了顯式的釋放資源,main goroutine也有必要通知其他goroutine進(jìn)程即將退出,做必要的處理
例如,我們的服務(wù)在啟動后會向服務(wù)中心進(jìn)行注冊,之后異步定時上報自身狀態(tài)。
為了讓注冊中心第一時間感知到服務(wù)已下線,需要主動注銷服務(wù)。在注銷服務(wù)前,需要先暫停異步的定時上報
context.Context
讓我們可以很輕松的做到這件事
ctx, cancel := context.WithCancel(context.Background()) defer func() { cancel() }() // 需要在服務(wù)啟動后才在注冊中心注冊 go func() { tc := time.NewTicker(5 * time.Second) for { select { case <-tc.C: // 上報狀態(tài) log.Println("status update success") case <-ctx.Done(): // server closed, return tc.Stop() log.Println("stop update success") return } } }()
示例倉庫中還有一個更復(fù)雜的利用context.Context
退出子goroutine的例子
?? 全貌
結(jié)合上面的所有的細(xì)節(jié),一個優(yōu)雅關(guān)閉的http server代碼如下
func registerService(ctx context.Context) { tc := time.NewTicker(5 * time.Second) for { select { case <-tc.C: // 上報狀態(tài) log.Println("status update success") case <-ctx.Done(): tc.Stop() log.Println("stop update success") return } } } func destroyService() { log.Println("destroy success") } func gracefulShutdown() { mainCtx, mainCancel := context.WithCancel(context.Background()) // 用ctx初始化資源,mysql,redis等 // ... defer func() { mainCancel() // 主動注銷服務(wù) destroyService() // 清理資源,mysql,redis等 // ... }() mx := http.NewServeMux() mx.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { time.Sleep(time.Duration(rand.Intn(10)) * time.Second) w.Write([]byte("Receive path foo\n")) }) srv := http.Server{ Addr: ":8009", Handler: mx, } // ListenAndServe也會阻塞,需要把它放到一個goroutine中 go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { panic(err) } }() // 需要在服務(wù)啟動后才在注冊中心注冊 go registerService(mainCtx) signalCh := make(chan os.Signal, 1) signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM) // 等待信號 sig := <-signalCh log.Printf("Received signal: %v\n", sig) ctxTimeout, cancelTimeout := context.WithTimeout(context.Background(), 5*time.Second) defer cancelTimeout() if err := srv.Shutdown(ctxTimeout); err != nil { select { case <-ctxTimeout.Done(): // 由于達(dá)到超時時間服務(wù)器關(guān)閉,未完成優(yōu)雅關(guān)閉 log.Println("timeout of 5 seconds.") default: // 其他原因?qū)е碌姆?wù)關(guān)閉異常,未完成優(yōu)雅關(guān)閉 log.Fatalf("Server shutdown failed: %v\n", err) } return } // 正確執(zhí)行優(yōu)雅關(guān)閉服務(wù)器 log.Println("Server shutdown gracefully") }
以上就是Go標(biāo)準(zhǔn)庫http server的優(yōu)雅關(guān)閉深入理解的詳細(xì)內(nèi)容,更多關(guān)于Go標(biāo)準(zhǔn)庫http server關(guān)閉的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go string轉(zhuǎn)int,int64,int32及注意事項說明
這篇文章主要介紹了Go string轉(zhuǎn)int,int64,int32及注意事項說明,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07go語言阻塞函數(shù)和非阻塞函數(shù)實現(xiàn)
本文主要介紹了go語言阻塞函數(shù)和非阻塞函數(shù)實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03