字節(jié)跳動云原生防護體系實踐

背景
隨著 Kubernetes 在企業(yè)中大規(guī)模使用和落地,逐漸形成了 "業(yè)務(wù) - 中臺 - 基礎(chǔ)設(shè)施" 的分層技術(shù)體系;這種分層能夠屏蔽平臺和基礎(chǔ)設(shè)施層的復(fù)雜概念,讓應(yīng)用專注于業(yè)務(wù)層的研發(fā),但同時也會導(dǎo)致上層應(yīng)用的穩(wěn)定性強依賴底層基礎(chǔ)設(shè)施的支持,從而對基礎(chǔ)設(shè)施在大規(guī)模集群下的穩(wěn)定性提出極大的挑戰(zhàn):
- 由于集群規(guī)模龐大,任何單一不起眼的小問題都可能被無限放大,帶來系統(tǒng)性風險;
 - 場景的復(fù)雜性和多樣性,也使得運維操作出現(xiàn)不符合預(yù)期的行為難以徹底避免。
 
這就要求我們對于 Kubernetes 所管理的資源和對象進行更有效的極端風險防護,盡可能緩解由于誤操作、組件版本與配置的錯誤、或者管控代碼 bug 對業(yè)務(wù)造成不可挽回的影響。
盡管 Kubernetes 原生提供了一系列的防護機制,例如嚴格的 RBAC 校驗機制、使用 PodDisruptionBudget(PDB)對 Eviction API 執(zhí)行校驗、較為豐富的 Admission Plugins 等,但是在實際生產(chǎn)實踐中,我們?nèi)匀话l(fā)現(xiàn)有很多無法覆蓋的場景。
在此背景下,字節(jié)跳動內(nèi)部對 Kubernetes 系統(tǒng)進行了擴展與改造,增加了一系列的防御性校驗措施與操作約束,為運行在 Kubernetes 上的業(yè)務(wù)提供更強有力的穩(wěn)定性支撐,降低極端風險。
防護加固
Kubernetes 是個相當復(fù)雜的分布式系統(tǒng),但其架構(gòu)設(shè)計上的核心思想還是非常簡單的。Kubernetes 通過 APIServer 提供統(tǒng)一的 API 接口實現(xiàn)對集群狀態(tài)的訪問與修改能力;各種自動化組件能以標準化的方式與集群通信持續(xù)獲取數(shù)據(jù),并通過本地計算當前集群狀態(tài)與預(yù)期集群狀態(tài)之間的區(qū)別,派生出一系列的變更操作;最終通過 kubelet 在每個節(jié)點上執(zhí)行這些狀態(tài)變更,將集群朝著預(yù)期的狀態(tài)推進。
由此可見,Kubernetes 組件間的交互和運行狀態(tài)可以大致分成以下三層
- KV 存儲系統(tǒng)(如 etcd、Kine、Kubebrain)與 apiserver 間的交互,提供 key-value 級別的讀寫操作與事件監(jiān)聽;
 - apiserver 與各種內(nèi)建或附加 controller/operator 間(以及 apiserver 與用戶間)通過 API 請求交互;
 - apiserver 與單機節(jié)點組件間的交互。
 

根據(jù)上述分層,我們可以針對性梳理出一系列常見的系統(tǒng)性風險,并分別采取對應(yīng)的措施進行加固以降低極端風險。
數(shù)據(jù)防護
存儲與 apiserver 之間的交互風險主要集中數(shù)據(jù)異常方面,例如數(shù)據(jù)的損壞與丟失等;存儲系統(tǒng)是 Kubernetes 的核心,是整個基于事件驅(qū)動的分布式系統(tǒng)的基石,一旦出現(xiàn)數(shù)據(jù)異??赡苤苯踊蜷g接地派生出一系列的故障。具體來說可能包括但不限于以下的常見極端風險問題:
- 存儲集群運維操作失誤導(dǎo)致存儲下線,導(dǎo)致整個 Kubernetes 集群不可用;
 - 管理員直接刪除 etcd 中的數(shù)據(jù),未經(jīng)過 apiserver 做校驗,可能導(dǎo)致一些非預(yù)期關(guān)鍵對象如 namespace、deployment、pod 等被直接刪除,并觸發(fā)對象的級聯(lián)刪除,導(dǎo)致業(yè)務(wù)大面積受損;
 - 管理員因誤操作直接修改 etcd 中的數(shù)據(jù),損壞了數(shù)據(jù)格式導(dǎo)致 apiserver 無法 decode 數(shù)據(jù)。
 
