一文講懂服務(wù)的優(yōu)雅重啟和更新

在服務(wù)端程序更新或重啟時,如果我們直接 kill -9 殺掉舊進程并啟動新進程有咨,會有以下幾個問題:

  1. 舊的請求未處理完寒波,如果服務(wù)端進程直接退出,會造成客戶端鏈接中斷(收到 RST
  2. 新請求打過來螟凭,服務(wù)還沒重啟完畢,造成 connection refused
  3. 即使是要退出程序它呀,直接 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)閉對 fdlisten , 確保不會有新的請求進來的情況下處理完已經(jīng)進入的請求, 然后退出宗弯。

go 原生中 http 中提供了 server.ShutDown()脯燃,先來看看它是怎么實現(xiàn)的:

  1. 設(shè)置 inShutdown 標志
  2. 關(guān)閉 listeners 保證不會有新請求進來
  3. 等待所有活躍鏈接變成空閑狀態(tài)
  4. 退出函數(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 maplistener,在 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 原生來說,signalNotify 提供系統(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 的流程如下:

  1. 取消監(jiān)聽信號,畢竟要退出了乌企,不需要重復監(jiān)聽了
  2. wrap up虑润,關(guān)閉目前服務(wù)請求,以及資源
  3. time.Sleep() 加酵,等待資源處理完成拳喻,以后關(guān)閉完成
  4. shutdown ,通知退出
  5. 如果主goroutine還沒有退出猪腕,則主動發(fā)送 SIGKILL 退出進程

這樣冗澈,服務(wù)不再接受新的請求,服務(wù)活躍的請求等待處理完成陋葡,同時也等待資源關(guān)閉(數(shù)據(jù)庫連接等)亚亲,如有超時,強制退出。

整體流程

我們目前 go 程序都是在 docker 容器中運行捌归,所以在服務(wù)發(fā)布過程中肛响,k8s 會向容器發(fā)送一個 SIGTERM 信號,然后容器中程序接收到信號陨溅,開始執(zhí)行 ShutDown

image

到這里终惑,整個優(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ū)群二維碼留攒。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末煤惩,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子炼邀,更是在濱河造成了極大的恐慌魄揉,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拭宁,死亡現(xiàn)場離奇詭異洛退,居然都是意外死亡,警方通過查閱死者的電腦和手機杰标,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門兵怯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人腔剂,你說我怎么就攤上這事媒区。” “怎么了桶蝎?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵驻仅,是天一觀的道長。 經(jīng)常有香客問我登渣,道長噪服,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任胜茧,我火速辦了婚禮粘优,結(jié)果婚禮上仇味,老公的妹妹穿的比我還像新娘。我一直安慰自己雹顺,他們只是感情好丹墨,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著嬉愧,像睡著了一般贩挣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上没酣,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天王财,我揣著相機與錄音,去河邊找鬼裕便。 笑死绒净,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的偿衰。 我是一名探鬼主播挂疆,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼下翎!你這毒婦竟也來了缤言?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤视事,失蹤者是張志新(化名)和其女友劉穎墨闲,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體郑口,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年盾鳞,在試婚紗的時候發(fā)現(xiàn)自己被綠了犬性。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡腾仅,死狀恐怖乒裆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情推励,我是刑警寧澤鹤耍,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站验辞,受9級特大地震影響稿黄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜跌造,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一杆怕、第九天 我趴在偏房一處隱蔽的房頂上張望族购。 院中可真熱鬧,春花似錦陵珍、人聲如沸寝杖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瑟幕。三九已至,卻和暖如春留潦,著一層夾襖步出監(jiān)牢的瞬間只盹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工愤兵, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鹿霸,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓秆乳,卻偏偏與公主長得像懦鼠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子屹堰,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容