Kubernetes 中如何保證優雅地中止 Pod

做者:吳葉磊git

一直以來我對優雅地中止 Pod 這件事理解得很單純:不就利用是 PreStop hook 作優雅退出嗎?但最近發現不少場景下 PreStop Hook 並不能很好地完成需求,這篇文章就簡單分析一下「優雅地中止 Pod」這回事兒。github

何謂優雅中止?

優雅中止(Graceful shutdown)這個說法來自於操做系統,咱們執行關機以後都得 OS 先完成一些清理操做,而與之相對的就是硬停止(Hard shutdown),好比拔電源。web

到了分佈式系統中,優雅中止就不只僅是單機上進程本身的事了,每每還要與系統中的其它組件打交道。好比說咱們起一個微服務,網關把一部分流量分給咱們,這時:數據庫

  • 假如咱們一聲不吭直接把進程殺了,那這部分流量就沒法獲得正確處理,部分用戶受到影響。不過還好,一般來講網關或者服務註冊中心會和咱們的服務保持一個心跳,過了心跳超時以後系統會自動摘除咱們的服務,問題也就解決了;這是硬停止,雖然咱們整個系統寫得不錯可以自愈,但仍是會產生一些抖動甚至錯誤。
  • 假如咱們先告訴網關或服務註冊中心咱們要下線,等對方完成服務摘除操做再停止進程,那不會有任何流量受到影響;這是優雅中止,將單個組件的啓停對整個系統影響最小化。

按照慣例,SIGKILL 是硬終止的信號,而 SIGTERM 是通知進程優雅退出的信號,所以不少微服務框架會監聽 SIGTERM 信號,收到以後去作反註冊等清理操做,實現優雅退出。api

PreStop Hook

回到 Kubernetes(下稱 K8s),當咱們想幹掉一個 Pod 的時候,理想情況固然是 K8s 從對應的 Service(假若有的話)把這個 Pod 摘掉,同時給 Pod 發 SIGTERM 信號讓 Pod 中的各個容器優雅退出就好了。但實際上 Pod 有可能犯各類幺蛾子:安全

  • 已經卡死了,處理不了優雅退出的代碼邏輯或須要好久才能處理完成。
  • 優雅退出的邏輯有 BUG,本身死循環了。
  • 代碼寫得野,根本不理會 SIGTERM。

所以,K8s 的 Pod 終止流程中還有一個「最多能夠容忍的時間」,即 grace period(在 Pod 的 .spec.terminationGracePeriodSeconds 字段中定義),這個值默認是 30 秒,咱們在執行 kubectl delete 的時候也可經過 --grace-period 參數顯式指定一個優雅退出時間來覆蓋 Pod 中的配置。而當 grace period 超出以後,K8s 就只能選擇 SIGKILL 強制幹掉 Pod 了。網絡

不少場景下,除了把 Pod 從 K8s 的 Service 上摘下來以及進程內部的優雅退出以外,咱們還必須作一些額外的事情,好比說從 K8s 外部的服務註冊中心上反註冊。這時就要用到 PreStop Hook 了,K8s 目前提供了 Exec 和 HTTP 兩種 PreStop Hook,實際用的時候,須要經過 Pod 的 .spec.containers[].lifecycle.preStop 字段爲 Pod 中的每一個容器單獨配置,好比:架構

spec:
  contaienrs:
  - name: my-awesome-container
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh","-c","/pre-stop.sh"]

/pre-stop.sh 腳本里就能夠寫咱們本身的清理邏輯。框架

最後咱們串起來再整個表述一下 Pod 退出的流程(官方文檔裏更嚴謹哦):運維

  1. 用戶刪除 Pod。
    • 2.1. Pod 進入 Terminating 狀態。
    • 2.2. 與此同時,K8s 會將 Pod 從對應的 service 上摘除。
    • 2.3. 與此同時,針對有 PreStop Hook 的容器,kubelet 會調用每一個容器的 PreStop Hook,假如 PreStop Hook 的運行時間超出了 grace period,kubelet 會發送 SIGTERM 並再等 2 秒。
    • 2.4. 與此同時,針對沒有 PreStop Hook 的容器,kubelet 發送 SIGTERM。
  2. grace period 超出以後,kubelet 發送 SIGKILL 幹掉還沒有退出的容器。

這個過程很不錯,但它存在一個問題就是咱們沒法預測 Pod 會在多久以內完成優雅退出,也沒法優雅地應對「優雅退出」失敗的狀況。而在咱們的產品 TiDB Operator 中,這就是一個沒法接受的事情。

有狀態分佈式應用的挑戰

爲何說沒法接受這個流程呢?其實這個流程對無狀態應用來講一般是 OK 的,但下面這個場景就稍微複雜一點:

TiDB 中有一個核心的分佈式 KV 存儲層 TiKV。TiKV 內部基於 Multi-Raft 作一致性存儲,這個架構比較複雜,這裏咱們能夠簡化描述爲一主多從的架構,Leader 寫入,Follower 同步。而咱們的場景是要對 TiKV 作計劃性的運維操做,好比滾動升級,遷移節點。

在這個場景下,儘管系統能夠接受小於半數的節點宕機,但對於預期性的停機,咱們要儘可能作到優雅中止。這是由於數據庫場景自己就是很是嚴苛的,基本上都處於整個架構的核心部分,所以咱們要把抖動作到越小越好。要作到這點,就得作很多清理工做,好比說咱們要在停機前將當前節點上的 Leader 所有遷移到其它節點上。