針對這些問題,我們在生產(chǎn)環(huán)境中采取了一系列措施——首先,盡可能標準化地約束對存儲集群的運維和數(shù)據(jù)操作,在存儲系統(tǒng)側(cè)開啟 TLS 雙向認證,盡量避免除了 Kubernetes 以外的用戶直接訪問存儲,降低數(shù)據(jù)損壞或丟失的風險;其次,對存儲進行定時的備份,在極端情況下,當發(fā)生不可逆的數(shù)據(jù)損失時,基于備份能快速恢復(fù)數(shù)據(jù),降低損失的影響;此外,通過對其他組件進行加固,盡可能降低數(shù)據(jù)異常派生的非預(yù)期事件對于業(yè)務(wù)的直接沖擊。
控制面防護
自動化組件與 apiserver 之間的交互風險,主要集中在非預(yù)期操作方面。正常情況下,用戶或平臺將預(yù)期的狀態(tài)提交到 apiserver,而其他內(nèi)部組件將立即根據(jù)當前狀態(tài)和預(yù)期狀態(tài)的區(qū)別派生出一系列的動作,從而使集群產(chǎn)生變更;而一旦錯誤的預(yù)期狀態(tài)被提交,集群將快速并且難以逆轉(zhuǎn)地朝著目標狀態(tài)進行變更。
針對這一類問題的主要防護思路,就是對關(guān)鍵對象的操作進行一些額外的限制,例如要求在操作時額外添加一些冗余操作,形成 double check 機制,降低由于誤操作或者管控代碼 bug 引發(fā)風險的概率;具體來說,操作防護通過 Kubernetes 原生提供的擴展機制 ValidatingAdmissionWebhook 來實現(xiàn)。我們通過 label 和 annotation 來標記需要進行操作防護的關(guān)鍵對象,并通過 selector 配置對這些關(guān)鍵對象以及對應(yīng)的操作進行篩選,在 Webhook 中實現(xiàn)一系列的約束以達到防護的目的,其中包括但不限于以下這些策略:
- 防止級聯(lián)刪除針對 Namespace、CRD 等根對象,一旦被刪除會導(dǎo)致級聯(lián)地觸發(fā)派生出的其他對象的刪除操作。因此我們在 Webhook 中對這些類型的關(guān)鍵對象的刪除進行攔截,避免誤操作引發(fā)級聯(lián)刪除操作引發(fā)災(zāi)難性后果。
 - 顯式副本修改當需要調(diào)整關(guān)鍵
 workload 資源副本數(shù)量時,為了避免意外地將副本數(shù)量縮減至 0,我們要求在通過 UPDATE 或者 PATCH 
請求調(diào)整副本數(shù)的同時,還需要顯式地給對象添加特定 annotation 寫入預(yù)期調(diào)整的數(shù)值作為 double check;在 Webhook 
中校驗關(guān)鍵 workload 對象進行變更時 
.spec.replicas字段中的值是否與 annotation 中提供的值保持一致,確保任何對于關(guān)鍵 workload 副本數(shù)的修改都是有意且明確的。 - 顯式資源刪除當需要刪除關(guān)鍵 workload 對象時,要求在刪除對象之前先通過修改操作將 workload 的副本數(shù)降至 0;通過這種約束,我們得以避免一些誤操作,例如某些關(guān)鍵的 workload 對象未經(jīng)確認直接,可能會觸發(fā)更多級聯(lián)的刪除操作,導(dǎo)致業(yè)務(wù)受損。
 - 操作程序約束對于一些特定的業(yè)務(wù),對于業(yè)務(wù)規(guī)格的變更有著嚴格的變更事件窗口限制,例如業(yè)務(wù)只接受在非繁忙時段對鏡像、環(huán)境變量等配置進行變更,這樣可以降低因為規(guī)格更改引起的潛在問題,以及相應(yīng)的業(yè)務(wù)中斷風險。我們通過 CRD 定義了可變更窗口、可變更字段等約束并暴露給用戶,在 Webhook 中根據(jù)用戶配置進行相應(yīng)的校驗,這樣可以確保在出現(xiàn)故障時,影響盡量少的終端用戶,確保有相對充分的故障處理時間,最大程度的減少潛在損失,降低系統(tǒng)風險。
 
