字节跳动技术团队 03月02日
Kubernetes 跨集群 Pod 可用性保护
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

Podseidon项目旨在解决多集群部署微服务中,因中心控制平面问题导致的Pod意外删除和服务容量丢失问题。它通过引入PodProtector CRD,设置跨集群的最小可用性要求,利用准入webhook在不满足要求时拒绝Pod删除请求。Podseidon结合list-watch和准入webhook两种数据源,平衡了数据正确性和及时性,并采用多种策略解决快照时间推断和高并发删除等问题。该方案对集群管理系统保持中立,适用于各种多集群架构,能有效强化Kubernetes控制面中的单点故障环节。

🛡️Podseidon通过引入PodProtector CRD,允许用户自定义最小可用性要求,类似于PodDisruptionBudget,但扩展到了跨集群的层面,从而确保在集群发生故障时,关键服务依然能够维持足够的容量。

🔄Podseidon巧妙地结合了list-watch和准入webhook两种数据源的优势。准入webhook能够及时地捕捉到Pod删除的请求,而list-watch则提供了集群状态的最终一致性视图。通过这种结合,Podseidon能够更准确地判断是否允许Pod删除,避免因数据不一致而导致的误判。

⏱️Podseidon针对高并发场景下的Pod删除问题,提供了maxConcurrentLag和aggregationRateMillis两个配置项。maxConcurrentLag限制了单个PodProtector中准入历史的最大条目数,避免缓冲挤拥;aggregationRateMillis则为aggregator添加了聚合延迟,缓解了因大量删除请求同时到达而引起的竞态条件问题。

🛠️为了保证在worker集群发生etcd损坏或配置错误时,Podseidon依然能够发挥作用,Kubelet需要进行改造。改造后的Kubelet仅在明确查看到带有deletionTimestamp的pod时才会停止容器;若所有者pod凭空消失则保持容器运行,从而避免Pod被意外移除。

2025-02-28 11:01 重庆

多集群部署微服务带来了可扩展性和容灾性等优势,但也引入了全局层面的脆弱性——中心控制平面的任何问题都会级联影响所有被管理集群,造成灾难性后果。其中最严重的场景之一是由于Pod删除导致的服务容量丢失。这在Kubernetes复杂的事件链中可能由多种原因引发,例如:


虽然这些问题均可单独解决,但成因多样且在持续变化的基础设施中难以穷举。更便捷的方式是采用端到端处理:只要全局要求未满足就阻止Pod删除。因此我们开发了Podseidon项目——当跨集群的最小可用性要求不满足时,拒绝删除请求的准入webhook。


01


整体设计

Podseidon引入PodProtector CRD,其spec与原生PodDisruptionBudget相近:

apiVersion: podseidon.kubewharf.io/v1alpha1kind: PodProtectorspec:selector:matchLabels: {app: www}minAvailable: 8minReadySeconds: 30

为与现有工作负载集成,Podseidon提供可定制插件的控制器 podseidon-generator,用于从Deployment等根属主工作负载自动生成并同步PodProtector。部署在每个集群的podseidon-aggregator控制器将集群内Pod当前状态聚合写入PodProtector的status字段,而各集群并配置了准入验证 webhook podseidon-webhook,在可用性底线未能满足时拒绝Pod的删除请求。





同步路径 vs 最终一致性

Pod删除事件有两种数据源:list-watch与准入webhook,各有优劣:


List-watch

准入 webhook

及时性

异步,通常滞后~100ms

同步,UpdateStatus成功时相对其他 webhook 实例具备原子性

准确性

代表过去某时刻的真实快照

可能包含被其他原因拒绝的删除事件

相对实际状态

≤ 实际删除数,可能遗漏最新事件

≥ 实际删除数,可能误计被拒事件

举个图表化的例子:

(实际的pod删除过程涉及多个步骤,但为了讨论简单起见,我们假设在成功执行DELETE请求并设置deletionTimestamp后,pod 会立即被删除)


list-watch 低于实际值,这意味着突然大量删除事件会被错误地允许爆发,所以这并非一个安全的选择。但是使用来自webhook的事件计数也不可行,因为它会无限制地偏离实际状态。


相反,我们结合了两个数据源:webhook 存储已批准的 pod 删除的历史记录,aggregator将其视图时间之前的历史记录压缩为最终正确的状态。PodProtector 对象同时包含来自aggregator最后观察到的状态以及其后来自 webhook 的增量历史,后者作为临时缓冲等待aggregator进行权威聚合。当删除突发过大,无法在单个 PodProtector 对象内存储(因为可能包含上万副本Deployment的每个副本的条目)时,最早的Pod删除事件将被压缩到某个时间范围内,避免超大PodProtector对象影响存储集群性能。


