干扰(Disruptions)

284 阅读14分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

自愿干扰和非自愿干扰

Pod不会消失,除非有人(用户或控制器)将其销毁,或者出现了不可避免的硬件或软件系统错误。

我们把这些不可避免的情况称为应用的非自愿干扰(Involuntary Disruptions)。例如:

  • 节点下层物理机的硬件故障
  • 集群管理员错误地删除虚拟机(实例)
  • 云提供商或虚拟机管理程序中的故障导致的虚拟机消失
  • 内核错误
  • 节点由于集群网络隔离从集群中消失
  • 由于节点资源不足导致Pod被驱逐

除了资源不足的情况,大多数用户应该都熟悉这些情况;他们不是特定于Kubernetes的。

我们称其他情况为自愿干扰(Voluntary Disruptions)。包括由应用程序所有者发起的操作和由集群管理员发起的操作。典型的应用程序所有者的操作包括:

  • 删除Deployment或其他管理Pod的控制器
  • 更新了Deployment的Pod模版导致Pod重启
  • 直接删除Pod(例如:误删除)

集群管理员操作包括:

  • 排空(drain)节点进行修复和升级
  • 从集群中排空节点以缩小集群
  • 从节点中移除一个Pod,以允许其他Pod使用该节点

这些操作可能由集群管理员直接执行,也可能由集群管理员所使用的自动化工具执行,或者由集群托管提供商自动执行。

咨询集群管理员或联系云提供商,或者查询发布文档,以确定是否为集群启用了任何资源干扰源。 如果没有启用,可以不用创建 Pod Disruption Budgets(Pod 干扰预算)

注意:并非所有的自愿干扰都会受到Pod干扰预算的限制。例如:删除Deployment或Pod的删除操作就会跳过Pod干扰预算检查。

处理干扰

以下是减轻非自愿干扰的一些方法:

  • 确保Pod在请求中给出所需资源
  • 如果需要更高的可用性,请复制应用程序
  • 为了在运行复制应用程序时获得更高的可用性,请跨机架(使用反亲和性或跨区域)扩展应用程序

自愿干扰的频率各不相同。在一个基本的 Kubernetes 集群中,没有自愿干扰(只有用户触发的干扰)。 然而,集群管理员或托管提供商可能运行一些可能导致自愿干扰的额外服务。例如,节点软 更新可能导致自愿干扰。另外,集群(节点)自动缩放的某些 实现可能导致碎片整理和紧缩节点的自愿干扰。集群 管理员或托管提供商应该已经记录了各级别的自愿干扰(如果有的话)。 有些配置选项,例如在 pod spec 中 使用 PriorityClasses 也会产生自愿(和非自愿)的干扰。

Kubernetes 提供特性来满足在出现频繁自愿干扰的同时运行高可用的应用程序。我们称这些特性为 干扰预算(Disruption Budget)

干扰预算

特性状态 :Kubernetes v1.21 [stable]

即使你会经常引入自愿性干扰,Kubernetes 也能够支持你运行高度可用的应用。

应用程序所有者可以为每个应用程序创建 PodDisruptionBudget 对象(PDB)。 PDB 将限制在同一时间因自愿干扰导致的复制应用程序中宕机的 Pod 数量。 例如,基于票选机制的应用程序希望确保运行的副本数永远不会低于仲裁所需的数量。 Web 前端可能希望确保提供负载的副本数量永远不会低于总数的某个百分比。

集群管理员和托管提供商应该使用遵循 PodDisruptionBudgets 的接口 (通过调用Eviction API), 而不是直接删除 Pod 或 Deployment。

例如,kubectl drain 命令可以用来标记某个节点即将停止服务。 运行 kubectl drain 命令时,工具会尝试驱逐机器上的所有 Pod。 kubectl 所提交的驱逐请求可能会暂时被拒绝,所以该工具会定时重试失败的请求, 直到所有的 Pod 都被终止,或者达到配置的超时时间。

PDB 指定应用程序可以容忍的副本数量(相当于应该有多少副本)。 例如,具有 .spec.replicas: 5 的 Deployment 在任何时间都应该有 5 个 Pod。 如果 PDB 允许其在某一时刻有 4 个副本,那么驱逐 API 将允许同一时刻仅有一个而不是两个 Pod 自愿干扰。

