Kubernetes 跨集群 Pod 可用性保護(hù)
多集群部署微服務(wù)帶來(lái)了可擴(kuò)展性和容災(zāi)性等優(yōu)勢(shì),但也引入了全局層面的脆弱性——中心控制平面的任何問(wèn)題都會(huì)級(jí)聯(lián)影響所有被管理集群,造成災(zāi)難性后果。其中最嚴(yán)重的場(chǎng)景之一是由于Pod刪除導(dǎo)致的服務(wù)容量丟失。這在Kubernetes復(fù)雜的事件鏈中可能由多種原因引發(fā),例如:
- 意外刪除所有Deployment的owner資源類型的CRD
- 集群拓?fù)渑渲缅e(cuò)誤,導(dǎo)致用其他集群的spec覆蓋當(dāng)前集群
- 多集群滾動(dòng)更新實(shí)現(xiàn)缺陷,同時(shí)在所有集群觸發(fā)更新
- 聯(lián)邦主集群的etcd磁盤損壞,導(dǎo)致Deployment對(duì)象從索引中移除
- 多個(gè)集群同時(shí)獨(dú)立進(jìn)行Pod驅(qū)逐操作,并發(fā)度不受控
雖然這些問(wèn)題均可單獨(dú)解決,但成因多樣且在持續(xù)變化的基礎(chǔ)設(shè)施中難以窮舉。更便捷的方式是采用端到端處理:只要全局要求未滿足就阻止Pod刪除。因此我們開(kāi)發(fā)了Podseidon項(xiàng)目——當(dāng)跨集群的最小可用性要求不滿足時(shí),拒絕刪除請(qǐng)求的準(zhǔn)入webhook。
01、整體設(shè)計(jì)
Podseidon引入PodProtector CRD,其spec與原生PodDisruptionBudget相近:
apiVersion: podseidon.kubewharf.io/v1alpha1
kind: PodProtector
spec:
selector:
matchLabels: {app: www}
minAvailable: 8
minReadySeconds: 30
為與現(xiàn)有工作負(fù)載集成,Podseidon提供可定制插件的控制器 podseidon-generator,用于從Deployment等根屬主工作負(fù)載自動(dòng)生成并同步PodProtector。部署在每個(gè)集群的podseidon-aggregator控制器將集群內(nèi)Pod當(dāng)前狀態(tài)聚合寫入PodProtector的status字段,而各集群并配置了準(zhǔn)入驗(yàn)證 webhook podseidon-webhook,在可用性底線未能滿足時(shí)拒絕Pod的刪除請(qǐng)求。
同步路徑 vs 最終一致性
Pod刪除事件有兩種數(shù)據(jù)源:list-watch與準(zhǔn)入webhook,各有優(yōu)劣:
List-watch | 準(zhǔn)入 webhook | |
及時(shí)性 | 異步,通常滯后~100ms | 同步,UpdateStatus成功時(shí)相對(duì)其他 webhook 實(shí)例具備原子性 |
準(zhǔn)確性 | 代表過(guò)去某時(shí)刻的真實(shí)快照 | 可能包含被其他原因拒絕的刪除事件 |
相對(duì)實(shí)際狀態(tài) | ≤ 實(shí)際刪除數(shù),可能遺漏最新事件 | ≥ 實(shí)際刪除數(shù),可能誤計(jì)被拒事件 |
舉個(gè)圖表化的例子:
(實(shí)際的pod刪除過(guò)程涉及多個(gè)步驟,但為了討論簡(jiǎn)單起見(jiàn),我們假設(shè)在成功執(zhí)行DELETE請(qǐng)求并設(shè)置deletionTimestamp后,pod 會(huì)立即被刪除)
list-watch 低于實(shí)際值,這意味著突然大量刪除事件會(huì)被錯(cuò)誤地允許爆發(fā),所以這并非一個(gè)安全的選擇。但是使用來(lái)自webhook的事件計(jì)數(shù)也不可行,因?yàn)樗鼤?huì)無(wú)限制地偏離實(shí)際狀態(tài)。
相反,我們結(jié)合了兩個(gè)數(shù)據(jù)源:webhook 存儲(chǔ)已批準(zhǔn)的 pod 刪除的歷史記錄,aggregator將其視圖時(shí)間之前的歷史記錄壓縮為最終正確的狀態(tài)。PodProtector 對(duì)象同時(shí)包含來(lái)自aggregator最后觀察到的狀態(tài)以及其后來(lái)自 webhook 的增量歷史,后者作為臨時(shí)緩沖等待aggregator進(jìn)行權(quán)威聚合。當(dāng)刪除突發(fā)過(guò)大,無(wú)法在單個(gè) PodProtector 對(duì)象內(nèi)存儲(chǔ)(因?yàn)榭赡馨先f(wàn)副本Deployment的每個(gè)副本的條目)時(shí),最早的Pod刪除事件將被壓縮到某個(gè)時(shí)間范圍內(nèi),避免超大PodProtector對(duì)象影響存儲(chǔ)集群性能。
示例時(shí)間線:
因此,分歧的 webhook 線會(huì)定期校準(zhǔn)到實(shí)際線。考慮之前的示例,其中aggregator每秒報(bào)告一秒前的狀態(tài),校準(zhǔn)后的線與實(shí)際線更接近:
除了壓縮刪除歷史之外,aggregator還會(huì)將擴(kuò)縮容、數(shù)據(jù)面等非刪除事件導(dǎo)致的可用性變化同步到基線值。由于 Podseidon 僅旨在防止控制平面導(dǎo)致的不可用,這些狀態(tài)變化的延遲相對(duì)可以接受。使用此雙數(shù)據(jù)源方案,可以平衡正確性和及時(shí)性的要求。
Aggregator 快照時(shí)間推斷
為準(zhǔn)確截?cái)鄿?zhǔn)入歷史并保留當(dāng)前快照后的增量刪除事件,需從aggregator的pod list-watch事件中獲取事件時(shí)間戳,然而Kubernetes原生未提供該功能。
最理想的方案本應(yīng)使webhook與aggregator共用同一時(shí)鐘,但由于刪除請(qǐng)求無(wú)法通過(guò)mutating webhook修改對(duì)象字段,這個(gè)想法并不可行。故此,我們需通過(guò)其他不可靠渠道推斷快照時(shí)間戳,包括:
- clock:使用aggregator系統(tǒng)時(shí)間
- status:使用Pod字段(creationTimestamp、deletionTimestamp、conditions等)
這些方法均存在偏差:
- 受webhook響應(yīng)延遲和watch延遲影響,aggregator系統(tǒng)時(shí)間會(huì)晚于準(zhǔn)入webhook錄入PodProtector準(zhǔn)入歷史的時(shí)間
- Pod字段時(shí)間數(shù)據(jù)源不一致,部分早于、部分晚于webhook響應(yīng),但無(wú)watch延遲問(wèn)題。尤其當(dāng)status.conditions推斷的快照時(shí)間早于webhook準(zhǔn)入時(shí)間時(shí),可能導(dǎo)致快照無(wú)法清除引發(fā)自身觸發(fā)刪除事件的準(zhǔn)入記錄。此外,部署于不同機(jī)器的組件間可能存在系統(tǒng)時(shí)鐘偏差。
在字節(jié)跳動(dòng)的實(shí)踐中,我們部署的定制版kube-apiserver會(huì)在etcd存儲(chǔ)的GuaranteedUpdate調(diào)用中添加annotation。
其值為當(dāng)前apiserver系統(tǒng)時(shí)間戳(功能上就是個(gè)lastUpdateTimestamp)。這使得準(zhǔn)入歷史時(shí)間戳與推斷的快照時(shí)間之間的延遲更可預(yù)測(cè),在生產(chǎn)環(huán)境中驗(yàn)證該方案,余下的競(jìng)態(tài)問(wèn)題機(jī)率較小可忽略。對(duì)于標(biāo)準(zhǔn)Kubernetes,推薦采用aggregator系統(tǒng)時(shí)間方案 (clock),至少避免了快照無(wú)法清除自身觸發(fā)事件的問(wèn)題。
不過(guò)理論上,即使忽略時(shí)鐘偏差,當(dāng)watch延遲過(guò)高時(shí),clock仍可能導(dǎo)致假陰性(錯(cuò)誤允許pod刪除):
在00:01之后,pod X和pod Y被允許刪除,此時(shí)PodProtector狀態(tài)中包含兩個(gè)準(zhǔn)入歷史記錄條目{X: 00:00}及{Y: 00:01}。在00:02時(shí),aggregator收到pod X 刪除的watch事件(延遲了2秒)。通過(guò)其informer中的新緩存狀態(tài),它觀察到00:02時(shí)的快照包含pod Y而不包含pod X,因此PodProtector狀態(tài)中的兩個(gè)準(zhǔn)入歷史條目都被清除了。實(shí)際上這是預(yù)期的行為,因?yàn)檫@符合我們的假設(shè):如果在00:02發(fā)生事件而前面沒(méi)有收到pod Y的刪除事件,則意味著pod Y實(shí)際上并未被刪除;但事實(shí)上,該請(qǐng)求仍在處理中,只是時(shí)間參照系不一致導(dǎo)致誤判。雖然aggregator最終會(huì)在00:04更新,正確地排除X和Y影響的pod數(shù),但如果webhook在00:03收到另一個(gè)其他pod的刪除請(qǐng)求(下稱pod Z),則會(huì)被錯(cuò)誤允許準(zhǔn)入,因?yàn)橄到y(tǒng)假定只有pod X被刪除而pod Y未被刪除,從而導(dǎo)致最后一個(gè)不可用配額同時(shí)被 pod Y 和 pod Z 重復(fù)使用。
在災(zāi)難性事件中,大量 pod 在短時(shí)間內(nèi)被刪除時(shí),這個(gè)問(wèn)題尤其顯著。假設(shè)有個(gè)控制器在同一個(gè)Deployment下,在 10 毫秒內(nèi)并發(fā)100個(gè)刪除不同pod的請(qǐng)求:由于webhook推送了100次準(zhǔn)入歷史,臨時(shí)不可用配額下降了 100;但如果aggregator在觀察到首個(gè)刪除事件后、觀察到后續(xù)事件之前過(guò)快進(jìn)行對(duì)賬,其中99個(gè)會(huì)立即被撤銷。如果前面的控制器再對(duì)其他pod激增99個(gè)刪除請(qǐng)求,不可用約束便會(huì)突破近雙倍了。
為緩解此問(wèn)題,Podseidon 提供了兩個(gè)配置項(xiàng),可全局配置或針對(duì)單個(gè)PodProtector進(jìn)行覆蓋:
- maxConcurrentLag限制限制單個(gè)PodProtector中準(zhǔn)入歷史的最大條目數(shù)。當(dāng)該值小于maxUnavailable閾值時(shí),可以確保aggregator單次最多清除maxConcurrentLag個(gè)條目。然而,將其設(shè)置得過(guò)小會(huì)導(dǎo)致PodProtector緩沖擠擁,引致更多假陽(yáng)性(誤拒絕)返回值,從而可能損害發(fā)布、縮容等操作的效率,尤其是當(dāng)頻繁失敗觸發(fā)控制器退避邏輯的更長(zhǎng)間隔時(shí)尤其顯著。
- aggregationRateMillis為aggregator添加從接收pod事件到執(zhí)行聚合的延遲,限制同一PodProtector的聚合頻率。如果一個(gè)PodProtector最近沒(méi)有新的 pod 事件,收到第一個(gè)事件后會(huì)先等待aggregationRateMillis,以容許更多激增性事件進(jìn)入informer,然后再執(zhí)行聚合;每次聚合后至少aggregationRateMillis內(nèi)不會(huì)對(duì)同一PodProtector進(jìn)行重聚合。雖然聚合觸發(fā)有所延遲,但聚合過(guò)程使用的是最新的快照,而非事件接收時(shí)的快照。這有助于緩解由上述相同對(duì)象突然大量刪除引起的競(jìng)態(tài)條件問(wèn)題(除非我們不幸接收到在第一個(gè)事件后正好過(guò)了aggregationRateMillis毫秒才開(kāi)始發(fā)生刪除激增)。然而,將該值設(shè)置得過(guò)高可能會(huì)導(dǎo)致更多由于控制器響應(yīng)變慢帶來(lái)的誤拒絕,例如在正常的滾動(dòng)更新過(guò)程中,當(dāng)新版本的pod X狀態(tài)切成可用,可以把舊版本的pod Y下線時(shí):
雖然這可能會(huì)導(dǎo)致偶爾的誤判拒絕,但replicaset controller或GC controller的重試退避通常能夠在第二次嘗試時(shí)成功刪除。
一個(gè)生產(chǎn)機(jī)房?jī)?nèi)的實(shí)際pod刪除準(zhǔn)入率。在有開(kāi)啟Podseidon保護(hù)的pod刪除中,拒絕率
甚低,基本對(duì)用戶無(wú)影響。
觸發(fā)最終壓縮
Kubernetes 的一個(gè)重要特性是系統(tǒng)必須具備自愈能力,狀態(tài)最終趨向spec的要求。然而,上述算法單獨(dú)使用時(shí)無(wú)法實(shí)現(xiàn)這一目標(biāo):當(dāng)webhook插入了錯(cuò)誤的刪除事件時(shí),aggregator不會(huì)主動(dòng)將其移除,而需等待kube-apiserver發(fā)送新的pod事件才能清除該記錄。這可能會(huì)在低副本場(chǎng)景中導(dǎo)致死鎖——pod刪除請(qǐng)求被webhook攔截,webhook正在等待aggregator清除先前無(wú)效的刪除事件以釋放不可用配額,而aggregator則在等待apiserver發(fā)送事件,但由于pod刪除被阻塞,apiserver根本沒(méi)有事件可發(fā)送。
為解決這一問(wèn)題,我們利用了單list-watch流中pod事件的強(qiáng)序特性:每個(gè)快照都是一個(gè)原子視圖,涵蓋了快照之前所有由list-watch選擇器覆蓋對(duì)象的所有事件。換句話說(shuō),如果我們?cè)趖?收到一個(gè)pod X的事件,在t?到t?之間沒(méi)有其他事件,然后在t?收到pod Y的事件,我們就可以確認(rèn)自t?事件以來(lái),pod X 沒(méi)有發(fā)生新變化。因此,aggregator可以維護(hù)一個(gè)待處理池,追蹤所有尚未完全壓縮準(zhǔn)入歷史的PodProtector對(duì)象,并在收到list-watch流任意pod事件時(shí)嘗試對(duì)這些對(duì)象進(jìn)行對(duì)賬。即使這些事件來(lái)自于未被 PodProtector 選擇器選中的 pod,更新了的全局快照時(shí)間也能觸發(fā)更多準(zhǔn)入歷史壓縮。
這問(wèn)題在大規(guī)模集群中便解決了,在這些集群中,每秒可能會(huì)有數(shù)百或數(shù)千次由真正的pod生命周期或數(shù)據(jù)面事件自然觸發(fā)pod 更新。然而,對(duì)于每秒不到一 pod事件的小型集群,更新事件頻率過(guò)低,可能數(shù)分鐘才自然觸發(fā)一遍對(duì)帳,對(duì)用戶造成可見(jiàn)的影響,如滾動(dòng)或縮容操作明顯減慢。為解決此問(wèn)題,可能會(huì)想到幾種方案:
- 為準(zhǔn)入歷史增設(shè)到期時(shí)間——這實(shí)際上違背了Podseidon的設(shè)計(jì)目的。在一些極端災(zāi)難事件中,watch請(qǐng)求很可能會(huì)中斷并觸發(fā)relist,從而在此期間引起非常高的watch時(shí)延(在大規(guī)模集群中relist可能需要長(zhǎng)達(dá)數(shù)分鐘),甚至無(wú)法重新建立新的一致性快照。增設(shè)到期時(shí)間在這種情況下實(shí)際上會(huì)使Podseidon webhook失去原來(lái)的作用。
- 如果一段時(shí)間內(nèi)沒(méi)有事件則進(jìn)行relist——但這樣的時(shí)間周期該設(shè)多長(zhǎng)?relist過(guò)程本身也會(huì)導(dǎo)致長(zhǎng)時(shí)間沒(méi)有事件,可能會(huì)使情況更糟。而且pod list是個(gè)很重的操作,頻繁的relist可能沖擊kube-apiserver的性能。
- 在一個(gè)dummy pod上觸發(fā)事件——這聽(tīng)起來(lái)可能不太優(yōu)雅,但這是我們找到最切實(shí)可行的方案。通過(guò)循環(huán)更新一個(gè)會(huì)在list-watch流中發(fā)送的dummy pod(例如切換pod注解),我們可以確保watch流的最小理論事件率,從而在正常情況下將無(wú)效準(zhǔn)入歷史條目的存續(xù)時(shí)間限制在這一時(shí)間下。
因此,只有第三種方案是可行的。podseidon-aggregator中嵌入了一個(gè)循環(huán)組件來(lái)執(zhí)行該方案,可以通過(guò) CLI 選項(xiàng)啟用。
根據(jù)直方圖指標(biāo)推算出某機(jī)房中不同大小的集群發(fā)送watch事件前后間的最大時(shí)間間隔
這個(gè)強(qiáng)一致性要求意味著,每個(gè)list-watch流的準(zhǔn)入歷史和聚合狀態(tài)必須獨(dú)立存儲(chǔ)(我們稱之為“cell”)。在aggregator需要使用多個(gè)reflector(如多個(gè)命名空間、獨(dú)立標(biāo)簽選擇算符等)場(chǎng)景中,它們必須在不同的aggregator實(shí)例中執(zhí)行,且webhook必須配置以識(shí)別每個(gè)準(zhǔn)入請(qǐng)求所對(duì)應(yīng)的cell。
PodProtector批量更新
由于每個(gè)刪除請(qǐng)求都需要在托管PodProtector對(duì)象的集群("core集群")中預(yù)留不可用配額,每次準(zhǔn)入都會(huì)對(duì)core集群進(jìn)行一個(gè)獨(dú)立的compare-and-swap更新。這種設(shè)計(jì)在架構(gòu)上并不合理,因?yàn)椴鸱侄嗉罕緛?lái)的目標(biāo)就是將apiserver負(fù)載從O(mn)(m為部署數(shù)量,n為每個(gè)部署的副本數(shù))降低到core集群O(m)。況且,對(duì)同一PodProtector對(duì)象進(jìn)行多次并發(fā)更新很可能導(dǎo)致沖突退避,在最壞情況下會(huì)給apiserver帶來(lái)O(mn2)的負(fù)載壓力。
通過(guò)RetryBatch機(jī)制,對(duì)同一對(duì)象的請(qǐng)求進(jìn)行緩沖和批量處理,可緩解這個(gè)問(wèn)題。當(dāng)某個(gè)PodProtector的更新請(qǐng)求正在處理時(shí),所有其他更新嘗試都會(huì)被暫存在RetryBatch通道中等待下次請(qǐng)求時(shí)同時(shí)批量處理。在下次請(qǐng)求時(shí),所有緩沖請(qǐng)求都會(huì)基于最新版本的PodProtector執(zhí)行操作:在配額可用時(shí)扣除配額,并向apiserver提交更新后的PodProtector狀態(tài)。這些請(qǐng)求在發(fā)生沖突時(shí)可帶到再下一批次里重試,若配額不足或準(zhǔn)入請(qǐng)求超時(shí)則會(huì)被直接拒絕。
此方案顯著降低了core集群apiserver的請(qǐng)求量。在字節(jié)跳動(dòng)生產(chǎn)環(huán)境中,使用3個(gè)webhook副本的配置,批量處理比率約為1:2。理論上,請(qǐng)求速率不應(yīng)超過(guò)正常多集群部署狀態(tài)聚合器的狀態(tài)更新速率乘以webhook實(shí)例數(shù)量。
某機(jī)房集群聯(lián)邦中開(kāi)啟Podseidon保護(hù)的Pod的刪除準(zhǔn)入請(qǐng)求率和實(shí)際core集群的PodProtector更新請(qǐng)求率(包括沖突)
02、各種多集群范式的適配
Podseidon對(duì)集群管理系統(tǒng)保持中立性。它既可用于單集群部署,也可應(yīng)用于單主多成員聯(lián)邦架構(gòu),亦可支持去中心化集群網(wǎng)。
通用配置涉及兩種集群類型:"core"與"worker"。托管PodProtector的集群稱為"core"集群,運(yùn)行受PodProtector保護(hù)pod的集群則稱為"worker"集群。若某集群同時(shí)具備這兩種資源,則可兼具兩種類型;也可以同時(shí)存在多個(gè)核心或工作集群。
各組件的部署拓?fù)淙缦拢?/span>
- Generator:每個(gè)core集群部署一組實(shí)例
- Aggregator:每個(gè)worker集群部署一組實(shí)例,連接所有core集群
- Webhook:全網(wǎng)部署一組實(shí)例供所有worker集群使用,連接所有core集群
也可為每個(gè)worker集群部署集群內(nèi)webhook,但這會(huì)降低RetryBatch效果,增加core集群控制面負(fù)載。
03、受保護(hù)場(chǎng)景
憑借端到端設(shè)計(jì)特性,Podseidon能在多種場(chǎng)景下防止pod被意外刪除。雖然無(wú)法杜絕所有問(wèn)題,但有效地強(qiáng)化了Kubernetes控制面中的單點(diǎn)故障環(huán)節(jié):
- Etcd數(shù)據(jù)損壞
core集群根負(fù)載對(duì)象(如Deployments)丟失:若根對(duì)象未顯式設(shè)置deletionTimestamp,Podseidon generator不會(huì)刪除PodProtector對(duì)象。為確保該機(jī)制可靠運(yùn)行,請(qǐng)盡量從根對(duì)象而非派生對(duì)象創(chuàng)建PodProtector
core/worker 集群中間對(duì)象丟失(如ReplicaSets):盡管這些對(duì)象可能被篡改,但只要PodProtector有效,它們就無(wú)法影響實(shí)際pod
worker集群pod丟失:參見(jiàn)下文"Kubelet適配"章節(jié)
core集群PodProtector丟失:當(dāng)前版本未受保護(hù);參見(jiàn)下文"潛在改進(jìn)"章節(jié)
worker集群ValidatingWebhookConfiguration丟失:無(wú)法保護(hù),因這是apiserver與etcd間的直連鏈路(可另外添加apiserver內(nèi)置準(zhǔn)入插件加固webhook存在性檢查)
- 顯式pod刪除(示例可參見(jiàn)文首):Podseidon全面覆蓋
04、系統(tǒng)集成
Kubelet適配
盡管Podseidon能阻止worker集群中的顯式pod刪除,還有最后一個(gè)環(huán)節(jié)仍需解決:當(dāng)worker集群發(fā)生etcd損壞或配置錯(cuò)誤(如kubelet連接錯(cuò)誤apiserver,而該集群恰好有同名節(jié)點(diǎn))時(shí),pod可能被意外移除。由于該問(wèn)題存在于kubelet與apiserver的直接交互中,保護(hù)實(shí)際容器不被刪除的唯一可靠方案是修改kubelet代碼。具體而言,kubelet被改造為:僅當(dāng)明確查看到帶有deletionTimestamp的pod時(shí)才會(huì)停止容器;若所有者pod憑空消失則保持容器運(yùn)行。該方案雖然會(huì)影響強(qiáng)制刪除等機(jī)制,但通過(guò)充分的監(jiān)控和限制強(qiáng)制刪除使用姿勢(shì),實(shí)踐證明能有效減少人為誤操作的風(fēng)險(xiǎn)。
05、類似項(xiàng)目
PodUnavailableBudget
OpenKruise提供的類似組件PodUnavailableBudget采用相似原理拒絕超出可用預(yù)算的pod刪除操作。Podseidon擴(kuò)展了多集群支持,提升了吞吐量和性能,并深入強(qiáng)化了容災(zāi)特性。
06、潛在改進(jìn)
- PodProtector informer健壯性:當(dāng)core集群的PodProtector被異常清除時(shí),webhook實(shí)例內(nèi)存中仍存有副本。通過(guò)在generator刪除前顯式標(biāo)記PodProtector失效并向webhook實(shí)例廣播該事件,可在etcd損壞等場(chǎng)景下依然維持有效保護(hù)。
- 多樣化保護(hù)條件:除pod就緒狀態(tài)外,Podseidon可整合Scheduled或Initialized等pod狀態(tài)。雖然滾動(dòng)控制器聚焦用戶容器報(bào)告的就緒狀態(tài),但用戶行為可能導(dǎo)致就緒狀態(tài)突變,使監(jiān)控復(fù)雜度提升。采用其他狀態(tài)或pod階段能為控制平面穩(wěn)定性監(jiān)控提供更可靠的SLI指標(biāo)
07、使用Podseidon
Podseidon 已在 GitHub 上開(kāi)源,歡迎大家使用。