示例时间线:



因此,分歧的 webhook 线会定期校准到实际线。考虑之前的示例,其中aggregator每秒报告一秒前的状态,校准后的线与实际线更接近:


除了压缩删除历史之外,aggregator还会将扩缩容、数据面等非删除事件导致的可用性变化同步到基线值。由于 Podseidon 仅旨在防止控制平面导致的不可用,这些状态变化的延迟相对可以接受。使用此双数据源方案,可以平衡正确性和及时性的要求。





Aggregator 快照时间推断

为准确截断准入历史并保留当前快照后的增量删除事件,需从aggregator的pod list-watch事件中获取事件时间戳,然而Kubernetes原生未提供该功能。


最理想的方案本应使webhook与aggregator共用同一时钟,但由于删除请求无法通过mutating webhook修改对象字段,这个想法并不可行。故此,我们需通过其他不可靠渠道推断快照时间戳,包括:


这些方法均存在偏差:


在字节跳动的实践中,我们部署的定制版kube-apiserver会在etcd存储的GuaranteedUpdate调用中添加annotation。


其值为当前apiserver系统时间戳(功能上就是个lastUpdateTimestamp)。这使得准入历史时间戳与推断的快照时间之间的延迟更可预测,在生产环境中验证该方案,余下的竞态问题机率较小可忽略。对于标准Kubernetes,推荐采用aggregator系统时间方案 (clock),至少避免了快照无法清除自身触发事件的问题。


不过理论上,即使忽略时钟偏差,当watch延迟过高时,clock仍可能导致假阴性(错误允许pod删除):



在00:01之后,pod X和pod Y被允许删除,此时PodProtector状态中包含两个准入历史记录条目{X: 00:00}及{Y: 00:01}。在00:02时,aggregator收到pod X 删除的watch事件(延迟了2秒)。通过其informer中的新缓存状态,它观察到00:02时的快照包含pod Y而不包含pod X,因此PodProtector状态中的两个准入历史条目都被清除了。实际上这是预期的行为,因为这符合我们的假设:如果在00:02发生事件而前面没有收到pod Y的删除事件,则意味着pod Y实际上并未被删除;但事实上,该请求仍在处理中,只是时间参照系不一致导致误判。虽然aggregator最终会在00:04更新,正确地排除X和Y影响的pod数,但如果webhook在00:03收到另一个其他pod的删除请求(下称pod Z),则会被错误允许准入,因为系统假定只有pod X被删除而pod Y未被删除,从而导致最后一个不可用配额同时被 pod Y 和 pod Z 重复使用。


在灾难性事件中,大量 pod 在短时间内被删除时,这个问题尤其显著。假设有个控制器在同一个Deployment下,在 10 毫秒内并发100个删除不同pod的请求:由于webhook推送了100次准入历史,临时不可用配额下降了 100;但如果aggregator在观察到首个删除事件后、观察到后续事件之前过快进行对账,其中99个会立即被撤销。如果前面的控制器再对其他pod激增99个删除请求,不可用约束便会突破近双倍了。


为缓解此问题,Podseidon 提供了两个配置项,可全局配置或针对单个PodProtector进行覆盖:



虽然这可能会导致偶尔的误判拒绝,但replicaset controller或GC controller的重试退避通常能够在第二次尝试时成功删除。


一个生产机房内的实际pod删除准入率。在有开启Podseidon保护的pod删除中,拒绝率

甚低,基本对用户无影响。





触发最终压缩

Kubernetes 的一个重要特性是系统必须具备自愈能力,状态最终趋向spec的要求。然而,上述算法单独使用时无法实现这一目标:当webhook插入了错误的删除事件时,aggregator不会主动将其移除,而需等待kube-apiserver发送新的pod事件才能清除该记录。这可能会在低副本场景中导致死锁——pod删除请求被webhook拦截,webhook正在等待aggregator清除先前无效的删除事件以释放不可用配额,而aggregator则在等待apiserver发送事件,但由于pod删除被阻塞,apiserver根本没有事件可发送。


为解决这一问题,我们利用了单list-watch流中pod事件的强序特性:每个快照都是一个原子视图,涵盖了快照之前所有由list-watch选择器覆盖对象的所有事件。换句话说,如果我们在t₀收到一个pod X的事件,在t₀t₁之间没有其他事件,然后在t₁收到pod Y的事件,我们就可以确认自t₀事件以来,pod X 没有发生新变化。因此,aggregator可以维护一个待处理池,追踪所有尚未完全压缩准入历史的PodProtector对象,并在收到list-watch流任意pod事件时尝试对这些对象进行对账。即使这些事件来自于未被 PodProtector 选择器选中的 pod,更新了的全局快照时间也能触发更多准入历史压缩。


