字节跳动云原生防护体系实践

容器服务云安全K8s

picture.image

本文主要介绍了字节跳动内部生产环境中 Kubernetes 应用过程中发现的主要系统风险,以及提出的相应的一系列防护措施。

来源 | 字节跳动基础架构团队

随着 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 与单机节点组件间的交互。

picture.image

根据上述分层,我们可以针对性梳理出一系列常见的系统性风险,并分别采取对应的措施进行加固以规避极端风险。

数据防护

在 KV 存储层,存储与 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 分钟内持续标记删数万节点,实际删除数千节点后及时止损 ,影响面巨大且手动止损窗口很短。

现在我们回顾这起事故,可以发现如果当时接入了现在的防护能力,我们就能够在多个点位实现防御:

  • 前置拦截:通过标记 CRD 为 critial 避免全量误删除引发级联问题;
  • 集群下线限流:集群大范围下线通常并不是常见的运维操作,控制节点下线的频率和安全水位,保证即使出现异常的级联删除行为,也能够尽量控制故障域;
  • 数据备份和恢复:当发生物理对象删除行为后,能够通过备份数据实现快速恢复。

picture.image

控制面防护:异常流量识别与限流

控制面异常通常源自于不合理的客户端行为和不够准确的服务端资源预估,由于场景过于复杂,在缺乏精细治理的情况下,最终因各种原因导致服务端过载。从现象上,控制面异常往往会伴随客户端大量的 List 请求和 APIServer OOM,进一步引发全量客户端 Relist,恶性循环直至集群出现雪崩。

如前文介绍,字节跳动内部应对控制面极端异常风险的防护手段是接入 7 层的 gateway ,配合全链路的自动化流量 tracing,实现灵活智能的 API 请求防护:

  • 常态限流:针对客户端和资源对象的组合和常态流量分析,定制限流规则,避免瞬时大量请求对服务端的压力;
  • 容灾场景熔断:当集群出现明显异常或者雪崩时,通过手动熔断止损,并逐步放开限流以恢复集群正常。

picture.image

节点防护:异常版本升级触发大面积驱逐

相对于控制面,数据面的版本和配置通常更加复杂多样,迭代也会更加频繁,更容易因为不当的组件运维操作引发不可预期的极端风险。

仍以内部事故为例,某次 SRE 在升级 kubelet 版本的过程中,应用了不符合预期的混部资源配置,在 kubelet 重启后,大量 Running 中的 pod 因为资源归属识别错误,导致 admit 失败而被 delete,同时,原生的 delete API 不过 PDB 拦截,预期会引发大量业务容量的损失。由于当时已经上线防护能力,最终没有引发严重的线上问题。

在该案例中,接入防御体系能够同时在单机和中心上提供防御能力:

  • 单机拦截:对于已经处于 Running 状态的核心服务,默认补充 explict-deletion 标签,确保只有显式地通过 API 标记删除 (设置 deletionTimestamp),能够保证因为数据面异常发版后,不影响业务实例的运行,给人为介入处理提供足够的时间。
  • 中心拦截:对于核心服务补充 Delete 与 DeleteCollection 两种 API 进行校验,避免类似非预期的删除 pod 行为对业务造成影响。

picture.image

后续规划

稳定性治理是一个长期工程,未来,字节跳动基础架构团队会持续增强云原生防护的功能特性,收敛并解决更多可能对云上服务造成稳定性风险的场景,包括如下内容:

控制面 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 和分布式训练等带存储状态的业务来说,其编排模型和运维方案有比较多定制需求,为此,防御体系需要针对其业务特点,如存储分片、纵向资源调整、原地重启等,补充完善更多精细化的策略以匹配特有的极端异常风险。

同时,字节跳动云原生防护实践也会逐渐集成到火山引擎容器服务 VKE中,为云上服务提供更加可靠的稳定性保证,这块的进展我们会在后续文章中介绍。

总结

本文主要介绍了字节跳动内部生产环境中 Kubernetes 应用过程中发现的主要系统风险,并提出了一系列防护措施。

具体来说,我们从 Kubernetes 组件的交互过程的角度出发,划分为数据、控制面、节点三个层面,并通过具体示例说明了常见问题,包括误操作和管控组件版本错误等等,并且针对这些常见问题,简单介绍了我们构建的一系列防御性措施,包括但不限于,约束组件访问权限、主动添加冗余操作与相关校验等等。通过这些防御性措施,我们能够降低已知问题给业务带来的风险,为业务提供稳定的基础服务。

除了必要的防御性加固措施,日常维护集群时的标准化变更流程也至关重要。通过控制集群规模并充分进行灰度验证,技术团队可以降低故障的影响范围。在生产环境中,只有综合利用系统自我防御性措施和标准化运维等多种手段,才能最大程度降低风险和故障损失。

如需技术交流,欢迎添加字节跳动云原生小助手,加入云原生社群:

picture.image

相关链接

[1] 火山引擎: www.volcengine.com

[2] 火山引擎 VKE: www.volcengine.com/product/vke

火山引擎云原生团队

火山引擎云原生团队主要负责火山引擎公有云及私有化场景中 PaaS 类产品体系的构建,结合字节跳动多年的云原生技术栈经验和最佳实践沉淀,帮助企业加速数字化转型和创新。产品包括容器服务、镜像仓库、分布式云原生平台、函数服务、服务网格、持续交付、可观测服务等。

picture.image

picture.image

picture.image

93
0
0
0
关于作者
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论