使用标签选择器来指定构成应用程序的一组 Pod,这与应用程序的控制器(Deployment,StatefulSet 等) 选择 Pod 的逻辑一样。

Pod 控制器的 .spec.replicas 计算“预期的” Pod 数量。 根据 Pod 对象的 .metadata.ownerReferences 字段来发现控制器。

PDB无法防止非自愿干扰,但他们确实计入预算。

由于应用程序的滚动升级而被删除或不可用的Pod确实会计入干扰预算,但是控制器(如 Deployment 和 StatefulSet)在进行滚动升级时不受 PDB 的限制。应用程序更新期间的故障处理方式是在对应的工作负载资源的 spec 中配置的。

当使用驱逐 API 驱逐 Pod 时,Pod 会被体面地 终止,期间会 参考 PodSpec中的 terminationGracePeriodSeconds 配置值。

PDB例子

假设集群有3个节点,node-1node-3。集群运行了一些应用。其中一个应用有三个副本,分别是pod-a,pod-b,pod-c。另外,还有一个不带PDB的无关pod pod-x也同样显示出来。最初所有的Pod分布如下:

node-1node-2node-3
pod-a avaliablepod-b avaliablepod-c avaliable
pod-x avaliable

3个Pod都是deploy的一部分,并且共同拥有同一个PDB,要求3个Pod中至少有2个始终处于可用状态。

例如,假设集群管理员想要重启系统,升级内核版本来修复内核中的权限。集群管理员首先使用kubectl drain命令尝试排空node-1节点。命令尝试驱逐pod-apod-x。操作立即就成功了。两个Pod同时进入terminating状态。这时的集群处于下面的状态:

node-1 drainingnode-2node-3
pod-a terminatingpod-b avaliablepod-c avaliable
pod-x terminating

Deployment 控制器观察到其中一个 Pod 正在终止,因此它创建了一个替代 Pod pod-d。 由于 node-1 被封锁(cordon),pod-d 落在另一个节点上。 同样其他控制器也创建了 pod-y 作为 pod-x 的替代品。

(注意:对于StatefulSet来说,pod-a(也称为pod-0)需要在替换Pod创建之前完全终止,替代它的也称为pod-0,但是具有不同的UID。除此之外,此示例也适用于StatefulSet。)

当前集群的状态如下:

node-1 drainingnode-2node-3
pod-a terminatingpod-b availablepod-c available
pod-x terminatingpod-d startingpod-y

在某一时刻,Pod被终止,集群如下所示:

node-1 drainednode-2node-3
pod-b availablepod-c available
pod-d startingpod-y

此时,如果一个急躁的集群管理员试图排空(drain)node-2node-3,drain 命令将被阻塞, 因为对于 Deployment 来说只有 2 个可用的 Pod,并且它的 PDB 至少需要 2 个。 经过一段时间,pod-d 变得可用。

集群状态如下所示:

node-1 drainednode-2node-3
pod-b availablepod-c available
pod-d availablepod-y

现在,集群管理员试图排空(drain)node-2。 drain 命令将尝试按照某种顺序驱逐两个 Pod,假设先是 pod-b,然后是 pod-d。 命令成功驱逐 pod-b,但是当它尝试驱逐 pod-d时将被拒绝,因为对于 Deployment 来说只剩一个可用的 Pod 了。

Deployment 创建 pod-b 的替代 Pod pod-e。 因为集群中没有足够的资源来调度 pod-e,drain 命令再次阻塞。集群最终将是下面这种状态:

node-1 drainednode-2node-3no node
pod-b terminatingpod-c availablepod-e pending
pod-d availablepod-y

此时,集群管理员需要增加一个节点到集群中以继续升级操作。

可以看到Kubernetes根据以下条件改变干扰发生的速率:

  • 应用程序需要多少个副本
  • 优雅关闭应用实例需要多长时间
  • 启动应用新实例需要多长时间
  • 控制器的类型
  • 集群的资源能力