此外,線上生產(chǎn)環(huán)境中經(jīng)常會遇到一些客戶端的異常,例如 OOM、大量緩存穿透等問題,這些異常往往會引發(fā)大量的開銷極大的讀請求,引發(fā)控制面異常甚至雪崩。針對線上異常流量的防護問題,我們對用戶行為進行了一定限制,禁止了一些開銷極大的讀穿透行為。其次,我們在控制面前置了針對 kube-apiserver 流量特征專門定制的七層網(wǎng)關(guān) KubeGateway,它解決了 kube-apiserver 負載不均衡的問題,同時實現(xiàn)了對 kube-apiserver 請求的完整治理,包括請求路由、分流、限流、降級等,顯著提高了 Kubernetes 集群的可用性。另外,我們對 Kubernetes 的審計日志進行擴展,將一些流量相關(guān)的信息附加到審計日志上,在此基礎(chǔ)上進行分析得到用戶畫像。在異常的場景下,將用戶畫像、流量監(jiān)控指標與控制面前置的七層網(wǎng)關(guān) KubeGateway 的限流能力相結(jié)合,對給控制面提供巨大壓力的 Client 進行流量控制,盡可能降低雪崩風險。
節(jié)點防護
在大多數(shù)場景下,pod 的刪除應(yīng)該分成兩個階段執(zhí)行:首先由中心化的 Controller 或者用戶通過發(fā)起 Delete 請求將 pod 標記為刪除狀態(tài)(即添加 DeletionTimestamp),然后應(yīng)該由 kubelet 負責對業(yè)務(wù)發(fā)起優(yōu)雅退出,等待業(yè)務(wù)終止且資源釋放之后,由 kubelet 來通過 APIServer 提供的接口將 pod 徹底移除。但在生產(chǎn)實踐中,我們遇到諸多了問題,可能導(dǎo)致 kubelet 因為異常而非預(yù)期地終止業(yè)務(wù) pod,例如:
- 由于配置錯誤或者代碼 bug,導(dǎo)致 kubelet 重啟后 reject 正在運行的業(yè)務(wù) pod,導(dǎo)致業(yè)務(wù)受損;
 - 由于控制面存儲出現(xiàn)的數(shù)據(jù)損壞或其他異常,導(dǎo)致 kubelet 發(fā)現(xiàn)本地實際運行的 pod 與控制面提供的本地應(yīng)該運行的 pod 不一致,進而引起非預(yù)期的業(yè)務(wù)退出。
 
針對這類問題,我們對 kubelet 進行了一系列的改造,涵蓋 admit、housekeeping 等環(huán)節(jié)。通過改造給 kubelet 刪除 pod 的操作加入前置約束:在嘗試刪除關(guān)鍵 pod 時,首先檢查 pod 是否被顯式地進行標記刪除,如果 pod 未被標記刪除,則不允許 kubelet 觸發(fā) pod 的刪除操作?;谶@種顯式刪除的約束,我們得以大幅度降低因為各種 Kubernetes 組件異常而引發(fā)的節(jié)點層面的業(yè)務(wù)運行風險。
小結(jié)
在生產(chǎn)環(huán)境中,我們主要根據(jù) Kubernetes 組件之間的交互過程識別和梳理出關(guān)鍵風險,通過特定的 label 與 annotation 對關(guān)鍵的對象進行標記,分別采取措施進行一定的加固:
- 數(shù)據(jù)防護主要是約束運維操作,收斂數(shù)據(jù)訪問入口,標準化存儲操作的各種行為以減小風險;
 - 控制面防護主要是通過定制 ValidatingAdmissionWebhook 進行擴展,在對于一些關(guān)鍵對象的變更過程中,要求主動引入一些冗余的操作與校驗,降低誤操作風險;
 - 節(jié)點防護主要是通過對 kubelet 的進行改造,嚴格約束關(guān)鍵 pod 必須顯式刪除,降低極端情況下的系統(tǒng)性風險。
 
