一文講懂 Go 服務(wù)的優(yōu)雅重啟和更新
在服務(wù)端程序更新或重啟時(shí),如果我們直接 kill -9 殺掉舊進(jìn)程并啟動(dòng)新進(jìn)程,會(huì)有以下幾個(gè)問(wèn)題:
舊的請(qǐng)求未處理完,如果服務(wù)端進(jìn)程直接退出,會(huì)造成客戶端鏈接中斷(收到 RST)新請(qǐng)求打過(guò)來(lái),服務(wù)還沒(méi)重啟完畢,造成 connection refused即使是要退出程序,直接 kill -9仍然會(huì)讓正在處理的請(qǐng)求中斷
很直接的感受就是:在重啟過(guò)程中,會(huì)有一段時(shí)間不能給用戶提供正常服務(wù);同時(shí)粗魯關(guān)閉服務(wù),也可能會(huì)對(duì)業(yè)務(wù)依賴的數(shù)據(jù)庫(kù)等狀態(tài)服務(wù)造成污染。
所以我們服務(wù)重啟或者是重新發(fā)布過(guò)程中,要做到新舊服務(wù)無(wú)縫切換,同時(shí)可以保障變更服務(wù) 零宕機(jī)時(shí)間!
作為一個(gè)微服務(wù)框架,那 go-zero 是怎么幫開(kāi)發(fā)者做到優(yōu)雅退出的呢?下面我們一起看看。
優(yōu)雅退出
在實(shí)現(xiàn)優(yōu)雅重啟之前首先需要解決的一個(gè)問(wèn)題是 如何優(yōu)雅退出:
對(duì) http 服務(wù)來(lái)說(shuō),一般的思路就是關(guān)閉對(duì)
fd的listen, 確保不會(huì)有新的請(qǐng)求進(jìn)來(lái)的情況下處理完已經(jīng)進(jìn)入的請(qǐng)求, 然后退出。
go 原生中 http 中提供了 server.ShutDown(),先來(lái)看看它是怎么實(shí)現(xiàn)的:
設(shè)置 inShutdown標(biāo)志關(guān)閉 listeners保證不會(huì)有新請(qǐng)求進(jìn)來(lái)等待所有活躍鏈接變成空閑狀態(tài) 退出函數(shù),結(jié)束
分別來(lái)解釋一下這幾個(gè)步驟的含義:
inShutdown
func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
....
// 實(shí)際監(jiān)聽(tīng)端口;生成一個(gè) listener
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
// 進(jìn)行實(shí)際邏輯處理,并將該 listener 注入
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
func (s *Server) shuttingDown() bool {
return atomic.LoadInt32(&s.inShutdown) != 0
}
ListenAndServe 是http啟動(dòng)服務(wù)器的必經(jīng)函數(shù),里面的第一句就是判斷 Server 是否被關(guān)閉了。
inShutdown 就是一個(gè)原子變量,非0表示被關(guān)閉。
listeners
func (srv *Server) Serve(l net.Listener) error {
...
// 將注入的 listener 加入內(nèi)部的 map 中
// 方便后續(xù)控制從該 listener 鏈接到的請(qǐng)求
if !srv.trackListener(&l, true) {
return ErrServerClosed
}
defer srv.trackListener(&l, false)
...
}
Serve 中注冊(cè)到內(nèi)部 listeners map 中 listener,在 ShutDown 中就可以直接從 listeners 中獲取到,然后執(zhí)行 listener.Close(),TCP四次揮手后,新的請(qǐng)求就不會(huì)進(jìn)入了。
closeIdleConns
簡(jiǎn)單來(lái)說(shuō)就是:將目前 Server 中記錄的活躍鏈接變成變成空閑狀態(tài),返回。
關(guān)閉
func (srv *Server) Serve(l net.Listener) error {
...
for {
rw, err := l.Accept()
// 此時(shí) accept 會(huì)發(fā)生錯(cuò)誤,因?yàn)榍懊嬉呀?jīng)將 listener close了
if err != nil {
select {
// 又是一個(gè)標(biāo)志:doneChan
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
}
}
}
其中 getDoneChan 中已經(jīng)在前面關(guān)閉 listener 時(shí),對(duì) doneChan 這個(gè)channel中push。
總結(jié)一下:Shutdown 可以優(yōu)雅的終止服務(wù),期間不會(huì)中斷已經(jīng)活躍的鏈接。
但服務(wù)啟動(dòng)后的某一時(shí)刻,程序如何知道服務(wù)被中斷了呢?服務(wù)被中斷時(shí)如何通知程序,然后調(diào)用Shutdown作處理呢?接下來(lái)看一下系統(tǒng)信號(hào)通知函數(shù)的作用
服務(wù)中斷
這個(gè)時(shí)候就要依賴 OS 本身提供的 signal。對(duì)應(yīng) go 原生來(lái)說(shuō),signal 的 Notify 提供系統(tǒng)信號(hào)通知的能力。
https://github.com/tal-tech/go-zero/blob/master/core/proc/signals.go
func init() {
go func() {
var profiler Stopper
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGTERM)
for {
v := <-signals
switch v {
case syscall.SIGUSR1:
dumpGoroutines()
case syscall.SIGUSR2:
if profiler == nil {
profiler = StartProfile()
} else {
profiler.Stop()
profiler = nil
}
case syscall.SIGTERM:
// 正在執(zhí)行優(yōu)雅關(guān)閉的地方
gracefulStop(signals)
default:
logx.Error("Got unregistered signal:", v)
}
}
}()
}
SIGUSR1-> 將goroutine狀況,dump下來(lái),這個(gè)在做錯(cuò)誤分析時(shí)還挺有用的SIGUSR2-> 開(kāi)啟/關(guān)閉所有指標(biāo)監(jiān)控,自行控制 profiling 時(shí)長(zhǎng)SIGTERM-> 真正開(kāi)啟gracefulStop,優(yōu)雅關(guān)閉
而 gracefulStop 的流程如下:
取消監(jiān)聽(tīng)信號(hào),畢竟要退出了,不需要重復(fù)監(jiān)聽(tīng)了 wrap up,關(guān)閉目前服務(wù)請(qǐng)求,以及資源time.Sleep(),等待資源處理完成,以后關(guān)閉完成shutdown,通知退出如果主goroutine還沒(méi)有退出,則主動(dòng)發(fā)送 SIGKILL 退出進(jìn)程
這樣,服務(wù)不再接受新的請(qǐng)求,服務(wù)活躍的請(qǐng)求等待處理完成,同時(shí)也等待資源關(guān)閉(數(shù)據(jù)庫(kù)連接等),如有超時(shí),強(qiáng)制退出。
整體流程
我們目前 go 程序都是在 docker 容器中運(yùn)行,所以在服務(wù)發(fā)布過(guò)程中,k8s 會(huì)向容器發(fā)送一個(gè) SIGTERM 信號(hào),然后容器中程序接收到信號(hào),開(kāi)始執(zhí)行 ShutDown:

到這里,整個(gè)優(yōu)雅關(guān)閉的流程就梳理完畢了。
但是還有平滑重啟,這個(gè)就依賴 k8s 了,基本流程如下:
old pod未退出之前,先啟動(dòng)new podold pod繼續(xù)處理完已經(jīng)接受的請(qǐng)求,并且不再接受新請(qǐng)求new pod接受并處理新請(qǐng)求的方式old pod退出
這樣整個(gè)服務(wù)重啟就算是成功了,如果 new pod 沒(méi)有啟動(dòng)成功,old pod 也可以提供服務(wù),不會(huì)對(duì)目前線上的服務(wù)造成影響。
推薦閱讀
