摘要:假如我們先告訴網關或服務注冊中心我們要下線,等對方完成服務摘除操作再中止進程,那不會有任何流量受到影響這是優雅停止,將單個組件的啟停對整個系統影響最小化。與此同時,會將從對應的上摘除。
作者:吳葉磊
一直以來我對優雅地停止 Pod 這件事理解得很單純:不就利用是?PreStop hook?做優雅退出嗎?但最近發現很多場景下 PreStop Hook 并不能很好地完成需求,這篇文章就簡單分析一下“優雅地停止 Pod”這回事兒。
何謂優雅停止?優雅停止(Graceful shutdown)這個說法來自于操作系統,我們執行關機之后都得 OS 先完成一些清理操作,而與之相對的就是硬中止(Hard shutdown),比如拔電源。
到了分布式系統中,優雅停止就不僅僅是單機上進程自己的事了,往往還要與系統中的其它組件打交道。比如說我們起一個微服務,網關把一部分流量分給我們,這時:
假如我們一聲不吭直接把進程殺了,那這部分流量就無法得到正確處理,部分用戶受到影響。不過還好,通常來說網關或者服務注冊中心會和我們的服務保持一個心跳,過了心跳超時之后系統會自動摘除我們的服務,問題也就解決了;這是硬中止,雖然我們整個系統寫得不錯能夠自愈,但還是會產生一些抖動甚至錯誤。
假如我們先告訴網關或服務注冊中心我們要下線,等對方完成服務摘除操作再中止進程,那不會有任何流量受到影響;這是優雅停止,將單個組件的啟停對整個系統影響最小化。
按照慣例,SIGKILL 是硬終止的信號,而 SIGTERM 是通知進程優雅退出的信號,因此很多微服務框架會監聽 SIGTERM 信號,收到之后去做反注冊等清理操作,實現優雅退出。
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 退出的流程(官方文檔里更嚴謹哦):
用戶刪除 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。
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 則會不斷拒絕,除非集群已經完成了所有的清理和準備工作。
下面是這個流程的分步描述:
用戶更新資源對象。
controller-manager watch 到對象變更。
controller-manager 開始同步對象狀態,嘗試刪除第一個 Pod。
apiserver 調用外部 webhook。
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?中思考過的解決方案分享。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/17971.html
摘要:假如我們先告訴網關或服務注冊中心我們要下線,等對方完成服務摘除操作再中止進程,那不會有任何流量受到影響這是優雅停止,將單個組件的啟停對整個系統影響最小化。與此同時,會將從對應的上摘除。 作者:吳葉磊 一直以來我對優雅地停止 Pod 這件事理解得很單純:不就利用是?PreStop hook?做優雅退出嗎?但最近發現很多場景下 PreStop Hook 并不能很好地完成需求,這篇文章就簡單...
摘要:被設計為這樣一種方式,父進程必須明確地等待子進程終止,以便收集它的退出狀態。會完成的刪除,將優雅退出的時間設置為表示立即刪除。 SIGINT SIGTERM SIGKILL區別 三者都是結束/終止進程運行。 1.SIGINT SIGTERM區別 前者與字符ctrl+c關聯,后者沒有任何控制字符關聯。前者只能結束前臺進程,后者則不是。 2.SIGTERM SIGKILL的區別 前者可以被...
摘要:被設計為這樣一種方式,父進程必須明確地等待子進程終止,以便收集它的退出狀態。會完成的刪除,將優雅退出的時間設置為表示立即刪除。 SIGINT SIGTERM SIGKILL區別 三者都是結束/終止進程運行。 1.SIGINT SIGTERM區別 前者與字符ctrl+c關聯,后者沒有任何控制字符關聯。前者只能結束前臺進程,后者則不是。 2.SIGTERM SIGKILL的區別 前者可以被...
摘要:序,可以使得服務近乎無縫地平滑升級,即在不停止對外服務的前提下完成應用的更新。如果少于指定數量的,會創建新的,反之則會刪除掉多余的以保證數量不變。對于應用,默認的帶有接口,可以用來進行啟動成功的判斷。 序 rolling update,可以使得服務近乎無縫地平滑升級,即在不停止對外服務的前提下完成應用的更新。 replication controller與deployment的區別 r...
閱讀 3406·2021-11-25 09:43
閱讀 2293·2021-09-06 15:02
閱讀 3537·2021-08-18 10:21
閱讀 3339·2019-08-30 15:55
閱讀 2342·2019-08-29 17:06
閱讀 3533·2019-08-29 16:59
閱讀 960·2019-08-29 13:47
閱讀 2755·2019-08-26 13:24