應(yīng)用案例
字節(jié)基于原生 Kubernetes 生態(tài)定制了較多的功能以支持個性化的場景,整體的研發(fā)、迭代和交付的效率都非常高,對集群穩(wěn)定性造成更大的挑戰(zhàn),即使在交付流程規(guī)范上嚴格把控,也不能完全杜絕異常情況下的極端異常風險;結(jié)合實踐過程出現(xiàn)過的故障案例和場景訴求,字節(jié)云原生團隊從元集群、控制面、數(shù)據(jù)面、業(yè)務(wù)定制等多個角度,構(gòu)建了較為全面的防御體系,有效避免線上大規(guī)模事故的發(fā)生。
數(shù)據(jù)防護:元集群級聯(lián)刪除
字節(jié)內(nèi)部的集群數(shù)量眾多,為實現(xiàn)自動化運維和集群管理,需要構(gòu)建元集群描述業(yè)務(wù)集群的狀態(tài);在這種情況下,元集群自身的異??赡軙|發(fā)更大范圍的故障。在字節(jié)早期,集群缺乏防護能力,SRE 在運維過程中使用過高權(quán)限,誤刪除了某個 region 元集群中用于描述 Node 狀態(tài)的 CRD,因為沒有防御系統(tǒng)攔截,CRD 被刪除后會引發(fā)全量 CR 的級聯(lián)刪除,導(dǎo)致元集群控制器認為幾乎所有的節(jié)點都需要下線,引發(fā)全量 pod 物理停服。該次故障最終引發(fā)單 region 生產(chǎn)集群在 30 分鐘內(nèi)持續(xù)標記刪 3W+ 節(jié)點,實際刪除 9K 節(jié)點后及時止損,影響面巨大且手動止損窗口很短。在該案例中,接入防御體系能夠?qū)崿F(xiàn)在多個點位實現(xiàn)防御能力
- 前置攔截:通過標記 CRD 為 critial 避免全量誤刪除引發(fā)級聯(lián)問題;
 - 集群下線限流:集群大范圍下線通常并不是常見的運維操作,控制節(jié)點下線的頻率和安全水位,保證即使出現(xiàn)異常的級聯(lián)刪除行為,也能夠盡量控制故障域;
 - 數(shù)據(jù)備份和恢復(fù):當發(fā)生物理對象刪除行為后,能夠通過備份數(shù)據(jù)實現(xiàn)快速的恢復(fù)。
 

控制面防護:異常流量識別與限流
控制面異常通常源自于不合理的客戶端行為和不夠準確的服務(wù)端資源預(yù)估,由于場景過于復(fù)雜,在缺乏精細治理的情況下,最終因各種原因?qū)е路?wù)端過載;通常從現(xiàn)象上,會伴隨著客戶端大量的 List 請求和 APIServer OOM,進一步引發(fā)全量客戶端 Relist,惡性循環(huán)直至集群出現(xiàn)雪崩。對于控制面的極端異常,字節(jié)內(nèi)部通過接入 7 層的 gateway ,配合全鏈路的自動化流量 tracing,實現(xiàn)靈活智能的 API 請求防護
- 常態(tài)限流:針對客戶端和資源對象的組合和常態(tài)流量分析,定制限流規(guī)則,避免瞬時大量請求對服務(wù)端的壓力
 - 容災(zāi)場景熔斷:當集群出現(xiàn)明顯異?;蛘哐┍罆r,通過手動熔斷止損,并逐步放開限流以恢復(fù)集群正常
 

節(jié)點防護:異常版本升級觸發(fā)大面積驅(qū)逐
相對于控制面,數(shù)據(jù)面的版本和配置通常更加復(fù)雜多樣,迭代通常會更加頻繁,更容易因為不當?shù)慕M件運維操作引發(fā)不可預(yù)期的極端風險。某次 SRE 在升級 Kubelet 版本的過程中,應(yīng)用了不符合預(yù)期的混部資源配置,在 Kubelet 重啟后,大量 Running 中的 pod 因為資源歸屬識別錯誤,導(dǎo)致 admit 失敗而被 delete,同時,原生的 delete API 不過 PDB 攔截,預(yù)期會引發(fā)大量業(yè)務(wù)容量的損失;但由于已經(jīng)上線防護能力,最終沒有引發(fā)嚴重的線上問題。在該案例中,接入防御體系能夠同時在單機和中心上提供防御能力
- 單機攔截:對于已經(jīng)處于 Running 狀態(tài)的核心服務(wù),默認補充 explict-deletion 標簽,確保只有顯式地通過 API 標記刪除 (設(shè)置 deletionTimestamp),能夠保證因為數(shù)據(jù)面異常發(fā)版后,不影響業(yè)務(wù)實例的運行,給人為介入處理提供足夠的時間
 - 中心攔截:對于核心服務(wù)補充 Delete 與 DeleteCollection 兩種 API 進行校驗,避免類似非預(yù)期的刪除 pod 行為對業(yè)務(wù)造成影響
 

