背景
随着 Kubernetes 在企业中大规模使用和落地,逐渐形成了 "业务 - 中台 - 基础设施" 的分层技术体系;这种分层能够屏蔽平台和基础设施层的复杂概念,让应用专注于业务层的研发,但同时也会导致上层应用的稳定性强依赖底层基础设施的支持,从而对基础设施在大规模集群下的稳定性提出极大的挑战:
- 由于集群规模庞大,任何单一不起眼的小问题都可能被无限放大,带来系统性风险;
- 场景的复杂性和多样性,也使得运维操作出现不符合预期的行为难以彻底避免。
这就要求我们对于 Kubernetes 所管理的资源和对象进行更有效的极端风险防护,尽可能缓解由于误操作、组件版本与配置的错误、或者管控代码 bug 对业务造成不可挽回的影响。
尽管 Kubernetes 原生提供了一系列的防护机制,例如严格的 RBAC 校验机制、使用 PodDisruptionBudget(PDB)对 Eviction API 执行校验、较为丰富的 Admission Plugins 等,但是在实际生产实践中,我们仍然发现有很多无法覆盖的场景。
在此背景下,字节跳动内部对 Kubernetes 系统进行了扩展与改造,增加了一系列的防御性校验措施与操作约束,为运行在 Kubernetes 上的业务提供更强有力的稳定性支撑,降低极端风险。
防护加固
Kubernetes 是个相当复杂的分布式系统,但其架构设计上的核心思想还是非常简单的。Kubernetes 通过 APIServer 提供统一的 API 接口实现对集群状态的访问与修改能力;各种自动化组件能以标准化的方式与集群通信持续获取数据,并通过本地计算当前集群状态与预期集群状态之间的区别,派生出一系列的变更操作;最终通过 kubelet 在每个节点上执行这些状态变更,将集群朝着预期的状态推进。
由此可见,Kubernetes 组件间的交互和运行状态可以大致分成以下三层
- KV 存储系统(如 etcd、Kine、Kubebrain)与 apiserver 间的交互,提供 key-value 级别的读写操作与事件监听;
- apiserver 与各种内建或附加 controller/operator 间(以及 apiserver 与用户间)通过 API 请求交互;
- apiserver 与单机节点组件间的交互。
根据上述分层,我们可以针对性梳理出一系列常见的系统性风险,并分别采取对应的措施进行加固以降低极端风险。
数据防护
存储与 apiserver 之间的交互风险主要集中数据异常方面,例如数据的损坏与丢失等;存储系统是 Kubernetes 的核心,是整个基于事件驱动的分布式系统的基石,一旦出现数据异常可能直接或间接地派生出一系列的故障。具体来说可能包括但不限于以下的常见极端风险问题:
- 存储集群运维操作失误导致存储下线,导致整个 Kubernetes 集群不可用;
- 管理员直接删除 etcd 中的数据,未经过 apiserver 做校验,可能导致一些非预期关键对象如 namespace、deployment、pod 等被直接删除,并触发对象的级联删除,导致业务大面积受损;
- 管理员因误操作直接修改 etcd 中的数据,损坏了数据格式导致 apiserver 无法 decode 数据。
针对这些问题,我们在生产环境中采取了一系列措施——首先,尽可能标准化地约束对存储集群的运维和数据操作,在存储系统侧开启 TLS 双向认证,尽量避免除了 Kubernetes 以外的用户直接访问存储,降低数据损坏或丢失的风险;其次,对存储进行定时的备份,在极端情况下,当发生不可逆的数据损失时,基于备份能快速恢复数据,降低损失的影响;此外,通过对其他组件进行加固,尽可能降低数据异常派生的非预期事件对于业务的直接冲击。
控制面防护
自动化组件与 apiserver 之间的交互风险,主要集中在非预期操作方面。正常情况下,用户或平台将预期的状态提交到 apiserver,而其他内部组件将立即根据当前状态和预期状态的区别派生出一系列的动作,从而使集群产生变更;而一旦错误的预期状态被提交,集群将快速并且难以逆转地朝着目标状态进行变更。
针对这一类问题的主要防护思路,就是对关键对象的操作进行一些额外的限制,例如要求在操作时额外添加一些冗余操作,形成 double check 机制,降低由于误操作或者管控代码 bug 引发风险的概率;具体来说,操作防护通过 Kubernetes 原生提供的扩展机制 ValidatingAdmissionWebhook 来实现。我们通过 label 和 annotation 来标记需要进行操作防护的关键对象,并通过 selector 配置对这些关键对象以及对应的操作进行筛选,在 Webhook 中实现一系列的约束以达到防护的目的,其中包括但不限于以下这些策略:
- 防止级联删除针对 Namespace、CRD 等根对象,一旦被删除会导致级联地触发派生出的其他对象的删除操作。因此我们在 Webhook 中对这些类型的关键对象的删除进行拦截,避免误操作引发级联删除操作引发灾难性后果。
- 显式副本修改当需要调整关键
workload 资源副本数量时,为了避免意外地将副本数量缩减至 0,我们要求在通过 UPDATE 或者 PATCH
请求调整副本数的同时,还需要显式地给对象添加特定 annotation 写入预期调整的数值作为 double check;在 Webhook
中校验关键 workload 对象进行变更时
.spec.replicas
字段中的值是否与 annotation 中提供的值保持一致,确保任何对于关键 workload 副本数的修改都是有意且明确的。 - 显式资源删除当需要删除关键 workload 对象时,要求在删除对象之前先通过修改操作将 workload 的副本数降至 0;通过这种约束,我们得以避免一些误操作,例如某些关键的 workload 对象未经确认直接,可能会触发更多级联的删除操作,导致业务受损。
- 操作程序约束对于一些特定的业务,对于业务规格的变更有着严格的变更事件窗口限制,例如业务只接受在非繁忙时段对镜像、环境变量等配置进行变更,这样可以降低因为规格更改引起的潜在问题,以及相应的业务中断风险。我们通过 CRD 定义了可变更窗口、可变更字段等约束并暴露给用户,在 Webhook 中根据用户配置进行相应的校验,这样可以确保在出现故障时,影响尽量少的终端用户,确保有相对充分的故障处理时间,最大程度的减少潜在损失,降低系统风险。
此外,线上生产环境中经常会遇到一些客户端的异常,例如 OOM、大量缓存穿透等问题,这些异常往往会引发大量的开销极大的读请求,引发控制面异常甚至雪崩。针对线上异常流量的防护问题,我们对用户行为进行了一定限制,禁止了一些开销极大的读穿透行为。其次,我们在控制面前置了针对 kube-apiserver 流量特征专门定制的七层网关 KubeGateway,它解决了 kube-apiserver 负载不均衡的问题,同时实现了对 kube-apiserver 请求的完整治理,包括请求路由、分流、限流、降级等,显著提高了 Kubernetes 集群的可用性。另外,我们对 Kubernetes 的审计日志进行扩展,将一些流量相关的信息附加到审计日志上,在此基础上进行分析得到用户画像。在异常的场景下,将用户画像、流量监控指标与控制面前置的七层网关 KubeGateway 的限流能力相结合,对给控制面提供巨大压力的 Client 进行流量控制,尽可能降低雪崩风险。
节点防护
在大多数场景下,pod 的删除应该分成两个阶段执行:首先由中心化的 Controller 或者用户通过发起 Delete 请求将 pod 标记为删除状态(即添加 DeletionTimestamp),然后应该由 kubelet 负责对业务发起优雅退出,等待业务终止且资源释放之后,由 kubelet 来通过 APIServer 提供的接口将 pod 彻底移除。但在生产实践中,我们遇到诸多了问题,可能导致 kubelet 因为异常而非预期地终止业务 pod,例如:
- 由于配置错误或者代码 bug,导致 kubelet 重启后 reject 正在运行的业务 pod,导致业务受损;
- 由于控制面存储出现的数据损坏或其他异常,导致 kubelet 发现本地实际运行的 pod 与控制面提供的本地应该运行的 pod 不一致,进而引起非预期的业务退出。
针对这类问题,我们对 kubelet 进行了一系列的改造,涵盖 admit、housekeeping 等环节。通过改造给 kubelet 删除 pod 的操作加入前置约束:在尝试删除关键 pod 时,首先检查 pod 是否被显式地进行标记删除,如果 pod 未被标记删除,则不允许 kubelet 触发 pod 的删除操作。基于这种显式删除的约束,我们得以大幅度降低因为各种 Kubernetes 组件异常而引发的节点层面的业务运行风险。
小结
在生产环境中,我们主要根据 Kubernetes 组件之间的交互过程识别和梳理出关键风险,通过特定的 label 与 annotation 对关键的对象进行标记,分别采取措施进行一定的加固:
- 数据防护主要是约束运维操作,收敛数据访问入口,标准化存储操作的各种行为以减小风险;
- 控制面防护主要是通过定制 ValidatingAdmissionWebhook 进行扩展,在对于一些关键对象的变更过程中,要求主动引入一些冗余的操作与校验,降低误操作风险;
- 节点防护主要是通过对 kubelet 的进行改造,严格约束关键 pod 必须显式删除,降低极端情况下的系统性风险。
应用案例
字节基于原生 Kubernetes 生态定制了较多的功能以支持个性化的场景,整体的研发、迭代和交付的效率都非常高,对集群稳定性造成更大的挑战,即使在交付流程规范上严格把控,也不能完全杜绝异常情况下的极端异常风险;结合实践过程出现过的故障案例和场景诉求,字节云原生团队从元集群、控制面、数据面、业务定制等多个角度,构建了较为全面的防御体系,有效避免线上大规模事故的发生。
数据防护:元集群级联删除
字节内部的集群数量众多,为实现自动化运维和集群管理,需要构建元集群描述业务集群的状态;在这种情况下,元集群自身的异常可能会触发更大范围的故障。在字节早期,集群缺乏防护能力,SRE 在运维过程中使用过高权限,误删除了某个 region 元集群中用于描述 Node 状态的 CRD,因为没有防御系统拦截,CRD 被删除后会引发全量 CR 的级联删除,导致元集群控制器认为几乎所有的节点都需要下线,引发全量 pod 物理停服。该次故障最终引发单 region 生产集群在 30 分钟内持续标记删 3W+ 节点,实际删除 9K 节点后及时止损,影响面巨大且手动止损窗口很短。在该案例中,接入防御体系能够实现在多个点位实现防御能力
- 前置拦截:通过标记 CRD 为 critial 避免全量误删除引发级联问题;
- 集群下线限流:集群大范围下线通常并不是常见的运维操作,控制节点下线的频率和安全水位,保证即使出现异常的级联删除行为,也能够尽量控制故障域;
- 数据备份和恢复:当发生物理对象删除行为后,能够通过备份数据实现快速的恢复。
控制面防护:异常流量识别与限流
控制面异常通常源自于不合理的客户端行为和不够准确的服务端资源预估,由于场景过于复杂,在缺乏精细治理的情况下,最终因各种原因导致服务端过载;通常从现象上,会伴随着客户端大量的 List 请求和 APIServer OOM,进一步引发全量客户端 Relist,恶性循环直至集群出现雪崩。对于控制面的极端异常,字节内部通过接入 7 层的 gateway ,配合全链路的自动化流量 tracing,实现灵活智能的 API 请求防护
- 常态限流:针对客户端和资源对象的组合和常态流量分析,定制限流规则,避免瞬时大量请求对服务端的压力
- 容灾场景熔断:当集群出现明显异常或者雪崩时,通过手动熔断止损,并逐步放开限流以恢复集群正常
节点防护:异常版本升级触发大面积驱逐
相对于控制面,数据面的版本和配置通常更加复杂多样,迭代通常会更加频繁,更容易因为不当的组件运维操作引发不可预期的极端风险。某次 SRE 在升级 Kubelet 版本的过程中,应用了不符合预期的混部资源配置,在 Kubelet 重启后,大量 Running 中的 pod 因为资源归属识别错误,导致 admit 失败而被 delete,同时,原生的 delete API 不过 PDB 拦截,预期会引发大量业务容量的损失;但由于已经上线防护能力,最终没有引发严重的线上问题。在该案例中,接入防御体系能够同时在单机和中心上提供防御能力
- 单机拦截:对于已经处于 Running 状态的核心服务,默认补充 explict-deletion 标签,确保只有显式地通过 API 标记删除 (设置 deletionTimestamp),能够保证因为数据面异常发版后,不影响业务实例的运行,给人为介入处理提供足够的时间
- 中心拦截:对于核心服务补充 Delete 与 DeleteCollection 两种 API 进行校验,避免类似非预期的删除 pod 行为对业务造成影响
后续规划
字节防护实践未来会逐渐集成火山引擎 VKE 产品中,为云上的服务提供更加可靠的稳定性保证;除此之外,我们也会持续增强云原生防护的功能特性,收敛并解决更多可能对云上服务造成稳定性风险的场景,包括如下内容
- 控制面 Delete pod API 防护内建的 PDB 防护机制仅作用于 Evict pod API,校验性能不佳。当存在大量 PDB 对象时,Evict pod API 耗时会大幅度劣化,请求时延远超 Delete pod,因此有很多组件刻意不使用 Evict pod 而直接 Delete pod,例如调度器发起抢占等。由于控制面 Delete pod 的内置校验较少,直接使用该接口容易导致业务 pod 的健康比例低于预期,影响业务正常运行。为避免这类风险,我们一方面需要优化 Evict pod 的性能,另一方面需要通过扩展对 Delete pod 操作进行更严格的校验,以保证业务运行的 pod 健康比例不低于预期。
- 收敛静态校验策略当前我们在控制面做的防护工作主要依托于对 Validating Admission Webhook 机制,这一方面会 apiserver 在处理请求过程中引入额外的外部过程,提高延迟与出错概率,另一方面也会一定程度提高集群运维的复杂度。在 Kubernetes 1.26 版本中,引入了新的 Admission Plugin,支持使用 CEL (Common Expression Language)对请求进行一些静态校验。后续我们会将控制面防护的一些冗余操作校验迁移到 CEL,对上述问题进行改善。
- 场景定制防护策略对于 Redis 和分布式训练等带存储状态的业务来说,其编排模型和运维方案上有比较多的定制需求,为此,防御体系需要针对其业务特点 (如存储分片、纵向资源调整、原地重启等),补充完善更多精细化的策略以匹配特有的极端异常风险。
总结
本文主要介绍了字节跳动内部生产环境中 Kubernetes 应用过程中发现的主要系统风险与提出一系列防护措施。具体来说,我们从 Kubernetes 组件的交互过程的角度出发,划分为数据、控制面、节点三个层面,并通过具体示例说明了常见问题,包括误操作和管控组件版本错误等等,并且针对这些常见问题,简单介绍了我们构建的一系列防御性措施,包括但不限于,约束组件访问权限、主动添加冗余操作与相关校验等等。通过这些防御性措施,我们能够降低已知问题给业务带来的风险,为业务提供稳定的基础服务。
除了必要的防御性加固措施,日常维护集群时的标准化变更流程也至关重要。通过控制集群规模并充分进行灰度验证,可以降低故障的影响范围。在生产环境中,只有综合利用系统自我防御性措施和标准化运维等多种手段,才能最大程度地降低风险和故障损失。