一文講懂服務(wù)的優(yōu)雅重啟和更新
本文轉(zhuǎn)載自微信公眾號「微服務(wù)實(shí)踐」,作者h(yuǎn)xl。轉(zhuǎn)載本文請聯(lián)系微服務(wù)實(shí)踐公眾號。
在服務(wù)端程序更新或重啟時(shí),如果我們直接 kill -9 殺掉舊進(jìn)程并啟動新進(jìn)程,會有以下幾個(gè)問題:
- 舊的請求未處理完,如果服務(wù)端進(jìn)程直接退出,會造成客戶端鏈接中斷(收到 RST)
- 新請求打過來,服務(wù)還沒重啟完畢,造成 connection refused
- 即使是要退出程序,直接 kill -9 仍然會讓正在處理的請求中斷
很直接的感受就是:在重啟過程中,會有一段時(shí)間不能給用戶提供正常服務(wù);同時(shí)粗魯關(guān)閉服務(wù),也可能會對業(yè)務(wù)依賴的數(shù)據(jù)庫等狀態(tài)服務(wù)造成污染。
所以我們服務(wù)重啟或者是重新發(fā)布過程中,要做到新舊服務(wù)無縫切換,同時(shí)可以保障變更服務(wù) 零宕機(jī)時(shí)間!
作為一個(gè)微服務(wù)框架,那 go-zero 是怎么幫開發(fā)者做到優(yōu)雅退出的呢?下面我們一起看看。
優(yōu)雅退出
在實(shí)現(xiàn)優(yōu)雅重啟之前首先需要解決的一個(gè)問題是 如何優(yōu)雅退出:
對 http 服務(wù)來說,一般的思路就是關(guān)閉對 fd 的 listen , 確保不會有新的請求進(jìn)來的情況下處理完已經(jīng)進(jìn)入的請求, 然后退出。
go 原生中 http 中提供了 server.ShutDown(),先來看看它是怎么實(shí)現(xiàn)的:
- 設(shè)置 inShutdown 標(biāo)志
- 關(guān)閉 listeners 保證不會有新請求進(jìn)來
- 等待所有活躍鏈接變成空閑狀態(tài)
- 退出函數(shù),結(jié)束
分別來解釋一下這幾個(gè)步驟的含義:
inShutdown
- func (srv *Server) ListenAndServe() error {
- if srv.shuttingDown() {
- return ErrServerClosed
- }
- ....
- // 實(shí)際監(jiān)聽端口;生成一個(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啟動服務(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 鏈接到的請求
- if !srv.trackListener(&l, true) {
- return ErrServerClosed
- }
- defer srv.trackListener(&l, false)
- ...
- }
Serve 中注冊到內(nèi)部 listeners map 中 listener,在 ShutDown 中就可以直接從 listeners 中獲取到,然后執(zhí)行 listener.Close(),TCP四次揮手后,新的請求就不會進(jìn)入了。
closeIdleConns
簡單來說就是:將目前 Server 中記錄的活躍鏈接變成變成空閑狀態(tài),返回。
關(guān)閉
- func (srv *Server) Serve(l net.Listener) error {
- ...
- for {
- rw, err := l.Accept()
- // 此時(shí) accept 會發(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í),對 doneChan 這個(gè)channel中push。
總結(jié)一下:Shutdown 可以優(yōu)雅的終止服務(wù),期間不會中斷已經(jīng)活躍的鏈接。
但服務(wù)啟動后的某一時(shí)刻,程序如何知道服務(wù)被中斷了呢?服務(wù)被中斷時(shí)如何通知程序,然后調(diào)用Shutdown作處理呢?接下來看一下系統(tǒng)信號通知函數(shù)的作用
服務(wù)中斷
這個(gè)時(shí)候就要依賴 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下來,這個(gè)在做錯(cuò)誤分析時(shí)還挺有用的
- SIGUSR2 -> 開啟/關(guān)閉所有指標(biāo)監(jiān)控,自行控制 profiling 時(shí)長
- SIGTERM -> 真正開啟 gracefulStop,優(yōu)雅關(guān)閉
而 gracefulStop 的流程如下:
- 取消監(jiān)聽信號,畢竟要退出了,不需要重復(fù)監(jiān)聽了
- wrap up,關(guān)閉目前服務(wù)請求,以及資源
- time.Sleep() ,等待資源處理完成,以后關(guān)閉完成
- shutdown ,通知退出
- 如果主goroutine還沒有退出,則主動發(fā)送 SIGKILL 退出進(jìn)程
這樣,服務(wù)不再接受新的請求,服務(wù)活躍的請求等待處理完成,同時(shí)也等待資源關(guān)閉(數(shù)據(jù)庫連接等),如有超時(shí),強(qiáng)制退出。
整體流程
我們目前 go 程序都是在 docker 容器中運(yùn)行,所以在服務(wù)發(fā)布過程中,k8s 會向容器發(fā)送一個(gè) SIGTERM 信號,然后容器中程序接收到信號,開始執(zhí)行 ShutDown:
到這里,整個(gè)優(yōu)雅關(guān)閉的流程就梳理完畢了。
但是還有平滑重啟,這個(gè)就依賴 k8s 了,基本流程如下:
- old pod 未退出之前,先啟動 new pod
- old pod 繼續(xù)處理完已經(jīng)接受的請求,并且不再接受新請求
- new pod接受并處理新請求的方式
- old pod 退出
這樣整個(gè)服務(wù)重啟就算是成功了,如果 new pod 沒有啟動成功,old pod 也可以提供服務(wù),不會對目前線上的服務(wù)造成影響。
項(xiàng)目地址
https://github.com/tal-tech/go-zero
歡迎使用 go-zero 并 star 支持我們!