当 Kubernetes 在 集群中启动和调度工作负载时(例如在更新或扩容期间),可能会看到 Pending Pod数量出现短暂的峰值 。只要集群有足够的资源,Pending Pod 通常会自行转换为 Running 状态,因为 Kubernetes 调度程序会将它们分配给合适的节点。但是,在某些情况下,Pending Pod 无法自动转换为Running状态,需要手动解决潜在问题才行。
下面我们一起探索各种方法来分析和解决Pending Pod,一般原因有下面几种
- 基于节点的调度约束,包括就绪性和污点
- Pod 请求的资源超出可分配容量
- 持久卷相关问题
- Pod 亲和性或反亲和性规则
- 滚动更新部署设置
Kubernetes Pod 的几种状态
在继续之前,我们先简要了解一下 Kubernetes 中 Pod 状态更新的工作原理。Pod 可以处于以下任何阶段:
- Pending: Pod 正在等待在节点上进行调度,或者等待其至少一个容器进行初始化。
- Running: Pod 已被分配到一个节点,并且有一个或多个正在运行的容器。
- Succeeded: Pod 的所有容器都退出且没有错误。
- Failed: Pod 的一个或多个容器因错误而终止。
- Unknown:通常在 Kubernetes API 服务器无法与 Pod 节点通信时发生。
pod 在对象的字段status.phase中显示其status。您可以使用此字段按status过滤 Pod,如以下命令所示:
$ kubectl get pods --field-selector=status.phase=Pending
NAME READY STATUS RESTARTS AGE
wordpress-5ccb957fb9-gxvwx 0/1 Pending 0 3m38s
当 Pod 等待调度时,它仍处于 Pending 阶段。使用kubectl describe pod <PENDING_POD_NAME>
了解 Pod 为何陷入 Pending 状态
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 2m (x26 over 8m) default-scheduler 0/39 nodes are available: 1 node(s) were out of disk space, 38 Insufficient cpu.
如果无法创建待处理 Pod,该FailedScheduling事件会在“消息”列中解释原因。在这种情况下,我们可以看到调度程序找不到任何具有足够资源节点来运行 Pod。
Kubernetes scheduling predicates
虽然FailedScheduling事件提供了对问题所在的一般描述,但更深入地了解 Kubernetes 如何做出调度决策有助于确定 Pending Pod 无法调度的原因。调度程序是 Kubernetes 控制平面的一个组件,它使用predicates来确定哪些节点有资格托管 Pending Pod。为了提高效率,调度程序按特定顺序评估predicates,保存更复杂的决策。如果调度程序在检查这些predicates后找不到任何符合条件的节点,则 pod 将保持 Pending 状态,直到有合适的节点可用。
接下来,我们将研究其中一些predicates如何导致调度失败。
基于节点的调度约束
多个predicates旨在检查节点是否满足某些要求,然后再允许在节点上调度 Pending Pod。在本节中,我们将介绍节点无法满足这些要求并被视为没有资格托管 Pending Pod 的一些原因:
- 节点不可调度
- 节点未就绪
- 节点标签与 Pod 的节点选择器或关联规则冲突
- 节点的污点与 Pod 的容忍度相冲突
不可调度的节点
如果您cordon节点(例如,准备节点升级),Kubernetes 会将其标记为不可调度。被cordon的节点可以继续托管它已经运行的 Pod,但它不能接受任何新的 Pod,即使它通常可以满足所有其他调度predicates。尽管不可调度的节点并不是调度失败的最常见原因,但如果是这种情况可能值得验证,而且它也是调度程序立即检查的第一个predicates。
如果由于节点不可调度而导致 Pending Pod 无法调度,您将在输出中看到类似以下内容kubectl:
$ kubectl describe pod <PENDING_POD_NAME>
[...]
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 2m8s (x4 over 3m27s) default-scheduler 0/1 nodes are available: 1 node(s) were unschedulable.
一旦被封锁,节点还将显示不可调度的污点(稍后将详细介绍污点)Unschedulable: true. 在其kubectl输出中:
$ kubectl describe node <NODE_NAME>
[…]
Taints: node.kubernetes.io/unschedulable:NoSchedule
Unschedulable: true
如果您仍然需要保持节点处于封锁状态,可以通过添加更多节点来托管 Pending Pod 来解决此问题。否则,您可以解除封锁(kubectl uncordon <NODE_NAME>
)。只要该节点满足其他调度predicates,这应该允许 pod 在该节点上进行调度。
节点状态
每个节点都有多个条件,可以是True、False或Unknown,具体取决于节点的状态。节点状况是调度决策的主要因素。例如,调度程序不会在报告有问题的资源相关的节点上调度 Pod。例如,DiskPressure 或者MemoryPressure 为True的节点)
同样,如果节点未报告ready 状态,调度程序将不会为其分配 Pending Pod。
一个节点可能会因为各种原因而未处于ready状态。在下面的示例中,该节点报告Unknown状态,因为其 kubelet停止中继任何节点状态信息。这通常表明节点崩溃或无法与控制平面通信。
$ kubectl describe node <UNREADY_NODE_NAME>
[…]
conditions:
- type: Ready
status: Unknown
lastHeartbeatTime: 2020-12-25T19:25:56Z
lastTransitionTime: 2020-12-25T19:26:45Z
reason: NodeStatusUnknown
message: Kubelet stopped posting node status.
节点标签
节点包括描述操作系统、实例类型(如果在云提供商上)和其他详细信息的内置标签。可以向节点添加自定义标签。无论它们是内置的还是自定义的,节点标签都可以在节点选择器和节点亲和性/反亲和性规则中使用,以影响调度决策。
节点选择器
如果您需要确保 pod 仅在特定类型的节点上调度,您可以向 pod 规范中添加节点选择器。例如,您可以使用节点选择器来指定某些 Pod 只能放置在运行与这些 Pod 工作负载兼容的操作系统或硬件类型的节点上。下面的 Pod 规范使用节点选择器来确保 Pod 只能放置在带有disktype: ssd标签的节点上。
如果没有可用节点与 Pending pod 规范中指定的节点选择器匹配,则该 pod 将保持 Pending 状态,直到有合适的节点可用。
调度程序在尝试查找与 pod 节点选择器匹配的节点时会考虑MatchNodeSelector predicates。如果调度不成功,您将看到类似以下内容的消息:0/1 nodes are available: 1 node(s) didn't match node selector
。请注意,如果故障是由节点亲和性或反亲和性规则(即字段而nodeAffinity.nodeSelectorTerms不是nodeSelector字段)引起的,也会出现相同的消息,因此您可能需要检查 Pod 规范以找到问题的确切原因。我们将在下一节中更详细地介绍后一种情况。
要使Pending Pod 得到调度,您需要确保至少一个可用节点包含与 Pod 的节点选择器(即 )匹配的标签,并且满足所有其他调度predicates。
节点亲和性和反亲和性规则
节点亲和性和反亲和性规则可以补充或取代节点选择器,作为一种更精确调度决策。与节点选择器一样,这些规则指示调度程序使用节点标签来确定哪些节点有资格(或没有资格)托管 Pod。
除了使用运算符来实现逻辑之外,您还可以配置硬 ( requiredDuringSchedulingIgnoredDuringExecution) 或首选 ( preferredDuringSchedulingIgnoredDuringExecution) 调度要求,为您的用例提供更多灵活性。
nodeAffinity节点亲和性和反亲和性规则在 Pod 规范中定义。下面的配置指定 Pod 只能在两个区域中进行调度。或者,您可以定义反关联性规则(例如,使用运算符NotIn指定不应将此工作负载安排在其中的任何区域)。
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- us-east-1a
- us-east-1b
由于该规则被定义为硬性要求,如果调度器找不到合适的节点,就会发生调度失败。该FailedScheduling事件将包含与调度程序无法找到与 pod 节点选择器匹配的节点时出现的相同消息:0/1 nodes are available: 1 node(s) didn't match node selector.
它不会指定是否涉及节点亲和性或反亲和性规则,因此您需要检查 pod 规范以获取更多信息。
节点亲和性和反亲和性规则可能导致调度失败。 要进行故障排除,您可以检查 Pod 规范以了解适用的规则。
为了降低节点亲和性或反亲和性规则触发调度失败的可能性,您可以使用preferredDuringSchedulingIgnoredDuringExecution而不是requiredDuringSchedulingIgnoredDuringExecution将规则定义为首选规则而不是必需规则
。请注意,如果使用此设置,则需要为首选项分配权重,以便调度程序可以使用此信息来确定哪些节点运行 pod.
Kubernetes 还允许您定义 pod 间亲和性和反亲和性规则,这些规则与节点亲和性和反亲和性规则类似,只不过它们考虑的是每个节点上运行的pod的标签,而不是节点标签。我们将在本文的后面部分探讨 Pod 间亲和性和反亲和性规则。
污染和容忍 (Taints and tolerations)
Kubernetes 允许您向节点添加污点,向 pod 添加容忍,以影响调度决策。将污点添加到节点后,该节点将只能托管具有兼容容忍度的 Pod(即容忍该节点污点的 Pod)。PodToleratesNodeTaints调度程序会查看 pod 的容忍度,并在评估predicates时取消任何具有冲突污点的节点的资格。
如果调度程序找不到合适的节点,您将看到一个FailedScheduling事件,其中包含有关违规污点的详细信息。
Tolerations: node-type=high-memory:NoSchedule
node.kubernetes.io/not-ready:NoExecute for 300s
node.kubernetes.io/unreachable:NoExecute for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 77s (x3 over 79s) default-scheduler 0/1 nodes are available: 1 node(s) had taint {node-typee: high-memory}, that the pod didn't tolerate.
在本例中,由于拼写错误,Pod 的容忍度 ( node-type=high-memory) 与节点的污点(node-typee: high-memory)不匹配,因此无法在该节点上进行调度。如果您修复了污点中的拼写错误,使其符合容忍度,Kubernetes 应该能够将 Pending pod 调度到预期的节点上。
同样重要的是要记住,如果未指定任何容忍度并且集群中的所有节点都受到污染,则 Pending pod 将无法调度。在这种情况下,您需要向 Pod 添加适当的容忍度或添加未受污染的节点,以避免调度失败。
为了防止导致调度问题的可能性,在向节点添加污点时,您可能需要将污点effect设置为PreferNoSchedule。这指定调度程序应尝试找到与 pod 的容忍度一致的节点,但如果需要,它仍然可以将 pod 调度到具有冲突污点的节点上。
Pod 请求的资源超出可分配容量
如果 pod 指定了资源请求(运行所需的最小 CPU 和/或Memeroy),Kubernetes 调度程序将尝试查找可以满足资源分配的节点。如果不成功,Pod 将保持 Pending 状态,直到有更多资源可用。
kubectl describe让我们回顾一下之前Pending Pod 输出的“Events”部分的示例:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 2m (x26 over 8m) default-scheduler 0/39 nodes are available: 1 node(s) were out of disk space, 38 Insufficient cpu.
在这种情况下,一个节点没有足够的磁盘空间,而集群中的其他 38 个节点没有足够的可分配 CPU 来满足该 pod 的请求 ( 38 Insufficient cpu)。
要调度此 Pod,您可以降低 Pod 的 CPU 请求、释放节点上的 CPU、解决节点上的磁盘空间问题DiskPressure,或者添加具有足够资源来满足 Pod 请求的节点。
同样重要的是要考虑调度程序将 pod 的请求与每个节点的可分配 CPU 和内存进行比较,而不是总 CPU 和内存容量。这考虑了节点上已经运行的任何 Pod,以及运行关键进程(例如 Kubernetes 本身)所需的任何资源。因此,如果 Pod 在运行c5.large AWS 实例(其 CPU 容量为 2 个核心)的集群上请求 2 个 CPU 核心,则该 Pod 将永远无法获得调度,除非您设置较低的 CPU 请求或将集群移至具有更多 CPU 容量的实例类型。
PersistentVolume相关的调度问题
PersistentVolumes(持久卷)旨在保持数据的持久性,超出任何需要访问它们的个别pod的生命周期。除了请求CPU和内存外,pod还可以通过在其规范中包含PersistentVolumeClaim来请求存储。PersistentVolume控制器负责绑定PersistentVolumes和PersistentVolumeClaims。当pod的PersistentVolumeClaim无法绑定到PersistentVolume或由于某种原因无法访问其PersistentVolume时,该pod将无法被调度。
接下来,我们将探讨一些PersistentVolumes出现问题可能导致pod调度失败的例子。
pod的PersistentVolumeClaim绑定到不同区域上的远程PersistentVolume pod的PersistentVolumeClaim可能包含一个存储类,其中包括卷的访问模式等要求。Kubernetes能够使用这些信息动态创建一个满足存储类要求的新PersistentVolume(例如,Amazon EBS卷)。或者,它可以找到一个符合存储类的可用PersistentVolume,并将其绑定到pod的PersistentVolumeClaim。
由于PersistentVolume控制器独立于调度器工作,它们可能做出相互冲突的决策。例如,PersistentVolume控制器可以将pod的PersistentVolumeClaim绑定到一个区域的PersistentVolume,而调度器同时将该pod分配给其他区域的一个节点。这将导致一个FailedScheduling事件,指示节点没有可用的卷区域,表明一个或多个节点不能满足NoVolumeZone。
让我们看另一个涉及到集群自动扩展器的例子。如果一个等待中的pod的PersistentVolumeClaim已经绑定到一个PersistentVolume,如果自动扩展器无法在与pod的PersistentVolume相同区域中添加一个节点(例如,因为已经达到了配置的最大节点总数或节点组的最大节点数),并且该区域中没有其他可用的节点来托管它,那么该pod将保持等待状态。如果发生这种情况,您可能会看到类似下面显示的日志,并伴随着一个NotTriggerScaleUp事件。
如果您的集群分布在多个区域内,Kubernetes文档建议在存储类的卷绑定模式中指定WaitForFirstConsumer。这样,持久卷控制器会等待调度程序选择符合Pod调度要求的节点,然后创建与选定节点的区域一致的持久卷,并将其绑定到Pod的持久卷声明上。如果与集群自动伸缩器一起使用此设置,建议为每个区域创建单独的节点组,这样新创建的节点就会位于正确的区域。当集群自动伸缩器为多区域节点组提供节点时,它无法指定节点应位于哪个区域 – 这由云提供商决定,因此新节点可能位于与等待中Pod的关联持久卷不同的区域。
由于WaitForFirstConsumer仅适用于新创建的持久卷,因此当Pod的持久卷声明已经与某个区域的持久卷绑定时,但调度程序将其分配给另一个区域的节点时,仍可能导致调度失败。
考虑以下情景:
- Pod A是StatefulSet的一部分,运行在区域0。
- 持久卷控制器将Pod A的持久卷声明绑定到区域0的持久卷上。
- 在下一次StatefulSet的更新中,Pod A的规范被修改,加入了一个节点亲和性规则,要求它在区域1进行调度。然而,它的持久卷声明仍然绑定到区域0的持久卷上。
- 由于持久卷声明仍然引用与亲和性规则冲突的持久卷,Pod A无法进行调度。
在这种情况下,您需要先删除Pod的持久卷声明,然后再删除Pod。需要注意的是,必须按照这个顺序完成两个步骤 – 如果您在没有删除持久卷声明的情况下删除Pod,控制器会重新创建Pod,但是持久卷声明仍然引用与Pod新分配的节点不同的区域的持久卷。一旦两者都删除了,StatefulSet控制器将重新创建Pod和持久卷声明,这样Pod就可以被正确调度了。
Pod 的 PersistentVolumeClaim 绑定到不可用的本地卷
Pod 可以使用本地持久卷来访问其节点的本地存储。与远程存储选项相比,本地存储提供了性能优势,但也可能导致调度问题。当节点被删除(即耗尽)或因任何原因变得不可用时,其 Pod 需要重新调度。但是,如果 pod 的 PersistentVolumeClaim 仍绑定到不可用节点上的本地存储,它将保持 Pending 状态并发出类似于以下内容的事件:
Warning FailedScheduling 11s (x7 over 42s) default-scheduler [...] 1 node(s) didn't find available persistent volumes to bind, [...]。
要让 Pod 重新运行,您需要删除 Local Persistent Volume和PersistentVolumeClaim,然后删除 Pod。这应该允许 Pod 重新调度。
Pod 的 PersistentVolumeClaim 请求超出本地存储容量
本地卷静态配置程序管理为本地存储创建的本地持久卷。如果您的 Pod 的 PersistentVolumeClaim 指定的请求超出了本地存储的容量,则该 Pod 将保持 Pending 状态。
$ kubectl get pv -l kubernetes.io/hostname=<HOSTNAME> -o json | jq -r '.items[].spec.capacity'
{ "storage": "137Gi" }
然后将其与 PersistentVolumeClaim 中请求的存储量进行比较:
$ kubectl get pvc <PVC_NAME> -o json | jq -r '.spec.resources'
{ "requests": { "storage": "300Gi" }}
在这种情况下,PersistentVolumeClaim 的请求(300Gi)超过了根据节点磁盘容量创建的本地持久卷(137Gi)的容量,因此 pod 的 PersistentVolumeClaim 永远无法得到满足。要解决此问题,您需要减少 PersistentVolumeClaim 请求,使其小于或等于节点上的分区/磁盘的容量,或者将节点升级到具有足够容量以满足 PersistentVolumeClaim 请求的实例类型。
Pod 间亲和性和反亲和性规则
Pod 间关联性和反关联性规则定义了 Pod 可以(或不可以)根据节点上已运行的其他 Pod 进行调度的位置。因为这对于调度程序来说是更密集的计算,所以它是调度程序将检查的最后一个predicates。这些规则补充了节点亲和性/反亲和性规则,并允许您进一步制定调度决策以适合您的用例。然而,在某些情况下,这些规则可能会导致调度失败。
Pod 间关联性规则指定某些 Pod 何时可以在同一节点(或其他拓扑组)上进行调度,换句话说,这些 Pod彼此之间具有关联性。例如,如果您希望应用程序 Pod 在运行应用程序所依赖的缓存的节点上运行,这可能会很有用。
如果将以下 pod 间亲和性规则添加到 Web 应用程序 pod 的规范中,则 Kubernetes 只能将pod调度到正在运行具有以下标签的 pod 的节点:topologyKeyapp: memcached
webapp-pod-spec.yaml
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- memcached
topologyKey: kubernetes.io/hostname
要调度此类 pod,除了满足其他调度predicates之外,您还需要确保节点已经运行memcached的 pod。否则,它将保持待定状态。
另一方面,Pod 间反关联性规则声明哪些 Pod不能在同一节点上运行。例如,您可能希望使用反关联性规则在节点之间分布 Kafka Pod以实现高可用性:
kafka-pod-spec.yaml
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- kafka
topologyKey: kubernetes.io/hostname
您可以随时查询 pod 的亲和性或反亲和性规范,如以下命令所示(podAntiAffinity:podAffinity)
$ kubectl get pod <PENDING_POD_NAME> -ojson | jq '.spec.affinity.podAntiAffinity'
{
"requiredDuringSchedulingIgnoredDuringExecution": [
{
"labelSelector": {
"matchExpressions": [
{
"key": "app",
"operator": "In",
"values": [
"kafka"
]
}
]
},
"topologyKey": "kubernetes.io/hostname"
}
]
}
在本例中,我们使用topologyKey来确保应用程序 Pod 不会位于同一节点上。因此,如果可用节点的数量小于标记为app: kafka 的 Pod 数量,则任何具有该标签的 Pod 将无法得到调度,直到有足够的节点可用于托管它们。
与节点污点、节亲和性/反亲和性规则的软硬要求类似,如果您想防止它pod未被调度,您可以在 pod 间亲和性或反亲和性规则中使用preferredDuringSchedulingIgnoredDuringExecution而非requiredDuringSchedulingIgnoredDuringExecution。如果您使用preferredDuringSchedulingIgnoredDuringExecution,调度程序将尝试根据您分配的权重来遵循此请求,但如果这是调度 Pod 的唯一方法,则仍然可以忽略它。
滚动更新部署设置
到目前为止,我们已经介绍了一些可能导致 Pending Pod 无法调度的predicates。接下来,我们将探讨 Pending Pod 在滚动更新期间为何遇到调度问题。
在这种类型的部署策略中,Kubernetes 尝试逐步更新 Pod,以降低工作负载可用性下降的可能性。为此,它会根据您的滚动更新策略配置逐渐减少过时的 Pod 数量并增加新的 Pod 数量。以下滚动更新策略参数限制滚动更新期间可以初始化或不可用的 Pod 数量:
- maxSurge:滚动更新期间可以运行的最大 Pod 数量
- maxUnavailable:滚动更新期间不可用的最大 Pod 数量
maxSurge防止过多的Pod同时启动,以减少节点负载过载的风险,而maxUnavailable则有助于确保正在更新的Deployment的Pod可用性达到最低水平。您可以将这些字段指定为百分比或整数。如果使用百分比,则计算为期望Pod数的百分比,然后向上(对于maxSurge)或向下(对于maxUnavailable)舍入为最近的整数。默认情况下,maxSurge和maxUnavailable设置为25%。
让我们看几种可能导致滚动更新停滞的情况,并了解这些设置如何影响整体情况。第一个问题出现在期望Pod数量少于四个,并且默认值的maxUnavailable和maxSurge正在生效。
如果您想要更新一个具有三个Pod的Deployment,那么在任何时候不可用的最大Pod数量为0(25% * 3个节点= 0.75,向下舍入为0)。这实际上阻止了滚动更新的任何进展-不能终止旧的Pod,因为那样将违反maxUnavailable的要求。要解除此阻塞,您需要将maxUnavailable修改为一个大于0的绝对值,而不是百分比。
第二个问题出现在达到maxUnavailable阈值时。即使将maxUnavailable设置为大于0的绝对值,滚动更新可能仍需要暂停。在许多情况下,这只是滚动暂时停止(即,直到不可用的Pod可以重新调度)。然而,如果更新在异常长的时间内仍停滞不前,您可能需要进行故障排除。
要获取更多详细信息,您可以使用kubectl describe命令来描述正在进行这个示例中的新的 ReplicaSet 部署。您将看到一系列期望的、更新的、总的、可用的和不可用的Pod,以及滚动更新策略设置。
$ kubectl describe deployment webapp
...
Replicas: 15 desired | 11 updated | 19 total | 9 available | 10 unavailable
RollingUpdateStrategy: 25% max unavailable, 25% max surge
...
Conditions:
Type Status Reason
---- ------ ------
Available False MinimumReplicasUnavailable
Progressing False ProgressDeadlineExceeded
OldReplicaSets: webapp-55cf4f7769 (8/8 replicas created)
NewReplicaSet: webapp-5684bccc75 (11/11 replicas created)
在这种情况下,一个或多个节点可能在滚动更新期间变得不可用,导致关联的 Pod 转换为 Pending。由于待处理的 Pod 会导致不可用的 Pod 计数增加,因此它们可能导致此 Deployment 达到阈值maxUnavailable,从而停止更新。
其他一些情况也可能导致不可用 Pod 的增加。例如:
- 有人可能触发了自愿中断——导致这些 Pod 重新启动或失败的操作(例如,删除 Pod 或耗尽节点)。
-Pod被驱逐了。当节点面临资源限制时,kubelet 开始驱逐 pod,首先驱逐那些使用超过其请求资源的 pod,然后按优先级顺序驱逐 pod 。
无论是什么原因导致这些 Pod 不可用,您都可以通过添加更多节点来托管 Pending Pod、手动缩减旧 ReplicaSet 或减少所需的副本数量来恢复滚动更新。
总结
在这篇文章中,我们介绍了 Kubernetes 调度程序在放置 Pending Pod 时可能遇到问题的几个原因。尽管 Kubernetes 调度程序尝试找到托管 Pod 的最佳节点,但情况不断变化,因此很难保证每个调度决策都成功。