分离集群所有者和应用所有者角色

通常,将集群管理者和应用所有者视为彼此了解有限的独立角色是很有用的。这种责任分离在下面这些场景下是有意义的:

  • 当有许多应用程序团队共用一个 Kubernetes 集群,并且有自然的专业角色
  • 当第三方工具或服务用于集群自动化管理

Pod 干扰预算通过在角色之间提供接口来支持这种分离。

如果你的组织中没有这样的责任分离,则可能不需要使用 Pod 干扰预算。

如何在集群上执行干扰性操作

如果你是集群管理员,并且需要对集群中的所有节点执行干扰操作,例如节点或系统软件升级,则可以使用以下选项

  • 接受升级期间的停机时间。
  • 故障转移到另一个完整的副本集群。
    • 没有停机时间,但是对于重复的节点和人工协调成本可能是昂贵的。
  • 编写可容忍干扰的应用程序和使用 PDB。
    • 不停机。
    • 最小的资源重复。
    • 允许更多的集群管理自动化。
    • 编写可容忍干扰的应用程序是棘手的,但对于支持容忍自愿干扰所做的工作,和支持自动扩缩和容忍非 自愿干扰所做工作相比,有大量的重叠

配置Pod干扰预算

确定要保护的应用

用户想要保护通过内置的Kubernetes控制器指定的应用,这是最常见的使用场景:

  • Deployment
  • ReplicationController
  • ReplicaSet
  • StatefulSet

在这种情况下,在控制器的.spec.selector字段中做记录,并在PDB的.spec.selector字段中加入同样的选择算符。

用户也可以用PDB来保护不受上述控制器控制的Pod,或任意的Pod集合。

应用对干扰的反应

确定在自发干扰时,多少实例可以在短时间内同时关闭:

  • 无状态的前端:
    • 关注:不能降低服务能力的10%以上
    • 解决方案:使用PDB,指定其minAvailable的值为90%
  • 单实例有状态应用:
    • 关注:不要在不通知的情况下终止该应用
    • 解决方案1:不实用PDB,并忍受偶尔的停机
    • 解决方案2:设置maxUnavailable=0的PDB,意为(Kubernetes 范畴之外的)集群操作人员需要在终止应用前与用户协商, 协商后准备停机,然后删除 PDB 表示准备接受干扰,后续再重新创建。
  • 多实例有状态的应用,如Consul、Etcd:
    • 关注:不要将实例数量减少至低于仲裁规模,否则将出现写入失败。
    • 解决方案1:设置maxUnavailable值为1(适用于不同规模的应用)
    • 解决方案2:设置minAvailable值为仲裁规模(例如规模为5时设置为3)
  • 可重新启动的批处理任务:
    • 关注:自发干扰的情况下,需要确保任务完成
    • 解决方案:不创建PDB。任务管理器会创建一个替代Pod
指定百分比时的舍入逻辑

minAvailablemaxUnavailable 的值可以表示为整数或百分比。

  • 指定整数值时,它表示 Pod 个数。例如,如果将 minAvailable 设置为 10, 那么即使在干扰期间,也必须始终有 10 个Pod可用。
  • 通过将值设置为百分比的字符串表示形式(例如 “50%”)来指定百分比时,它表示占总 Pod 数的百分比。 例如,如果将 "minUnavailable" 设置为 “50%”,则干扰期间只允许 50% 的 Pod 不可用。

如果将值指定为百分比,则可能无法映射到确切数量的 Pod。例如,如果你有 7 个 Pod, 并且你将 minAvailable 设置为 "50%",具体是 3 个 Pod 或 4 个 Pod 必须可用 并非显而易见。 Kubernetes 采用向上取整到最接近的整数的办法,因此在这种情况下,必须有 4 个 Pod。

指定PodDisruptionBudget

一个PodDisruptionBudget有三个字段:

  • 标签选择算符 .spec.selector 用于指定其所作用的 Pod 集合,该字段为必需字段。
  • .spec.minAvailable 表示驱逐后仍须保证可用的 Pod 数量。即使因此影响到 Pod 驱逐 (即该条件在和 Pod 驱逐发生冲突时优先保证)。 minAvailable 值可以是绝对值,也可以是百分比。
  • .spec.maxUnavailable (Kubernetes 1.7 及更高的版本中可用)表示驱逐后允许不可用的 Pod 的最大数量。其值可以是绝对值或是百分比。

