gRPC的平滑關閉和在Kubernetes上的服務摘流方案總結

平滑關閉服務摘流是保證部署了多節點的應用可以持續穩定對外提供服務的兩個重要手段,平滑關閉保證了應用節點在關閉以前處理完已接收到的請求,之前在文章「學習用Go編寫HTTP服務」裏給你們介紹過怎麼用net/http庫提供的 http.ShutDown平滑關停HTTP 服務,今天再給你們介紹一下gRPC分佈式服務的平滑關停方法。應用在進入平滑關閉階段後拒絕爲新進來的流量提供服務,若是此時繼續有新流量訪問而來,勢必會讓發送請求的客戶端感知到服務的斷開,因此在平滑關閉應用前咱們還要對應用節點作摘流操做,保證網關不會再把新流量分發到要關閉的應用節點上才行。編程

若是服務部署在雲主機上,摘流只須要運維人員從負載均衡上把機器節點的IP拿掉,待應用重啓或者更新完畢後再將機器節點的IP掛回負載均衡上便可。可是人工摘流這對 Kubernetes 這樣的進行多節點集羣資源調度的系統顯然是不可能的,因此咱們在文章裏還會介紹如何讓 Kubernetes 系統自動爲咱們即將關停的應用節點完成摘流操做。bash

平滑關閉

在這個章節裏除了介紹 gRPC框架平滑關閉應用的方法外還會介紹一下Kubernetes集羣裏完成Pod刪除的整個生命週期,由於若是咱們的gRPC服務部署在Kubernetes集羣裏的話,服務的平滑關閉和摘流都會依賴這個Pod 刪除的生命週期,或者叫Pod關閉序列來實現。若是在下面內容裏看到我一下子說 「Pod 的關閉序列」,一下子說「 Pod 刪除的生命週期」請必定要記住他們是一個東西的兩種表述方式而已。markdown

gRPC的gracefulStop

gRPC 框架使用的通訊協議是HTTP2HTTP2對於鏈接關閉使用 goaway 幀信號(類型是0x7,用於啓動鏈接關閉或發出嚴重錯誤狀態信號)。goaway 容許服務端點正常中止接受新的流量,同時仍然完成對先前已創建的流的處理。app

Go 語言版本的 gRPC Server 提供了兩個退出方法StopGracefulStop,光看名字就知道後面這個是實現平滑關閉用的。GracefulStop 方法裏首先會關閉服務監聽,這樣就沒法再創建新的請求,而後會遍歷全部的當前鏈接發送goaway幀信號。serveWG.Wait()會等待全部handleRawConn協程的退出(在gRPC Server裏每一個新鏈接都會建立一個handleRawConn協程,而且增長WaitGroup的計數器的計數)。負載均衡