这问题在大规模集群中便解决了,在这些集群中,每秒可能会有数百或数千次由真正的pod生命周期或数据面事件自然触发pod 更新。然而,对于每秒不到一 pod事件的小型集群,更新事件频率过低,可能数分钟才自然触发一遍对帐,对用户造成可见的影响,如滚动或缩容操作明显减慢。为解决此问题,可能会想到几种方案:


因此,只有第三种方案是可行的。podseidon-aggregator中嵌入了一个循环组件来执行该方案,可以通过 CLI 选项启用。



根据直方图指标推算出某机房中不同大小的集群发送watch事件前后间的最大时间间隔


这个强一致性要求意味着,每个list-watch流的准入历史和聚合状态必须独立存储(我们称之为“cell”)。在aggregator需要使用多个reflector(如多个命名空间、独立标签选择算符等)场景中,它们必须在不同的aggregator实例中执行,且webhook必须配置以识别每个准入请求所对应的cell。





PodProtector批量更新

由于每个删除请求都需要在托管PodProtector对象的集群("core集群")中预留不可用配额,每次准入都会对core集群进行一个独立的compare-and-swap更新。这种设计在架构上并不合理,因为拆分多集群本来的目标就是将apiserver负载从O(mn)(m为部署数量,n为每个部署的副本数)降低到core集群O(m)。况且,对同一PodProtector对象进行多次并发更新很可能导致冲突退避,在最坏情况下会给apiserver带来O(mn²)的负载压力。


通过RetryBatch机制,对同一对象的请求进行缓冲和批量处理,可缓解这个问题。当某个PodProtector的更新请求正在处理时,所有其他更新尝试都会被暂存在RetryBatch通道中等待下次请求时同时批量处理。在下次请求时,所有缓冲请求都会基于最新版本的PodProtector执行操作:在配额可用时扣除配额,并向apiserver提交更新后的PodProtector状态。这些请求在发生冲突时可带到再下一批次里重试,若配额不足或准入请求超时则会被直接拒绝。



此方案显著降低了core集群apiserver的请求量。在字节跳动生产环境中,使用3个webhook副本的配置,批量处理比率约为1:2。理论上,请求速率不应超过正常多集群部署状态聚合器的状态更新速率乘以webhook实例数量。



某机房集群联邦中开启Podseidon保护的Pod的删除准入请求率和实际core集群的PodProtector更新请求率(包括冲突)


02


各种多集群范式的适配


Podseidon对集群管理系统保持中立性。它既可用于单集群部署,也可应用于单主多成员联邦架构,亦可支持去中心化集群网。


通用配置涉及两种集群类型:"core"与"worker"。托管PodProtector的集群称为"core"集群,运行受PodProtector保护pod的集群则称为"worker"集群。若某集群同时具备这两种资源,则可兼具两种类型;也可以同时存在多个核心或工作集群。



各组件的部署拓扑如下:


03


受保护场景


凭借端到端设计特性,Podseidon能在多种场景下防止pod被意外删除。虽然无法杜绝所有问题,但有效地强化了Kubernetes控制面中的单点故障环节:


04


系统集成




Kubelet适配

尽管Podseidon能阻止worker集群中的显式pod删除,还有最后一个环节仍需解决:当worker集群发生etcd损坏或配置错误(如kubelet连接错误apiserver,而该集群恰好有同名节点)时,pod可能被意外移除。由于该问题存在于kubelet与apiserver的直接交互中,保护实际容器不被删除的唯一可靠方案是修改kubelet代码。具体而言,kubelet被改造为:仅当明确查看到带有deletionTimestamp的pod时才会停止容器;若所有者pod凭空消失则保持容器运行。该方案虽然会影响强制删除等机制,但通过充分的监控和限制强制删除使用姿势,实践证明能有效减少人为误操作的风险。


05


类似项目




PodUnavailableBudget

OpenKruise提供的类似组件PodUnavailableBudget采用相似原理拒绝超出可用预算的pod删除操作。Podseidon扩展了多集群支持,提升了吞吐量和性能,并深入强化了容灾特性。


06


潜在改进


07


使用Podseidon


Podseidon 已在 GitHub 上开源,欢迎大家使用,点击下方“阅读原文”进入原文链接。

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Podseidon 多集群 微服务 Kubernetes 容灾
相关文章