说明:

policy/v1beta1policy/v1 API 中 PodDisruptionBudget 的空选择算符的行为 略有不同。在 policy/v1beta1 中,空的选择算符不会匹配任何 Pods,而 policy/v1 中,空的选择算符会匹配名字空间中所有 Pods。

用户在同一个 PodDisruptionBudget 中只能够指定 maxUnavailableminAvailable 中的一个。 maxUnavailable 只能够用于控制存在相应控制器的 Pod 的驱逐(即不受控制器控制的 Pod 不在 maxUnavailable 控制范围内)。在下面的示例中, “所需副本” 指的是相应控制器的 scale,控制器对 PodDisruptionBudget 所选择的 Pod 进行管理。

示例 1:设置 minAvailable 值为 5 的情况下,驱逐时需保证 PodDisruptionBudget 的 selector 选中的 Pod 中 5 个或 5 个以上处于健康状态。

示例 2:设置 minAvailable 值为 30% 的情况下,驱逐时需保证 Pod 所需副本的至少 30% 处于健康状态。

示例 3:设置 maxUnavailable 值为 5 的情况下,驱逐时需保证所需副本中最多 5 个处于不可用状态。

示例 4:设置 maxUnavailable 值为 30% 的情况下,驱逐时需保证所需副本中最多 30% 处于不可用状态。

在典型用法中,干扰预算会被用于一个控制器管理的一组 Pod 中 —— 例如:一个 ReplicaSet 或 StatefulSet 中的 Pod。

说明:

干扰预算并不能真正保证指定数量/百分比的 Pod 一直处于运行状态。例如: 当 Pod 集合的 规模处于预算指定的最小值时,承载集合中某个 Pod 的节点发生了故障,这样就导致集合中可用 Pod 的 数量低于预算指定值。预算只能够针对自发的驱逐提供保护,而不能针对所有 Pod 不可用的诱因。

设置 maxUnavailable 值为 0%(或 0)或设置 minAvailable 值为 100%(或等于副本数) 可能会阻塞节点,导致资源耗尽。按照 PodDisruptionBudget 的语义,这是允许的。

使用minAvailable的PDB示例:

# mypdb.yaml

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: myapp-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: myapp

使用maxUnavailable的PDB示例:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: myapp-pdb
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: myapp

例如,如果上述 myapp-pdb 选择的是一个规格为 3 的 StatefulSet 对应的 Pod, 那么上面两种规范的含义完全相同。 推荐使用 maxUnavailable ,因为它自动响应控制器副本数量的变化。

创建PDB对象

可以使用kubectl创建PDB对象,PDB对象无法更新,必须删除后重新创建。

kubectl apply -f mypdb.yaml
检查PDB的状态

同样我们使用kubectl进行查看:

kubectl get -f mypdb.yaml

假设命名空间下没有匹配app:myapp的pod,用户会看到类似下面的信息:

NAME        MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
myapp-pdb   2               N/A               0                     2m46s

假设有匹配的Pod,那么可能会看到类似下面的信息:

NAME        MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
myapp-pdb   2               N/A               1                     8s

ALLOWED-DISRUPTIONS 值非 0 意味着干扰控制器已经感知到相应的 Pod,对匹配的 Pod 进行统计, 并更新了 PDB 的状态。

任意控制器和选择算符

如果对应的Pod由其他类型(Deployment、ReplicationController、ReplicaSet和StatefulSet之外的控制器)的控制器控制,也可能由operator控制,或者“裸的(不受控制器控制)”的Pod,该类Pod存在以下限制:

  • 只能够使用.spec.minAvailable,而不能够使用.spec.maxUnavailable
  • 只能够使用整数作为.spec.minAvailable的值,而不能使用百分比

参考链接

kubernetes.io/zh-cn/docs/…

kubernetes.io/zh-cn/docs/…