func (s *Server) GracefulStop() {
    s.mu.Lock()
    ...

    // 關閉監聽,再也不接收新的鏈接請求
    for lis := range s.lis {
        lis.Close()
    }

    s.lis = nil
    if !s.drain {
        for st := range s.conns {
            // 給全部的鏈接發佈goaway信號
            st.Drain()  
        }
        s.drain = true
    }


    // 等待全部handleRawConn協程退出,每一個請求都是一個goroutine,經過WaitGroup控制.
    s.serveWG.Wait()

    // 當還有空閒鏈接時,須要等待。在退出serveStreams邏輯時,會進行Broadcast喚醒。只要有一個客戶端退出就會觸發removeConn繼而進行喚醒。
    for len(s.conns) != 0 {
        s.cv.Wait()
    }
...
複製代碼

Stop 方法相對於 GracefulStop 來講少了給鏈接發送 goaway 幀信號和等待鏈接退出的邏輯,這裏就再也不作過多介紹了。框架

應用監聽OS信號,啓動平滑關閉

知道 gRPC框架提供的服務平滑關閉的方法後,與HTTP服務的平滑關閉同樣,咱們的應用要能接收到OS發來的TERMInterrupt之類的信號,而後主動去觸發調用GracefulStop進行服務的平滑關閉,固然調用平滑關閉前咱們還能夠作一些其餘應用內的首尾工做,好比應用使用Etcd實現的服務註冊,那麼這裏我建議要先去主動的把節點的IP對應的Key從Etcd上註銷掉,若是Key不能及時過時,那麼客戶端作負載均衡時沒有收到這個節點IP刪除的通知就仍有可能會往要關閉的端點上發請求。運維

下面是gRPC服務啓動後監聽 OS 發來的斷開信號時開始平滑關閉的方法,演示的代碼只是一些僞代碼,不過真實度已經很高了,實際應用時能夠直接往這個代碼模板裏套用本身的方法。分佈式

errChan := make(chan error)

stopChan := make(chan os.Signal)

signal.Notify(stopChan, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL)

go func() {
     if err := grpcServer.Serve(lis); err != nil {
        errChan <- err
     }
}()

select {
case err := <- errChan:
   panic(err)
case <-stopChan:
   // TODO 作一些項目本身的清理工做
   DoSomeCleanJob()
   // 平滑關閉服務
   grpcServer.GracefulStop()
}
複製代碼

Kubernetes Pod關閉經歷生命週期

Kubernetes 在應用須要更新、節點須要升級維護、節點資源耗盡的時候都會刪除Pod再從新建立和調度Pod,Pod 在Kubernetes集羣中被刪除前會經歷如下生命週期:ide

  1. Pod 狀態被標記爲Terminating, 此時 Pod 開始中止接收新流量。
  2. Pod 的 preStop 鉤子會被執行,在鉤子裏咱們能夠設置要執行的命令或者要發送的HTTP請求,大部分應用能夠處理OS發來的TERM中斷信號,可是若是應用依賴了不受自主控制的外部系統,能夠經過鉤子裏發送請求完成註銷之類的動做,後面介紹的服務摘流也會用到 preStop鉤子。
  3. Kubernetes向Pod 發送 SIGTERM 信號。
  4. Kubernetes 會默認等待30秒讓Pod完成關閉,若是須要等待超過30秒應用才能正常退出,可使用terminationGracePeriodSeconds 在Deployment裏本身配置平滑關閉Pod Kubernetes要等待的時間。須要注意的是上面說的 preStop 鉤子的執行和 SIGTERM 信號的發送都包含在這個時間裏,若是應用早於這個時間關閉會當即進入生命週期的下個階段
  5. Kubernetes嚮應用發送 SIGKILL 信號,而後刪除Pod。

上面那個 gRPC 服務,部署在Kubernetes集羣裏後,假如遇到節點升級或者其餘要關閉某個節點上Pod的狀況,應用就能夠收到Kubernetes 向Pod發送的TERM信號,主動完成平滑關閉服務的操做。oop

關於Pod關閉所經歷的生命週期更詳細的內容能夠看一看我最近寫的文章「如何優雅地關閉Kubernetes集羣中的Pod

Kubernetes服務摘流

提及Kubernetes的服務摘流,咱們就不得再也不把Kubernetes裏的Pod和Service這種資源的概念再簡單捋一遍。咱們的應用服務運行在容器裏,容器被 Kubernetes 封裝在Pod裏,Pod裏能夠有多個容器,但只能有一個運行主進程的主容器,其餘容器都是輔助用的,即Pod 支持的(sidecar)邊車模式。

Pod 自身每次重建IP都會變,且Pod自身的IP只能在節點內訪問,因此 Kubernetes 就用了一種叫作的 Service 的控制器來管控一組Pod,爲它們向外部提供統一的訪問方式,Service 它經過selector 指定 Pod的標籤來把符合條件的Pod 都加到它的服務端點列表裏。

因此咱們應用啓動後向註冊中心註冊的IP,不是應用所在的Pod的IP,而是上層 NodePort 類型的Service的IP,這個IP是VIP,訪問它的時候會自動作負載均衡,隨機把流量路由給Service後面掛的Pod。

若是你以前對Pod 和 Service的概念瞭解的較少,能夠看下我以前的文章,就能理解上面說的這些東西了、

Kubernetes Pod入門指南

學練結合,快速掌握Kubernetes Service

Service 自己實際上是會爲Pod作探活和摘流的,可是若是你的應用的訪問量足夠大,Service的摘流有時候並不及時,在Pod 關閉的時候仍是會有新流量進來。這就致使了在重啓服務,或者是Kubernetes集羣內部有一個節點升級、重啓之類的動做,節點上的Pod被調度到其餘節點上時,客戶端仍是能感知到閃斷。這實際上是一個很大的問題,由於 Kubernetes 集羣內部作資源從新調度,切換新節點之類的動做仍是挺常見的。通過翻看Kubernetes相關的資料和一些社區裏的討論,咱們終於找到了Service摘流慢的緣由(其實沒費多大勁,Github和Kubernetes In Action 那本書裏都有說過這個問題)。

緣由是 Kubernetes 刪除 Pod 前會向 Kubernetes 集羣內廣播 Pod 的刪除事件,會同時有幾個子系統接收廣播處理事件,其中就包括:

  • 要刪除的 Pod 所在節點上的Kubelet接收到事件後會開啓上面介紹的Pod 關閉的生命週期,Pod拒絕伺服新流量等待生命週期內的動做執行完成後被刪除。
  • Pod 的 Service 控制器收到事件後會把要關閉的 Pod 從服務端點列表裏移除出去。

上面動做會同時並行發生,這就致使了有可能Pod已經進入關閉序列了,可是Service那裏尚未作完摘流,Service仍是有可能會把新來的流量路由給要關閉的Pod上。

社區裏和Kubernetes In Action 這本書裏針對這個問題,都給出了一個相同的解決方案。利用 Pod 關閉生命週期裏的preStop 鉤子,讓其執行 sleep 命令休眠5~10秒,經過延遲關閉Pod來讓Service先完成摘流,preStop的執行並不影響Pod內應用繼續處理現存請求

在 preStop 鉤子裏引入延遲的方法,能夠參考下面的配置文件片斷。

containers:
  - args:
  - /bin/bash
  - -c
  - /go-big-app
  ... 
  # 下面的preStop鉤子裏引入10秒延遲
  lifecycle:
    preStop:
      exec:
        command:
          - sh
          - -c
          - sleep 10
複製代碼

這樣就讓並行執行的摘流和平滑關閉動做在時間線上儘可能錯開了,也就不會出現Service摘流可能會有延遲的問題了。

關於這個問題詳細的描述和解決方案能夠參考我前面翻譯的文章「藉助 Pod 刪除事件的傳播實現 Pod 摘流」,裏面有詳細的圖文解釋來講明這個問題的由來和解決辦法。

總結

文章裏講的這些內容算是咱們研發團隊以前作服務高可用保證時總結出來的一些經驗吧,裏面介紹的知識點,單看每一個在它本身的領域其實都不是什麼難掌握的東西,用Go開發的基本上都會用signal.Notify接收OS信號完成應用的平滑關閉,作 Kubernetes 運維的對後面那些概念和解決問題的方法應該也都是輕車熟路,可是做爲研發若是咱們能"跨界"多學一些像Kubernetes這樣的在生態裏和程序開發緊密結合的知識,靠研發主動推進運維配合咱們解決這些問題,所得到的經驗和成就感仍是挺不同。

閒話也很少說了,對Go編程實踐和進階還有Kubernetes感興趣的同窗能夠訪問個人公衆號『網管叨bi叨』進入公衆號在菜單欄裏便可訪問這些專題的原創文章。但願這些通俗易懂的文章能幫助到一塊兒努力的同路人。

圖片

圖片

圖片

相關文章
相關標籤/搜索