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