得益於系統的良好設計,大多數時候這類操做都很快,然而分佈式系統中異常是屢見不鮮,優雅退出耗時過長甚至失敗的場景是咱們必需要考慮的。假如相似的事情發生了,爲了業務穩定和數據安全,咱們就不能強制關閉 Pod,而應該中止操做過程,通知工程師介入。 這時,上面所說的 Pod 退出流程就再也不適用了。

當心翼翼:手動控制全部流程

這個問題其實 K8s 自己沒有開箱即用的解決方案,因而咱們在本身的 Controller 中(TiDB 對象自己就是一個 CRD)與很是細緻地控制了各類操做場景下的服務啓停邏輯。

拋開細節不談,最後的大體邏輯是在每次停服務前,由 Controller 通知集羣進行節點下線前的各類遷移操做,操做完成後,才真正下線節點,並進行下一個節點的操做。

而假如集羣沒法正常完成遷移等操做或耗時太久,咱們也能「守住底線」,不會強行把節點幹掉,這就保證了諸如滾動升級,節點遷移之類操做的安全性。

但這種辦法存在一個問題就是實現起來比較複雜,咱們須要本身實現一個控制器,在其中實現細粒度的控制邏輯而且在 Controller 的控制循環中不斷去檢查可否安全中止 Pod。

另闢蹊徑:解耦 Pod 刪除的控制流

複雜的邏輯老是沒有簡單的邏輯好維護,同時寫 CRD 和 Controller 的開發量也不小,能不能有一種更簡潔,更通用的邏輯,能實現「保證優雅關閉(不然不關閉)」的需求呢?

有,辦法就是 ValidatingAdmissionWebhook

這裏先介紹一點點背景知識,Kubernetes 的 apiserver 一開始就有 AdmissionController 的設計,這個設計和各種 Web 框架中的 Filter 或 Middleware 很像,就是一個插件化的責任鏈,責任鏈中的每一個插件針對 apiserver 收到的請求作一些操做或校驗。舉兩個插件的例子:

  • DefaultStorageClass,爲沒有聲明 storageClass 的 PVC 自動設置 storageClass。
  • ResourceQuota,校驗 Pod 的資源使用是否超出了對應 Namespace 的 Quota。

雖說這是插件化的,但在 1.7 以前,全部的 plugin 都須要寫到 apiserver 的代碼中一塊兒編譯,很不靈活。而在 1.7 中 K8s 就引入了 Dynamic Admission Control 機制,容許用戶向 apiserver 註冊 webhook,而 apiserver 則經過 webhook 調用外部 server 來實現 filter 邏輯。1.9 中,這個特性進一步作了優化,把 webhook 分紅了兩類: MutatingAdmissionWebhook 和 ValidatingAdmissionWebhook,顧名思義,前者就是操做 api 對象的,好比上文例子中的 DefaultStroageClass,然後者是校驗 api 對象的,好比 ResourceQuota。拆分以後,apiserver 就能保證在校驗(Validating)以前先作完全部的修改(Mutating),下面這個示意圖很是清晰:

而咱們的辦法就是,利用 ValidatingAdmissionWebhook,在重要的 Pod 收到刪除請求時,先在 webhook server 上請求集羣進行下線前的清理和準備工做,並直接返回拒絕。這時候重點來了,Control Loop 爲了達到目標狀態(好比說升級到新版本),會不斷地進行 reconcile,嘗試刪除 Pod,而咱們的 webhook 則會不斷拒絕,除非集羣已經完成了全部的清理和準備工做

下面是這個流程的分步描述:

  1. 用戶更新資源對象。
  2. controller-manager watch 到對象變動。
  3. controller-manager 開始同步對象狀態,嘗試刪除第一個 Pod。
  4. apiserver 調用外部 webhook。
  5. webhook server 請求集羣作 tikv-1 節點下線前的準備工做(這個請求是冪等的),並查詢準備工做是否完成,假如準備完成,容許刪除,假如沒有完成,則拒絕,整個流程會由於 controller manager 的控制循環回到第 2 步。

好像一會兒全部東西都清晰了,這個 webhook 的邏輯很清晰,就是要保證全部相關的 Pod 刪除操做都要先完成優雅退出前的準備,徹底不用關心外部的控制循環是怎麼跑的,也所以它很是容易編寫和測試,很是優雅地知足了咱們「保證優雅關閉(不然不關閉)」的需求,目前咱們正在考慮用這種方式替換線上的舊方案。

後記

其實 Dynamic Admission Control 的應用很廣,好比 Istio 就是用 MutatingAdmissionWebhook 來實現 envoy 容器的注入的。從上面的例子中咱們也能夠看到它的擴展能力很強,並且經常能站在一個正交的視角上,很是乾淨地解決問題,與其它邏輯作到很好的解耦。

固然了,Kubernetes 中還有 很是多的擴展點,從 kubectl 到 apiserver,scheduler,kubelet(device plugin,flexvolume),自定義 Controller 再到集羣層面的網絡(CNI),存儲(CSI)能夠說是到處能夠作事情。之前作一些常規的微服務部署對這些並不熟悉也沒用過,而如今面對 TiDB 這樣複雜的分佈式系統,尤爲在 Kubernetes 對有狀態應用和本地存儲的支持還不夠好的狀況下,得在每個擴展點上去悉心考量,作起來很是有意思,所以後續可能還有一些 TiDB Operator 中思考過的解決方案分享。

相關文章
相關標籤/搜索