在服務(wù)端程序更新或重啟時,如果我們直接 kill -9
殺掉舊進程并啟動新進程有咨,會有以下幾個問題:
- 舊的請求未處理完寒波,如果服務(wù)端進程直接退出,會造成客戶端鏈接中斷(收到
RST
) - 新請求打過來螟凭,服務(wù)還沒重啟完畢,造成
connection refused
- 即使是要退出程序它呀,直接
kill -9
仍然會讓正在處理的請求中斷
很直接的感受就是:在重啟過程中螺男,會有一段時間不能給用戶提供正常服務(wù);同時粗魯關(guān)閉服務(wù)钟些,也可能會對業(yè)務(wù)依賴的數(shù)據(jù)庫等狀態(tài)服務(wù)造成污染烟号。
所以我們服務(wù)重啟或者是重新發(fā)布過程中,要做到新舊服務(wù)無縫切換政恍,同時可以保障變更服務(wù) 零宕機時間汪拥!
作為一個微服務(wù)框架,那 go-zero
是怎么幫開發(fā)者做到優(yōu)雅退出的呢篙耗?下面我們一起看看迫筑。
優(yōu)雅退出
在實現(xiàn)優(yōu)雅重啟之前首先需要解決的一個問題是 如何優(yōu)雅退出:
對 http 服務(wù)來說,一般的思路就是關(guān)閉對
fd
的listen
, 確保不會有新的請求進來的情況下處理完已經(jīng)進入的請求, 然后退出宗弯。
go 原生中 http
中提供了 server.ShutDown()
脯燃,先來看看它是怎么實現(xiàn)的:
- 設(shè)置
inShutdown
標志 - 關(guān)閉
listeners
保證不會有新請求進來 - 等待所有活躍鏈接變成空閑狀態(tài)
- 退出函數(shù),結(jié)束
分別來解釋一下這幾個步驟的含義:
inShutdown
func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
....
// 實際監(jiān)聽端口蒙保;生成一個 listener
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
// 進行實際邏輯處理辕棚,并將該 listener 注入
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
func (s *Server) shuttingDown() bool {
return atomic.LoadInt32(&s.inShutdown) != 0
}
ListenAndServe
是http啟動服務(wù)器的必經(jīng)函數(shù),里面的第一句就是判斷 Server
是否被關(guān)閉了。
inShutdown
就是一個原子變量逝嚎,非0表示被關(guān)閉扁瓢。
listeners
func (srv *Server) Serve(l net.Listener) error {
...
// 將注入的 listener 加入內(nèi)部的 map 中
// 方便后續(xù)控制從該 listener 鏈接到的請求
if !srv.trackListener(&l, true) {
return ErrServerClosed
}
defer srv.trackListener(&l, false)
...
}
Serve
中注冊到內(nèi)部 listeners map
中 listener
,在 ShutDown
中就可以直接從 listeners
中獲取到补君,然后執(zhí)行 listener.Close()
引几,TCP四次揮手后,新的請求就不會進入了挽铁。
closeIdleConns
簡單來說就是:將目前 Server
中記錄的活躍鏈接變成變成空閑狀態(tài)伟桅,返回。
關(guān)閉
func (srv *Server) Serve(l net.Listener) error {
...
for {
rw, err := l.Accept()
// 此時 accept 會發(fā)生錯誤叽掘,因為前面已經(jīng)將 listener close了
if err != nil {
select {
// 又是一個標志:doneChan
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
}
}
}
其中 getDoneChan
中已經(jīng)在前面關(guān)閉 listener
時楣铁,對 doneChan
這個channel中push。
總結(jié)一下:Shutdown
可以優(yōu)雅的終止服務(wù)够掠,期間不會中斷已經(jīng)活躍的鏈接民褂。
但服務(wù)啟動后的某一時刻,程序如何知道服務(wù)被中斷了呢疯潭?服務(wù)被中斷時如何通知程序,然后調(diào)用Shutdown作處理呢面殖?接下來看一下系統(tǒng)信號通知函數(shù)的作用
服務(wù)中斷
這個時候就要依賴 OS 本身提供的 signal
竖哩。對應(yīng) go 原生來說,signal
的 Notify
提供系統(tǒng)信號通知的能力脊僚。
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下來,這個在做錯誤分析時還挺有用的SIGUSR2
-> 開啟/關(guān)閉所有指標監(jiān)控辽幌,自行控制 profiling 時長SIGTERM
-> 真正開啟gracefulStop
增淹,優(yōu)雅關(guān)閉
而 gracefulStop
的流程如下:
- 取消監(jiān)聽信號,畢竟要退出了乌企,不需要重復監(jiān)聽了
-
wrap up
虑润,關(guān)閉目前服務(wù)請求,以及資源 -
time.Sleep()
加酵,等待資源處理完成拳喻,以后關(guān)閉完成 -
shutdown
,通知退出 - 如果主goroutine還沒有退出猪腕,則主動發(fā)送 SIGKILL 退出進程
這樣冗澈,服務(wù)不再接受新的請求,服務(wù)活躍的請求等待處理完成陋葡,同時也等待資源關(guān)閉(數(shù)據(jù)庫連接等)亚亲,如有超時,強制退出。
整體流程
我們目前 go 程序都是在 docker
容器中運行捌归,所以在服務(wù)發(fā)布過程中肛响,k8s
會向容器發(fā)送一個 SIGTERM
信號,然后容器中程序接收到信號陨溅,開始執(zhí)行 ShutDown
:
到這里终惑,整個優(yōu)雅關(guān)閉的流程就梳理完畢了。
但是還有平滑重啟门扇,這個就依賴 k8s
了雹有,基本流程如下:
-
old pod
未退出之前,先啟動new pod
-
old pod
繼續(xù)處理完已經(jīng)接受的請求臼寄,并且不再接受新請求 -
new pod
接受并處理新請求的方式 -
old pod
退出
這樣整個服務(wù)重啟就算是成功了霸奕,如果 new pod
沒有啟動成功,old pod
也可以提供服務(wù)吉拳,不會對目前線上的服務(wù)造成影響质帅。
項目地址
https://github.com/tal-tech/go-zero
歡迎使用 go-zero 并 star 支持我們!
微信交流群
關(guān)注『微服務(wù)實踐』公眾號并點擊 交流群 獲取社區(qū)群二維碼留攒。