后續(xù)規(guī)劃
字節(jié)防護實踐未來會逐漸集成火山引擎 VKE 產(chǎn)品中,為云上的服務(wù)提供更加可靠的穩(wěn)定性保證;除此之外,我們也會持續(xù)增強云原生防護的功能特性,收斂并解決更多可能對云上服務(wù)造成穩(wěn)定性風險的場景,包括如下內(nèi)容
- 控制面 Delete pod API 防護內(nèi)建的 PDB 防護機制僅作用于 Evict pod API,校驗性能不佳。當存在大量 PDB 對象時,Evict pod API 耗時會大幅度劣化,請求時延遠超 Delete pod,因此有很多組件刻意不使用 Evict pod 而直接 Delete pod,例如調(diào)度器發(fā)起搶占等。由于控制面 Delete pod 的內(nèi)置校驗較少,直接使用該接口容易導(dǎo)致業(yè)務(wù) pod 的健康比例低于預(yù)期,影響業(yè)務(wù)正常運行。為避免這類風險,我們一方面需要優(yōu)化 Evict pod 的性能,另一方面需要通過擴展對 Delete pod 操作進行更嚴格的校驗,以保證業(yè)務(wù)運行的 pod 健康比例不低于預(yù)期。
 - 收斂靜態(tài)校驗策略當前我們在控制面做的防護工作主要依托于對 Validating Admission Webhook 機制,這一方面會 apiserver 在處理請求過程中引入額外的外部過程,提高延遲與出錯概率,另一方面也會一定程度提高集群運維的復(fù)雜度。在 Kubernetes 1.26 版本中,引入了新的 Admission Plugin,支持使用 CEL (Common Expression Language)對請求進行一些靜態(tài)校驗。后續(xù)我們會將控制面防護的一些冗余操作校驗遷移到 CEL,對上述問題進行改善。
 - 場景定制防護策略對于 Redis 和分布式訓練等帶存儲狀態(tài)的業(yè)務(wù)來說,其編排模型和運維方案上有比較多的定制需求,為此,防御體系需要針對其業(yè)務(wù)特點 (如存儲分片、縱向資源調(diào)整、原地重啟等),補充完善更多精細化的策略以匹配特有的極端異常風險。
 
總結(jié)
本文主要介紹了字節(jié)跳動內(nèi)部生產(chǎn)環(huán)境中 Kubernetes 應(yīng)用過程中發(fā)現(xiàn)的主要系統(tǒng)風險與提出一系列防護措施。具體來說,我們從 Kubernetes 組件的交互過程的角度出發(fā),劃分為數(shù)據(jù)、控制面、節(jié)點三個層面,并通過具體示例說明了常見問題,包括誤操作和管控組件版本錯誤等等,并且針對這些常見問題,簡單介紹了我們構(gòu)建的一系列防御性措施,包括但不限于,約束組件訪問權(quán)限、主動添加冗余操作與相關(guān)校驗等等。通過這些防御性措施,我們能夠降低已知問題給業(yè)務(wù)帶來的風險,為業(yè)務(wù)提供穩(wěn)定的基礎(chǔ)服務(wù)。
除了必要的防御性加固措施,日常維護集群時的標準化變更流程也至關(guān)重要。通過控制集群規(guī)模并充分進行灰度驗證,可以降低故障的影響范圍。在生產(chǎn)環(huán)境中,只有綜合利用系統(tǒng)自我防御性措施和標準化運維等多種手段,才能最大程度地降低風險和故障損失。















 
 
 















 
 
 
 