[K8s]Kubernetes-调度抢占驱逐

Posted 若水三千

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[K8s]Kubernetes-调度抢占驱逐相关的知识,希望对你有一定的参考价值。

调度,抢占和驱逐

在Kubernetes中,调度 (scheduling) 指的是确保 Pods 匹配到合适的节点,以便 kubelet 能够运行它们。抢占 (Preemption) 指的是终止低优先级的 Pods 以便高优先级的 Pods 可以调度运行的过程。驱逐 (Eviction) 是在资源匮乏的节点上,主动让一个或多个 Pods 失效的过程。

1 - Kubernetes 调度器

在 Kubernetes 中,调度是指将 Pod 放置到合适的 Node 上,然后对应 Node 上的 Kubelet 才能够运行这些 pod。

调度概览

调度器通过 kubernetes 的监测(Watch)机制来发现集群中新创建且尚未被调度到 Node 上的 Pod。调度器会将发现的每一个未调度的 Pod 调度到一个合适的 Node 上来运行。调度器会依据下文的调度原则来做出调度选择。

如果你想要理解 Pod 为什么会被调度到特定的 Node 上,或者你想要尝试实现一个自定义的调度器,这篇文章将帮助你了解调度。

kube-scheduler

kube-scheduler 是 Kubernetes 集群的默认调度器,并且是集群控制面的一部分。如果你真的希望或者有这方面的需求,kube-scheduler 在设计上是允许你自己写一个调度组件并替换原有的 kube-scheduler。

对每一个新创建的 Pod 或者是未被调度的 Pod,kube-scheduler 会选择一个最优的 Node 去运行这个 Pod。然而,Pod 内的每一个容器对资源都有不同的需求,而且 Pod 本身也有不同的资源需求。因此,Pod 在被调度到 Node 上之前,根据这些特定的资源调度需求,需要对集群中的 Node 进行一次过滤。

在一个集群中,满足一个 Pod 调度请求的所有 Node 称之为可调度节点。如果没有任何一个 Node 能满足 Pod 的资源请求,那么这个 Pod 将一直停留在未调度状态直到调度器能够找到合适的 Node。

调度器先在集群中找到一个 Pod 的所有可调度节点,然后根据一系列函数对这些可调度节点打分,选出其中得分最高的 Node 来运行 Pod。之后,调度器将这个调度决定通知给 kube-apiserver,这个过程叫做绑定。

在做调度决定时需要考虑的因素包括:单独和整体的资源请求、硬件/软件/策略限制、亲和以及反亲和要求、数据局域性、负载间的干扰等等。

kube-scheduler 调度流程

kube-scheduler 给一个 pod 做调度选择包含两个步骤:

  1. 过滤
  2. 打分

过滤阶段会将所有满足 Pod 调度需求的 Node 选出来。例如,PodFitsResources 过滤函数会检查候选 Node 的可用资源能否满足 Pod 的资源请求。在过滤之后,得出一个 Node 列表,里面包含了所有可调度节点;通常情况下,这个 Node 列表包含不止一个 Node。如果这个列表是空的,代表这个 Pod 不可调度。

在打分阶段,调度器会为 Pod 从所有可调度节点中选取一个最合适的 Node。根据当前启用的打分规则,调度器会给每一个可调度节点进行打分。

最后,kube-scheduler 会将 Pod 调度到得分最高的 Node 上。如果存在多个得分最高的 Node,kube-scheduler 会从中随机选取一个。

支持以下两种方式配置调度器的过滤和打分行为:

  1. 调度策略:允许你配置过滤的断言(Predicates) 和打分的优先级(Priorities) 。
  2. 调度配置:允许你配置实现不同调度阶段的插件,包括:QueueSort,Filter,Score,Bind,Reserve,Permit 等等。你也可以配置 kube-scheduler 运行不同的配置文件。

2 - 将 Pod 分配给节点

你可以约束一个 Pod 只能在特定的节点上运行。有几种方法可以实现这点,推荐的方法都是用标签选择算符来进行选择。通常这样的约束不是必须的,因为调度器将自动进行合理的放置(比如,将 Pod 分散到节点上,而不是将 Pod 放置在可用资源不足的节点上等等)。但在某些情况下,你可能需要进一步控制 Pod 停靠的节点,例如,确保 Pod 最终落在连接了 SSD 的机器上,或者将来自两个不同的服务且有大量通信的 Pods 被放置在同一个可用区。

nodeSelector

nodeSelector 是节点选择约束的最简单推荐形式。nodeSelector 是 PodSpec 的一个字段。它包含键值对的映射。为了使 pod 可以在某个节点上运行,该节点的标签中必须包含这里的每个键值对(它也可以具有其他标签)。最常见的用法的是一对键值对。

让我们来看一个使用 nodeSelector 的例子。

步骤零:先决条件

本示例假设你已基本了解 Kubernetes 的 Pod 并且已经建立一个 Kubernetes 集群。

步骤一:添加标签到节点

执行 kubectl get nodes 命令获取集群的节点名称。选择一个你要增加标签的节点,然后执行 kubectl label nodes <node-name> <label-key>=<label-value> 命令将标签添加到你所选择的节点上。例如,如果你的节点名称为 \'kubernetes-foo-node-1.c.a-robinson.internal\' 并且想要的标签是 \'disktype=ssd\',则可以执行 kubectl label nodes kubernetes-foo-node-1.c.a-robinson.internal disktype=ssd 命令。

你可以通过重新运行 kubectl get nodes --show-labels,查看节点当前具有了所指定的标签来验证它是否有效。你也可以使用 kubectl describe node "nodename" 命令查看指定节点的标签完整列表。

步骤二:添加 nodeSelector 字段到 Pod 配置中

选择任何一个你想运行的 Pod 的配置文件,并且在其中添加一个 nodeSelector 部分。例如,如果下面是我的 pod 配置:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: nginx

然后像下面这样添加 nodeSelector:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
  nodeSelector:
    disktype: ssd

当你之后运行 kubectl apply -f https://k8s.io/examples/pods/pod-nginx.yaml 命令,Pod 将会调度到将标签添加到的节点上。你可以通过运行 kubectl get pods -o wide 并查看分配给 pod 的 “NODE” 来验证其是否有效。

插曲:内置的节点标签

除了你添加的标签外,节点还预制了一组标准标签。参见这些常用的标签,注解以及污点:

  • kubernetes.io/hostname
  • failure-domain.beta.kubernetes.io/zone
  • failure-domain.beta.kubernetes.io/region
  • topology.kubernetes.io/zone
  • topology.kubernetes.io/region
  • beta.kubernetes.io/instance-type
  • node.kubernetes.io/instance-type
  • kubernetes.io/os
  • kubernetes.io/arch

说明:
这些标签的值是特定于云供应商的,因此不能保证可靠。例如,kubernetes.io/hostname 的值在某些环境中可能与节点名称相同,但在其他环境中可能是一个不同的值。

节点隔离/限制

向 Node 对象添加标签可以将 pod 定位到特定的节点或节点组。这可以用来确保指定的 Pod 只能运行在具有一定隔离性,安全性或监管属性的节点上。当为此目的使用标签时,强烈建议选择节点上的 kubelet 进程无法修改的标签键。这可以防止受感染的节点使用其 kubelet 凭据在自己的 Node 对象上设置这些标签,并影响调度器将工作负载调度到受感染的节点。

NodeRestriction 准入插件防止 kubelet 使用 node-restriction.kubernetes.io/ 前缀设置或修改标签。要使用该标签前缀进行节点隔离:

  1. 检查是否在使用 Kubernetes v1.11+,以便 NodeRestriction 功能可用。
  2. 确保你在使用节点授权并且已经启用 NodeRestriction 准入插件。
  3. 将 node-restriction.kubernetes.io/ 前缀下的标签添加到 Node 对象,然后在节点选择器中使用这些标签。例如,example.com.node-restriction.kubernetes.io/fips=true 或 example.com.node-restriction.kubernetes.io/pci-dss=true。

亲和性与反亲和性

nodeSelector 提供了一种非常简单的方法来将 Pod 约束到具有特定标签的节点上。亲和性/反亲和性功能极大地扩展了你可以表达约束的类型。关键的增强点包括:

  1. 语言更具表现力(不仅仅是“对完全匹配规则的 AND”)
  2. 你可以发现规则是“软需求”/“偏好”,而不是硬性要求,因此,如果调度器无法满足该要求,仍然调度该 Pod
  3. 你可以使用节点上(或其他拓扑域中)的 Pod 的标签来约束,而不是使用节点本身的标签,来允许哪些 pod 可以或者不可以被放置在一起。

亲和性功能包含两种类型的亲和性,即“节点亲和性”和“Pod 间亲和性/反亲和性”。节点亲和性就像现有的 nodeSelector(但具有上面列出的前两个好处),然而 Pod 间亲和性/反亲和性约束 Pod 标签而不是节点标签(在上面列出的第三项中描述,除了具有上面列出的第一和第二属性)。

节点亲和性

节点亲和性概念上类似于 nodeSelector,它使你可以根据节点上的标签来约束 Pod 可以调度到哪些节点。

目前有两种类型的节点亲和性,分别为 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution。你可以视它们为“硬需求”和“软需求”,意思是,前者指定了将 Pod 调度到一个节点上必须满足的规则(就像 nodeSelector 但使用更具表现力的语法),后者指定调度器将尝试执行但不能保证的偏好。名称的“IgnoredDuringExecution”部分意味着,类似于 nodeSelector 的工作原理,如果节点的标签在运行时发生变更,从而不再满足 Pod 上的亲和性规则,那么 Pod 将仍然继续在该节点上运行。将来我们计划提供 requiredDuringSchedulingRequiredDuringExecution,它将与 requiredDuringSchedulingIgnoredDuringExecution 完全相同,只是它会将 Pod 从不再满足 Pod 的节点亲和性要求的节点上驱逐。

因此,requiredDuringSchedulingIgnoredDuringExecution 的示例将是 “仅将 Pod 运行在具有 Intel CPU 的节点上”,而 preferredDuringSchedulingIgnoredDuringExecution 的示例为 “尝试将这组 Pod 运行在 XYZ 故障区域,如果这不可能的话,则允许一些 Pod 在其他地方运行”。

节点亲和性通过 PodSpec 的 affinity 字段下的 nodeAffinity 字段进行指定。

下面是一个使用节点亲和性的 Pod 的实例:

apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/e2e-az-name
            operator: In
            values:
            - e2e-az1
            - e2e-az2
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: another-node-label-key
            operator: In
            values:
            - another-node-label-value
  containers:
  - name: with-node-affinity
    image: k8s.gcr.io/pause:2.0

此节点亲和性规则表示,Pod 只能放置在具有标签键 kubernetes.io/e2e-az-name 且标签值为 e2e-az1 或 e2e-az2 的节点上。另外,在满足这些标准的节点中,具有标签键为 another-node-label-key 且标签值为 another-node-label-value 的节点应该优先使用。

你可以在上面的例子中看到 In 操作符的使用。新的节点亲和性语法支持下面的操作符:In,NotIn,Exists,DoesNotExist,Gt,Lt。你可以使用 NotIn 和 DoesNotExist 来实现节点反亲和性行为,或者使用节点污点将 Pod 从特定节点中驱逐。

如果你同时指定了 nodeSelector 和 nodeAffinity,两者必须都要满足,才能将 Pod 调度到候选节点上。

如果你指定了多个与 nodeAffinity 类型关联的 nodeSelectorTerms,则如果其中一个nodeSelectorTerms 满足的话,pod将可以调度到节点上。

如果你指定了多个与 nodeSelectorTerms 关联的 matchExpressions,则只有当所有matchExpressions 满足的话,Pod 才会可以调度到节点上。

如果你修改或删除了 pod 所调度到的节点的标签,Pod 不会被删除。换句话说,亲和性选择只在 Pod 调度期间有效。

preferredDuringSchedulingIgnoredDuringExecution 中的 weight 字段值的范围是 1-100。对于每个符合所有调度要求(资源请求、RequiredDuringScheduling 亲和性表达式等)的节点,调度器将遍历该字段的元素来计算总和,并且如果节点匹配对应的 MatchExpressions,则添加“权重”到总和。然后将这个评分与该节点的其他优先级函数的评分进行组合。总分最高的节点是最优选的。

逐个调度方案中设置节点亲和性

FEATURE STATE: Kubernetes v1.20 [beta]

在配置多个调度方案时,你可以将某个方案与节点亲和性关联起来,如果某个调度方案仅适用于某组特殊的节点时,这样做是很有用的。要实现这点,可以在调度器配置中为 NodeAffinity 插件添加 addedAffinity 参数。例如:

apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration

profiles:
  - schedulerName: default-scheduler
  - schedulerName: foo-scheduler
    pluginConfig:
      - name: NodeAffinity
        args:
          addedAffinity:
            requiredDuringSchedulingIgnoredDuringExecution:
              nodeSelectorTerms:
              - matchExpressions:
                - key: scheduler-profile
                  operator: In
                  values:
                  - foo

这里的 addedAffinity 除遵从 Pod 规约中设置的节点亲和性之外,还适用于将 .spec.schedulerName 设置为 foo-scheduler。

说明: DaemonSet 控制器为 DaemonSet 创建 Pods,但该控制器不理会调度方案。因此,建议你保留一个调度方案,例如 default-scheduler,不要在其中设置 addedAffinity。这样,DaemonSet 的 Pod 模板将会使用此调度器名称。否则,DaemonSet 控制器所创建的某些 Pods 可能持续处于不可调度状态。

pod 间亲和性与反亲和性

Pod 间亲和性与反亲和性使你可以基于已经在节点上运行的 Pod 的标签来约束 Pod 可以调度到的节点,而不是基于节点上的标签。规则的格式为“如果 X 节点上已经运行了一个或多个满足规则 Y 的 Pod,则这个 Pod 应该(或者在反亲和性的情况下不应该)运行在 X 节点”。Y 表示一个具有可选的关联命令空间列表的 LabelSelector;与节点不同,因为 Pod 是命名空间限定的(因此 Pod 上的标签也是命名空间限定的),因此作用于 Pod 标签的标签选择算符必须指定选择算符应用在哪个命名空间。从概念上讲,X 是一个拓扑域,如节点、机架、云供应商可用区、云供应商地理区域等。你可以使用 topologyKey 来表示它,topologyKey 是节点标签的键以便系统用来表示这样的拓扑域。请参阅上面插曲:内置的节点标签部分中列出的标签键。

说明:
Pod 间亲和性与反亲和性需要大量的处理,这可能会显著减慢大规模集群中的调度。我们不建议在超过数百个节点的集群中使用它们。

说明:
Pod 反亲和性需要对节点进行一致的标记,即集群中的每个节点必须具有适当的标签能够匹配 topologyKey。如果某些或所有节点缺少指定的 topologyKey 标签,可能会导致意外行为。

与节点亲和性一样,当前有两种类型的 Pod 亲和性与反亲和性,即 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution,分别表示“硬性”与“软性”要求。请参阅前面节点亲和性部分中的描述。

requiredDuringSchedulingIgnoredDuringExecution 亲和性的一个示例是 “将服务 A 和服务 B 的 Pod 放置在同一区域,因为它们之间进行大量交流”,而 preferredDuringSchedulingIgnoredDuringExecution 反亲和性的示例将是 “将此服务的 pod 跨区域分布”(硬性要求是说不通的,因为你可能拥有的 Pod 数多于区域数)。

Pod 间亲和性通过 PodSpec 中 affinity 字段下的 podAffinity 字段进行指定。而 Pod 间反亲和性通过 PodSpec 中 affinity 字段下的 podAntiAffinity 字段进行指定。

Pod 使用 pod 亲和性 的示例:

apiVersion: v1
kind: Pod
metadata:
  name: with-pod-affinity
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: security
            operator: In
            values:
            - S1
        topologyKey: topology.kubernetes.io/zone
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: security
              operator: In
              values:
              - S2
          topologyKey: topology.kubernetes.io/zone
  containers:
  - name: with-pod-affinity
    image: k8s.gcr.io/pause:2.0

在这个 Pod 的亲和性配置定义了一条 Pod 亲和性规则和一条 Pod 反亲和性规则。在此示例中,podAffinity 配置为 requiredDuringSchedulingIgnoredDuringExecution,然而 podAntiAffinity 配置为 preferredDuringSchedulingIgnoredDuringExecution。Pod 亲和性规则表示,仅当节点和至少一个已运行且有键为“security”且值为“S1”的标签 的 Pod 处于同一区域时,才可以将该 Pod 调度到节点上。(更确切的说,如果节点 N 具有带有键 topology.kubernetes.io/zone 和某个值 V 的标签,则 Pod 有资格在节点 N 上运行,以便集群中至少有一个节点具有键 topology.kubernetes.io/zone 和值为 V 的节点正在运行具有键“security”和值 “S1”的标签的 pod。)

Pod 反亲和性规则表示,如果节点处于 Pod 所在的同一可用区且具有键“security”和值“S2”的标签,则该 pod 不应将其调度到该节点上。(如果 topologyKey 为 topology.kubernetes.io/zone,则意味着当节点和具有键 “security”和值“S2”的标签的 Pod 处于相同的区域,Pod 不能被调度到该节点上。)查阅设计文档以获取 Pod 亲和性与反亲和性的更多样例,包括 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution 两种配置。

Pod 亲和性与反亲和性的合法操作符有 In,NotIn,Exists,DoesNotExist。

原则上,topologyKey 可以是任何合法的标签键。然而,出于性能和安全原因,topologyKey 受到一些限制:

  1. 对于 Pod 亲和性而言,在 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution 中,topologyKey 不允许为空。
  2. 对于 Pod 反亲和性而言,requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution 中,topologyKey 都不可以为空。
  3. 对于 requiredDuringSchedulingIgnoredDuringExecution 要求的 Pod 反亲和性,准入控制器 LimitPodHardAntiAffinityTopology 被引入以确保 topologyKey 只能是 kubernetes.io/hostname。如果你希望 topologyKey 也可用于其他定制拓扑逻辑,你可以更改准入控制器或者禁用之。
  4. 除上述情况外,topologyKey 可以是任何合法的标签键。

除了 labelSelector 和 topologyKey,你也可以指定表示命名空间的 namespaces 队列,labelSelector 也应该匹配它(这个与 labelSelector 和 topologyKey 的定义位于相同的级别)。如果忽略或者为空,则默认为 Pod 亲和性/反亲和性的定义所在的命名空间。

所有与 requiredDuringSchedulingIgnoredDuringExecution 亲和性与反亲和性关联的 matchExpressions 必须满足,才能将 pod 调度到节点上。

名字空间选择算符

FEATURE STATE: Kubernetes v1.22 [beta]

用户也可以使用 namespaceSelector 选择匹配的名字空间,namespaceSelector 是对名字空间集合进行标签查询的机制。亲和性条件会应用到 namespaceSelector 所选择的名字空间和 namespaces 字段中所列举的名字空间之上。注意,空的 namespaceSelector()会匹配所有名字空间,而 null 或者空的 namespaces 列表以及 null 值 namespaceSelector 意味着“当前 Pod 的名字空间”。

此功能特性是 Beta 版本的,默认是被启用的。你可以通过针对 kube-apiserver 和 kube-scheduler 设置特性门控 PodAffinityNamespaceSelector 来禁用此特性。

更实际的用例

Pod 间亲和性与反亲和性在与更高级别的集合(例如 ReplicaSets、StatefulSets、Deployments 等)一起使用时,它们可能更加有用。可以轻松配置一组应位于相同定义拓扑(例如,节点)中的工作负载。

始终放置在相同节点上

在三节点集群中,一个 web 应用程序具有内存缓存,例如 redis。我们希望 web 服务器尽可能与缓存放置在同一位置。

下面是一个简单 redis Deployment 的 YAML 代码段,它有三个副本和选择器标签 app=store。Deployment 配置了 PodAntiAffinity,用来确保调度器不会将副本调度到单个节点上。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-cache
spec:
  selector:
    matchLabels:
      app: store
  replicas: 3
  template:
    metadata:
      labels:
        app: store
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: redis-server
        image: redis:3.2-alpine

下面 webserver Deployment 的 YAML 代码段中配置了 podAntiAffinity 和 podAffinity。这将通知调度器将它的所有副本与具有 app=store 选择器标签的 Pod 放置在一起。这还确保每个 web 服务器副本不会调度到单个节点上。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-server
spec:
  selector:
    matchLabels:
      app: web-store
  replicas: 3
  template:
    metadata:
      labels:
        app: web-store
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - web-store
            topologyKey: "kubernetes.io/hostname"
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: web-app
        image: nginx:1.16-alpine

如果我们创建了上面的两个 Deployment,我们的三节点集群将如下表所示。

node-1
node-2
node-3
webserver-1
webserver-2
webserver-3
cache-1
cache-2
cache-3

如你所见,web-server 的三个副本都按照预期那样自动放置在同一位置。

kubectl get pods -o wide

输出类似于如下内容:

NAME                           READY     STATUS    RESTARTS   AGE       IP           NODE
redis-cache-1450370735-6dzlj   1/1       Running   0          8m        10.192.4.2   kube-node-3
redis-cache-1450370735-j2j96   1/1       Running   0          8m        10.192.2.2   kube-node-1
redis-cache-1450370735-z73mh   1/1       Running   0          8m        10.192.3.1   kube-node-2
web-server-1287567482-5d4dz    1/1       Running   0          7m        10.192.2.3   kube-node-1
web-server-1287567482-6f7v5    1/1       Running   0          7m        10.192.4.3   kube-node-3
web-server-1287567482-s330j    1/1       Running   0          7m        10.192.3.2   kube-node-2
永远不放置在相同节点

上面的例子使用 PodAntiAffinity 规则和 topologyKey: "kubernetes.io/hostname" 来部署 redis 集群以便在同一主机上没有两个实例。参阅 ZooKeeper 教程,以获取配置反亲和性来达到高可用性的 StatefulSet 的样例(使用了相同的技巧)。

nodeName

nodeName 是节点选择约束的最简单方法,但是由于其自身限制,通常不使用它。nodeName 是 PodSpec 的一个字段。如果它不为空,调度器将忽略 Pod,并且给定节点上运行的 kubelet 进程尝试执行该 Pod。因此,如果 nodeName 在 PodSpec 中指定了,则它优先于上面的节点选择方法。

使用 nodeName 来选择节点的一些限制:

  • 如果指定的节点不存在。
  • 如果指定的节点没有资源来容纳 Pod,Pod 将会调度失败并且其原因将显示为,比如 OutOfmemory 或 OutOfcpu。
  • 云环境中的节点名称并非总是可预测或稳定的。

下面的是使用 nodeName 字段的 Pod 配置文件的例子:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx
  nodeName: kube-01

上面的 pod 将运行在 kube-01 节点上。

3 - Pod 开销

FEATURE STATE: Kubernetes v1.18 [beta]

在节点上运行 Pod 时,Pod 本身占用大量系统资源。这些资源是运行 Pod 内容器所需资源的附加资源。POD 开销是一个特性,用于计算 Pod 基础设施在容器请求和限制之上消耗的资源。

Pod 开销

在 Kubernetes 中,Pod 的开销是根据与 Pod 的 RuntimeClass 相关联的开销在准入时设置的。

如果启用了 Pod Overhead,在调度 Pod 时,除了考虑容器资源请求的总和外,还要考虑 Pod 开销。类似地,kubelet 将在确定 Pod cgroups 的大小和执行 Pod 驱逐排序时也会考虑 Pod 开销。

启用 Pod 开销

您需要确保在集群中启用了 PodOverhead 特性门控(在 1.18 默认是开启的),以及一个用于定义 overhead 字段的 RuntimeClass。

使用示例

要使用 PodOverhead 特性,需要一个定义 overhead 字段的 RuntimeClass。作为例子,可以在虚拟机和寄宿操作系统中通过一个虚拟化容器运行时来定义 RuntimeClass 如下,其中每个 Pod 大约使用 120MiB:

---
kind: RuntimeClass
apiVersion: node.k8s.io/v1
metadata:
    name: kata-fc
handler: kata-fc
overhead:
    podFixed:
        memory: "120Mi"
        cpu: "250m"

通过指定 kata-fc RuntimeClass 处理程序创建的工作负载会将内存和 cpu 开销计入资源配额计算、节点调度以及 Pod cgroup 分级。

假设我们运行下面给出的工作负载示例 test-pod:

apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  runtimeClassName: kata-fc
  containers:
  - name: busybox-ctr
    image: busybox
    stdin: true
    tty: true
    resources:
      limits:
        cpu: 500m
        memory: 100Mi
  - name: nginx-ctr
    image: nginx
    resources:
      limits:
        cpu: 1500m
        memory: 100Mi

在准入阶段 RuntimeClass 准入控制器更新工作负载的 PodSpec 以包含 RuntimeClass 中定义的 overhead。如果 PodSpec 中该字段已定义,该 Pod 将会被拒绝。在这个例子中,由于只指定了 RuntimeClass 名称,所以准入控制器更新了 Pod, 包含了一个 overhead。

在 RuntimeClass 准入控制器之后,可以检验一下已更新的 PodSpec:

kubectl get pod test-pod -o jsonpath=\'.spec.overhead\'

输出:

map[cpu:250m memory:120Mi]

如果定义了 ResourceQuata,则容器请求的总量以及 overhead 字段都将计算在内。

当 kube-scheduler 决定在哪一个节点调度运行新的 Pod 时,调度器会兼顾该 Pod 的 overhead 以及该 Pod 的容器请求总量。在这个示例中,调度器将资源请求和开销相加,然后寻找具备 2.25 CPU 和 320 MiB 内存可用的节点。

一旦 Pod 调度到了某个节点,该节点上的 kubelet 将为该 Pod 新建一个 cgroup。底层容器运行时将在这个 pod 中创建容器。

如果该资源对每一个容器都定义了一个限制(定义了受限的 Guaranteed QoS 或者 Bustrable QoS),kubelet 会为与该资源(CPU 的 cpu.cfs_quota_us 以及内存的 memory.limit_in_bytes)相关的 pod cgroup 设定一个上限。该上限基于容器限制总量与 PodSpec 中定义的 overhead 之和。

对于 CPU,如果 Pod 的 QoS 是 Guaranteed 或者 Burstable,kubelet 会基于容器请求总量与 PodSpec 中定义的 overhead 之和设置 cpu.shares。

请看这个例子,验证工作负载的容器请求:

kubectl get pod test-pod -o jsonpath=\'.spec.containers[*].resources.limits\'

容器请求总计 2000m CPU 和 200MiB 内存:

map[cpu: 500m memory:100Mi] map[cpu:1500m memory:100Mi]

对照从节点观察到的情况来检查一下:

kubectl describe node | grep test-pod -B2

该输出显示请求了 2250m CPU 以及 320MiB 内存,包含了 PodOverhead 在内:

  Namespace                   Name                CPU Requests  CPU Limits   Memory Requests  Memory Limits  AGE
  ---------                   ----                ------------  ----------   ---------------  -------------  ---
  default                     test-pod            2250m (56%)   2250m (56%)  320Mi (1%)       320Mi (1%)     36m

验证 Pod cgroup 限制

在工作负载所运行的节点上检查 Pod 的内存 cgroups。在接下来的例子中,将在该节点上使用具备 CRI 兼容的容器运行时命令行工具 crictl。

首先在特定的节点上确定该 Pod 的标识符:

# 在该 Pod 调度的节点上执行如下命令:
POD_ID="$(sudo crictl pods --name test-pod -q)"

可以依此判断该 Pod 的 cgroup 路径:

# 在该 Pod 调度的节点上执行如下命令:
sudo crictl inspectp -o=json $POD_ID | grep cgroupsPath

执行结果的 cgroup 路径中包含了该 Pod 的 pause 容器。Pod 级别的 cgroup 即上面的一个目录。

        "cgroupsPath": "/kubepods/podd7f4b509-cf94-4951-9417-d1087c92a5b2/7ccf55aee35dd16aca4189c952d83487297f3cd760f1bbf09620e206e7d0c27a"

在这个例子中,该 pod 的 cgroup 路径是 kubepods/podd7f4b509-cf94-4951-9417-d1087c92a5b2。验证内存的 Pod 级别 cgroup 设置:

# 在该 Pod 调度的节点上执行这个命令。
# 另外,修改 cgroup 的名称以匹配为该 pod 分配的 cgroup。
 cat /sys/fs/cgroup/memory/kubepods/podd7f4b509-cf94-4951-9417-d1087c92a5b2/memory.limit_in_bytes

和预期的一样是 320 MiB

335544320

可观察性

在 kube-state-metrics 中可以通过 kube_pod_overhead 指标来协助确定何时使用 PodOverhead 以及协助观察以一个既定开销运行的工作负载的稳定性。该特性在 kube-state-metrics 的 1.9 发行版本中不可用,不过预计将在后续版本中发布。在此之前,用户需要从源代码构建 kube-state-metrics。

4 - 污点和容忍度

节点亲和性 是 Pod 的一种属性,它使 Pod 被吸引到一类特定的节点(这可能出于一种偏好,也可能是硬性要求)。污点(Taint)则相反——它使节点能够排斥一类特定的 Pod。

容忍度(Toleration)是应用于 Pod 上的,允许(但并不要求)Pod 调度到带有与之匹配的污点的节点上。

污点和容忍度(Toleration)相互配合,可以用来避免 Pod 被分配到不合适的节点上。每个节点上都可以应用一个或多个污点,这表示对于那些不能容忍这些污点的 Pod,是不会被该节点接受的。

概念

您可以使用命令 kubectl taint 给节点增加一个污点。比如,

kubectl taint nodes node1 key1=value1:NoSchedule

给节点 node1 增加一个污点,它的键名是 key1,键值是 value1,效果是 NoSchedule。这表示只有拥有和这个污点相匹配的容忍度的 Pod 才能够被分配到 node1 这个节点。

若要移除上述命令所添加的污点,你可以执行:

kubectl taint nodes node1 key1=value1:NoSchedule-

您可以在 PodSpec 中定义 Pod 的容忍度。下面两个容忍度均与上面例子中使用 kubectl taint 命令创建的污点相匹配,因此如果一个 Pod 拥有其中的任何一个容忍度都能够被分配到 node1 :

tolerations:
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoSchedule"
tolerations:
- key: "key1"
  operator: "Exists"
  effect: "NoSchedule"

这里是一个使用了容忍度的 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
  tolerations:
  - key: "example-key"
    operator: "Exists"
    effect: "NoSchedule"

operator 的默认值是 Equal。

一个容忍度和一个污点相“匹配”是指它们有一样的键名和效果,并且:

  • 如果 operator 是 Exists(此时容忍度不能指定 value)
  • 如果 operator 是 Equal,则它们的 value 应该相等

说明:
存在两种特殊情况:

如果一个容忍度的 key 为空且 operator 为 Exists,表示这个容忍度与任意的 key 、value 和 effect 都匹配,即这个容忍度能容忍任意 taint。

如果 effect 为空,则可以与所有键名 key1 的效果相匹配。

上述例子中 effect 使用的值为 NoSchedule,您也可以使用另外一个值 PreferNoSchedule。这是“优化”或“软”版本的 NoSchedule —— 系统会尽量避免将 Pod 调度到存在其不能容忍污点的节点上,但这不是强制的。effect 的值还可以设置为 NoExecute,下文会详细描述这个值。

您可以给一个节点添加多个污点,也可以给一个 Pod 添加多个容忍度设置。Kubernetes 处理多个污点和容忍度的过程就像一个过滤器:从一个节点的所有污点开始遍历,过滤掉那些 Pod 中存在与之相匹配的容忍度的污点。余下未被过滤的污点的 effect 值决定了 Pod 是否会被分配到该节点,特别是以下情况:

  • 如果未被过滤的污点中存在至少一个 effect 值为 NoSchedule 的污点,则 Kubernetes 不会将 Pod 分配到该节点。
  • 如果未被过滤的污点中不存在 effect 值为 NoSchedule 的污点,但是存在 effect 值为 PreferNoSchedule 的污点,则 Kubernetes 会尝试不将 Pod 分配到该节点。
  • 如果未被过滤的污点中存在至少一个 effect 值为 NoExecute 的污点,则 Kubernetes 不会将 Pod 分配到该节点(如果 Pod 还未在节点上运行),或者将 Pod 从该节点驱逐(如果 Pod 已经在节点上运行)。

例如,假设您给一个节点添加了如下污点

kubectl taint nodes node1 key1=value1:NoSchedule
kubectl taint nodes node1 key1=value1:NoExecute
kubectl taint nodes node1 key2=value2:NoSchedule

假定有一个 Pod,它有两个容忍度:

tolerations:
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoSchedule"
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoExecute"

在这种情况下,上述 Pod 不会被分配到上述节点,因为其没有容忍度和第三个污点相匹配。但是如果在给节点添加上述污点之前,该 Pod 已经在上述节点运行,那么它还可以继续运行在该节点上,因为第三个污点是三个污点中唯一不能被这个 Pod 容忍的。

通常情况下,如果给一个节点添加了一个 effect 值为 NoExecute 的污点,则任何不能忍受这个污点的 Pod 都会马上被驱逐,任何可以忍受这个污点的 Pod 都不会被驱逐。但是,如果 Pod 存在一个 effect 值为 NoExecute 的容忍度指定了可选属性 tolerationSeconds 的值,则表示在给节点添加了上述污点之后,Pod 还能继续在节点上运行的时间。例如:

tolerations:
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoExecute"
  tolerationSeconds: 3600

这表示如果这个 Pod 正在运行,同时一个匹配的污点被添加到其所在的节点,那么 Pod 还将继续在节点上运行 3600 秒,然后被驱逐。如果在此之前上述污点被删除了,则 Pod 不会被驱逐。

使用例子

通过污点和容忍度,可以灵活地让 Pod 避开某些节点或者将 Pod 从某些节点驱逐。下面是几个使用例子:

  • 专用节点:如果您想将某些节点专门分配给特定的一组用户使用,您可以给这些节点添加一个污点(即,kubectl taint nodes nodename dedicated=groupName:NoSchedule),然后给这组用户的 Pod 添加一个相对应的 toleration(通过编写一个自定义的准入控制器,很容易就能做到)。拥有上述容忍度的 Pod 就能够被分配到上述专用节点,同时也能够被分配到集群中的其它节点。如果您希望这些 Pod 只能被分配到上述专用节点,那么您还需要给这些专用节点另外添加一个和上述污点类似的 label(例如:dedicated=groupName),同时还要在上述准入控制器中给 Pod 增加节点亲和性要求上述 Pod 只能被分配到添加了 dedicated=groupName 标签的节点上。
  • 配备了特殊硬件的节点:在部分节点配备了特殊硬件(比如 GPU)的集群中,我们希望不需要这类硬件的 Pod 不要被分配到这些特殊节点,以便为后继需要这类硬件的 Pod 保留资源。要达到这个目的,可以先给配备了特殊硬件的节点添加 taint (例如 kubectl taint nodes nodename special=true:NoSchedule 或 kubectl taint nodes nodename special=true:PreferNoSchedule),然后给使用了这类特殊硬件的 Pod 添加一个相匹配的 toleration。和专用节点的例子类似,添加这个容忍度的最简单的方法是使用自定义准入控制器。比如,我们推荐使用扩展资源来表示特殊硬件,给配置了特殊硬件的节点添加污点时包含扩展资源名称,然后运行一个 ExtendedResourceToleration 准入控制器。此时,因为节点已经被设置污点了,没有对应容忍度的 Pod 不会被调度到这些节点。但当你创建一个使用了扩展资源的 Pod 时,ExtendedResourceToleration 准入控制器会自动给 Pod 加上正确的容忍度,这样 Pod 就会被自动调度到这些配置了特殊硬件件的节点上。这样就能够确保这些配置了特殊硬件的节点专门用于运行需要使用这些硬件的 Pod,并且您无需手动给这些 Pod 添加容忍度。
  • 基于污点的驱逐: 这是在每个 Pod 中配置的在节点出现问题时的驱逐行为,接下来的章节会描述这个特性。

基于污点的驱逐

FEATURE STATE: Kubernetes v1.18 [stable]

前文提到过污点的 effect 值 NoExecute 会影响已经在节点上运行的 Pod

  • 如果 Pod 不能忍受 effect 值为 NoExecute 的污点,那么 Pod 将马上被驱逐。
  • 如果 Pod 能够忍受 effect 值为 NoExecute 的污点,但是在容忍度定义中没有指定 tolerationSeconds,则 Pod 还会一直在这个节点上运行。
  • 如果 Pod 能够忍受 effect 值为 NoExecute 的污点,而且指定了 tolerationSeconds,则 Pod 还能在这个节点上继续运行这个指定的时间长度。

当某种条件为真时,节点控制器会自动给节点添加一个污点。当前内置的污点包括:

  • node.kubernetes.io/not-ready:节点未准备好。这相当于节点状态 Ready 的值为 "False"。
  • node.kubernetes.io/unreachable:节点控制器访问不到节点。这相当于节点状态 Ready 的值为 "Unknown"。
  • node.kubernetes.io/memory-pressure:节点存在内存压力。
  • node.kubernetes.io/disk-pressure:节点存在磁盘压力。
  • node.kubernetes.io/pid-pressure: 节点的 PID 压力。
  • node.kubernetes.io/network-unavailable:节点网络不可用。
  • node.kubernetes.io/unschedulable: 节点不可调度。
  • node.cloudprovider.kubernetes.io/uninitialized:如果 kubelet 启动时指定了一个 "外部" 云平台驱动,它将给当前节点添加一个污点将其标志为不可用。在 cloud-controller-manager 的一个控制器初始化这个节点后,kubelet 将删除这个污点。

在节点被驱逐时,节点控制器或者 kubelet 会添加带有 NoExecute 效应的相关污点。如果异常状态恢复正常,kubelet 或节点控制器能够移除相关的污点。

说明: 为了保证由于节点问题引起的 Pod 驱逐速率限制行为正常,系统实际上会以限定速率的方式添加污点。在像主控节点与工作节点间通信中断等场景下,这样做可以避免 Pod 被大量驱逐。

使用这个功能特性,结合 tolerationSeconds,Pod 就可以指定当节点出现一个或全部上述问题时还将在这个节点上运行多长的时间。

比如,一个使用了很多本地状态的应用程序在网络断开时,仍然希望停留在当前节点上运行一段较长的时间,愿意等待网络恢复以避免被驱逐。在这种情况下,Pod 的容忍度可能是下面这样的:

tolerations:
- key: "node.kubernetes.io/unreachable"
  operator: "Exists"
  effect: "NoExecute"
  tolerationSeconds: 6000

说明:
Kubernetes 会自动给 Pod 添加一个 key 为 node.kubernetes.io/not-ready 的容忍度并配置 tolerationSeconds=300,除非用户提供的 Pod 配置中已经已存在了 key 为 node.kubernetes.io/not-ready 的容忍度。

同样,Kubernetes 会给 Pod 添加一个 key 为 node.kubernetes.io/unreachable 的容忍度并配置 tolerationSeconds=300,除非用户提供的 Pod 配置中已经已存在了 key 为 node.kubernetes.io/unreachable 的容忍度。

这种自动添加的容忍度意味着在其中一种问题被检测到时 Pod 默认能够继续停留在当前节点运行 5 分钟。

DaemonSet 中的 Pod 被创建时,针对以下污点自动添加的 NoExecute 的容忍度将不会指定 tolerationSeconds:

  • node.kubernetes.io/unreachable
  • node.kubernetes.io/not-ready

这保证了出现上述问题时 DaemonSet 中的 Pod 永远不会被驱逐。

基于节点状态添加污点

控制平面使用节点控制器自动创建与节点状况对应的带有 NoSchedule 效应的污点。

调度器在进行调度时检查污点,而不是检查节点状况。这确保节点状况不会直接影响调度。例如,如果 DiskPressure 节点状况处于活跃状态,则控制平面添加 node.kubernetes.io/disk-pressure 污点并且不会调度新的 pod 到受影响的节点。如果 MemoryPressure 节点状况处于活跃状态,则控制平面添加 node.kubernetes.io/memory-pressure 污点。

对于新创建的 Pod,可以通过添加相应的 Pod 容忍度来忽略节点状况。控制平面还在具有除 BestEffort 之外的 QoS 类的 pod 上 添加 node.kubernetes.io/memory-pressure 容忍度。这是因为 Kubernetes 将 Guaranteed 或 Burstable QoS 类中的 Pod(甚至没有设置内存请求的 Pod)视为能够应对内存压力,而新创建的 BestEffort Pod 不会被调度到受影响的节点上。

DaemonSet 控制器自动为所有守护进程添加如下 NoSchedule 容忍度以防 DaemonSet 崩溃:

  • node.kubernetes.io/memory-pressure
  • node.kubernetes.io/disk-pressure
  • node.kubernetes.io/pid-pressure (1.14 或更高版本)
  • node.kubernetes.io/unschedulable (1.10 或更高版本)
  • node.kubernetes.io/network-unavailable (只适合主机网络配置)

添加上述容忍度确保了向后兼容,您也可以选择自由向 DaemonSet 添加容忍度。

5 - Pod 优先级和抢占

FEATURE STATE: Kubernetes v1.14 [stable]

Pod 可以有优先级。优先级表示一个 Pod 相对于其他 Pod 的重要性。如果一个 Pod 无法被调度,调度程序会尝试抢占(驱逐)较低优先级的 Pod,以使悬决 Pod 可以被调度。

警告:
在一个并非所有用户都是可信的集群中,恶意用户可能以最高优先级创建 Pod,导致其他 Pod 被驱逐或者无法被调度。管理员可以使用 ResourceQuota 来阻止用户创建高优先级的 Pod。

如何使用优先级和抢占

要使用优先级和抢占:

  1. 新增一个或多个 PriorityClass。

  2. 创建 Pod,并将其 priorityClassName 设置为新增的 PriorityClass。当然你不需要直接创建 Pod;通常,你将会添加 priorityClassName 到集合对象(如 Deployment) 的 Pod 模板中。

继续阅读以获取有关这些步骤的更多信息。

说明:
Kubernetes 已经提供了 2 个 PriorityClass:system-cluster-critical 和 system-node-critical。这些是常见的类,用于确保始终优先调度关键组件。

PriorityClass

PriorityClass 是一个无名称空间对象,它定义了从优先级类名称到优先级整数值的映射。名称在 PriorityClass 对象元数据的 name 字段中指定。值在必填的 value 字段中指定。值越大,优先级越高。PriorityClass 对象的名称必须是有效的 DNS 子域名,并且它不能以 system- 为前缀。

PriorityClass 对象可以设置任何小于或等于 10 亿的 32 位整数值。较大的数字是为通常不应被抢占或驱逐的关键的系统 Pod 所保留的。集群管理员应该为这类映射分别创建独立的 PriorityClass 对象。

PriorityClass 还有两个可选字段:globalDefault 和 description。globalDefault 字段表示这个 PriorityClass 的值应该用于没有 priorityClassName 的 Pod。系统中只能存在一个 globalDefault 设置为 true 的 PriorityClass。如果不存在设置了 globalDefault 的 PriorityClass,则没有 priorityClassName 的 Pod 的优先级为零。

description 字段是一个任意字符串。它用来告诉集群用户何时应该使用此 PriorityClass。

关于 PodPriority 和现有集群的注意事项

  • 如果你升级一个已经存在的但尚未使用此特性的集群,该集群中已经存在的 Pod 的优先级等效于零。

  • 添加一个将 globalDefault 设置为 true 的 PriorityClass 不会改变现有 Pod 的优先级。此类 PriorityClass 的值仅用于添加 PriorityClass 后创建的 Pod。

  • 如果你删除了某个 PriorityClass 对象,则使用被删除的 PriorityClass 名称的现有 Pod 保持不变,但是你不能再创建使用已删除的 PriorityClass 名称的 Pod。

PriorityClass 示例

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000
globalDefault: false
description: "此优先级类应仅用于 XYZ 服务 Pod。"

非抢占式 PriorityClass

FEATURE STATE: Kubernetes v1.19 [beta]

配置了 PreemptionPolicy: Never 的 Pod 将被放置在调度队列中较低优先级 Pod 之前,但它们不能抢占其他 Pod。等待调度的非抢占式 Pod 将留在调度队列中,直到有足够的可用资源,它才可以被调度。非抢占式 Pod,像其他 Pod 一样,受调度程序回退的影响。这意味着如果调度程序尝试这些 Pod 并且无法调度它们,它们将以更低的频率被重试,从而允许其他优先级较低的 Pod 排在它们之前。

非抢占式 Pod 仍可能被其他高优先级 Pod 抢占。

PreemptionPolicy 默认为 PreemptLowerPriority,这将允许该 PriorityClass 的 Pod 抢占较低优先级的 Pod(现有默认行为也是如此)。如果 PreemptionPolicy 设置为 Never,则该 PriorityClass 中的 Pod 将是非抢占式的。

数据科学工作负载是一个示例用例。用户可以提交他们希望优先于其他工作负载的作业,但不希望因为抢占运行中的 Pod 而导致现有工作被丢弃。设置为 PreemptionPolicy: Never 的高优先级作业将在其他排队的 Pod 之前被调度,只要足够的集群资源“自然地”变得可用。

非抢占式 PriorityClass 示例

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority-nonpreempting
value: 1000000
preemptionPolicy: Never
globalDefault: false
description: "This priority class will not cause other pods to be preempted."

Pod 优先级

在你拥有一个或多个 PriorityClass 对象之后,你可以创建在其规约中指定这些 PriorityClass 名称之一的 Pod。优先级准入控制器使用 priorityClassName 字段并填充优先级的整数值。如果未找到所指定的优先级类,则拒绝 Pod。

以下 YAML 是 Pod 配置的示例,它使用在前面的示例中创建的 PriorityClass。优先级准入控制器检查 Pod 规约并将其优先级解析为 1000000。

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
  priorityClassName: high-priority

Pod 优先级对调度顺序的影响

当启用 Pod 优先级时,调度程序会按优先级对悬决 Pod 进行排序,并且每个悬决的 Pod 会被放置在调度队列中其他优先级较低的悬决 Pod 之前。因此,如果满足调度要求,较高优先级的 Pod 可能会比具有较低优先级的 Pod 更早调度。如果无法调度此类 Pod,调度程序将继续并尝试调度其他较低优先级的 Pod。

抢占

Pod 被创建后会进入队列等待调度。调度器从队列中挑选一个 Pod 并尝试将它调度到某个节点上。如果没有找到满足 Pod 的所指定的所有要求的节点,则触发对悬决 Pod 的抢占逻辑。让我们将悬决 Pod 称为 P。抢占逻辑试图找到一个节点,在该节点中删除一个或多个优先级低于 P 的 Pod,则可以将 P 调度到该节点上。如果找到这样的节点,一个或多个优先级较低的 Pod 会被从节点中驱逐。被驱逐的 Pod 消失后,P 可以被调度到该节点上。

用户暴露的信息

当 Pod P 抢占节点 N 上的一个或多个 Pod 时,Pod P 状态的 nominatedNodeName 字段被设置为节点 N 的名称。该字段帮助调度程序跟踪为 Pod P 保留的资源,并为用户提供有关其集群中抢占的信息。

请注意,Pod P 不一定会调度到“被提名的节点(Nominated Node)”。在 Pod 因抢占而牺牲时,它们将获得体面终止期。如果调度程序正在等待牺牲者 Pod 终止时另一个节点变得可用,则调度程序将使用另一个节点来调度 Pod P。因此,Pod 规约中的 nominatedNodeName 和 nodeName 并不总是相同。此外,如果调度程序抢占节点 N 上的 Pod,但随后比 Pod P 更高优先级的 Pod 到达,则调度程序可能会将节点 N 分配给新的更高优先级的 Pod。在这种情况下,调度程序会清除 Pod P 的 nominatedNodeName。通过这样做,调度程序使 Pod P 有资格抢占另一个节点上的 Pod。

抢占的限制

被抢占牺牲者的体面终止

当 Pod 被抢占时,牺牲者会得到他们的体面终止期。它们可以在体面终止期内完成工作并退出。如果它们不这样做就会被杀死。这个体面终止期在调度程序抢占 Pod 的时间点和待处理的 Pod (P) 可以在节点 (N) 上调度的时间点之间划分出了一个时间跨度。同时,调度器会继续调度其他待处理的 Pod。当牺牲者退出或被终止时,调度程序会尝试在待处理队列中调度 Pod。因此,调度器抢占牺牲者的时间点与 Pod P 被调度的时间点之间通常存在时间间隔。为了最小化这个差距,可以将低优先级 Pod 的体面终止时间设置为零或一个小数字。

支持 PodDisruptionBudget,但不保证

PodDisruptionBudget (PDB) 允许多副本应用程序的所有者限制因自愿性质的干扰而同时终止的 Pod 数量。Kubernetes 在抢占 Pod 时支持 PDB,但对 PDB 的支持是基于尽力而为原则的。调度器会尝试寻找不会因被抢占而违反 PDB 的牺牲者,但如果没有找到这样的牺牲者,抢占仍然会发生,并且即使违反了 PDB 约束也会删除优先级较低的 Pod。

与低优先级 Pod 之间的 Pod 间亲和性

只有当这个问题的答案是肯定的时,才考虑在一个节点上执行抢占操作:“如果从此节点上删除优先级低于悬决 Pod 的所有 Pod,悬决 Pod 是否可以在该节点上调度?”

说明: 抢占并不一定会删除所有较低优先级的 Pod。如果悬决 Pod 可以通过删除少于所有较低优先级的 Pod 来调度,那么只有一部分较低优先级的 Pod 会被删除。即便如此,上述问题的答案必须是肯定的。如果答案是否定的,则不考虑在该节点上执行抢占。

如果悬决 Pod 与节点上的一个或多个较低优先级 Pod 具有 Pod 间亲和性,则在没有这些较低优先级 Pod 的情况下,无法满足 Pod 间亲和性规则。在这种情况下,调度程序不会抢占节点上的任何 Pod。相反,它寻找另一个节点。调度程序可能会找到合适的节点,也可能不会。无法保证悬决 Pod 可以被调度。

我们针对此问题推荐的解决方案是仅针对同等或更高优先级的 Pod 设置 Pod 间亲和性。

跨节点抢占

假设正在考虑在一个节点 N 上执行抢占,以便可以在 N 上调度待处理的 Pod P。只有当另一个节点上的 Pod 被抢占时,P 才可能在 N 上变得可行。下面是一个例子:

  • 正在考虑将 Pod P 调度到节点 N 上。
  • Pod Q 正在与节点 N 位于同一区域的另一个节点上运行。
  • Pod P 与 Pod Q 具有 Zone 维度的反亲和(topologyKey:topology.kubernetes.io/zone)。
  • Pod P 与 Zone 中的其他 Pod 之间没有其他反亲和性设置。
  • 为了在节点 N 上调度 Pod P,可以抢占 Pod Q,但调度器不会进行跨节点抢占。因此,Pod P 将被视为在节点 N 上不可调度。

如果将 Pod Q 从所在节点中移除,则不会违反 Pod 间反亲和性约束,并且 Pod P 可能会被调度到节点 N 上。

如果有足够的需求,并且如果我们找到性能合理的算法,我们可能会考虑在未来版本中添加跨节点抢占。

故障排除

Pod 优先级和抢占可能会产生不必要的副作用。以下是一些潜在问题的示例以及处理这些问题的方法。

Pod 被不必要地抢占

抢占在资源压力较大时从集群中删除现有 Pod,为更高优先级的悬决 Pod 腾出空间。如果你错误地为某些 Pod 设置了高优先级,这些无意的高优先级 Pod 可能会导致集群中出现抢占行为。Pod 优先级是通过设置 Pod 规约中的 priorityClassName 字段来指定的。优先级的整数值然后被解析并填充到 podSpec 的 priority 字段。

为了解决这个问题,你可以将这些 Pod 的 priorityClassName 更改为使用较低优先级的类,或者将该字段留空。默认情况下,空的 priorityClassName 解析为零。

当 Pod 被抢占时,集群会为被抢占的 Pod 记录事件。只有当集群没有足够的资源用于 Pod 时,才会发生抢占。在这种情况下,只有当悬决 Pod(抢占者)的优先级高于受害 Pod 时才会发生抢占。当没有悬决 Pod,或者悬决 Pod 的优先级等于或低于牺牲者时,不得发生抢占。如果在这种情况下发生抢占,请提出问题。

有 Pod 被抢占,但抢占者并没有被调度

当 Pod 被抢占时,它们会收到请求的体面终止期,默认为 30 秒。如果受害 Pod 在此期限内没有终止,它们将被强制终止。一旦所有牺牲者都离开,就可以调度抢占者 Pod。

在抢占者 Pod 等待牺牲者离开的同时,可能某个适合同一个节点的更高优先级的 Pod 被创建。在这种情况下,调度器将调度优先级更高的 Pod 而不是抢占者。

这是预期的行为:具有较高优先级的 Pod 应该取代具有较低优先级的 Pod。

优先级较高的 Pod 在优先级较低的 Pod 之前被抢占

调度程序尝试查找可以运行悬决 Pod 的节点。如果没有找到这样的节点,调度程序会尝试从任意节点中删除优先级较低的 Pod,以便为悬决 Pod 腾出空间。如果具有低优先级 Pod 的节点无法运行悬决 Pod,调度器可能会选择另一个具有更高优先级 Pod 的节点(与其他节点上的 Pod 相比)进行抢占。牺牲者的优先级必须仍然低于抢占者 Pod。

当有多个节点可供执行抢占操作时,调度器会尝试选择具有一组优先级最低的 Pod 的节点。但是,如果此类 Pod 具有 PodDisruptionBudget,当它们被抢占时,则会违反 PodDisruptionBudget,那么调度程序可能会选择另一个具有更高优先级 Pod 的节点。

当存在多个节点抢占且上述场景均不适用时,调度器会选择优先级最低的节点。

Pod 优先级和服务质量之间的相互作用

Pod 优先级和 QoS 类是两个正交特征,交互很少,并且对基于 QoS 类设置 Pod 的优先级没有默认限制。调度器的抢占逻辑在选择抢占目标时不考虑 QoS。抢占会考虑 Pod 优先级并尝试选择一组优先级最低的目标。仅当移除优先级最低的 Pod 不足以让调度程序调度抢占式 Pod,或者最低优先级的 Pod 受 PodDisruptionBudget 保护时,才会考虑优先级较高的 Pod。

kubelet 使用优先级来确定节点压力驱逐 Pod 的顺序。你可以使用 QoS 类来估计 Pod 最有可能被驱逐的顺序。kubelet 根据以下因素对 Pod 进行驱逐排名:

  1. 对紧俏资源的使用是否超过请求值
  2. Pod 优先级
  3. 相对于请求的资源使用量

当某 Pod 的资源用量未超过其请求时,kubelet 节点压力驱逐不会驱逐该 Pod。如果优先级较低的 Pod 没有超过其请求,则不会被驱逐。另一个优先级高于其请求的 Pod 可能会被驱逐。

6 - 节点压力驱逐

节点压力驱逐是 kubelet 主动终止 Pod 以回收节点上资源的过程。

kubelet 监控集群节点的 CPU、内存、磁盘空间和文件系统的 inode 等资源。当这些资源中的一个或者多个达到特定的消耗水平,kubelet 可以主动地使节点上一个或者多个 Pod 失效,以回收资源防止饥饿。

在节点压力驱逐期间,kubelet 将所选 Pod 的 PodPhase 设置为 Failed。这将终止 Pod。

节点压力驱逐不同于 API 发起的驱逐。

kubelet 并不理会你配置的 PodDisruptionBudget 或者是 Pod 的 terminationGracePeriodSeconds。如果你使用了软驱逐条件,kubelet 会考虑你所配置的 eviction-max-pod-grace-period。如果你使用了硬驱逐条件,它使用 0s 宽限期来终止 Pod。

如果 Pod 是由替换失败 Pod 的工作负载资源(例如 StatefulSet 或者 Deployment)管理,则控制平面或 kube-controller-manager 会创建新的 Pod 来代替被驱逐的 Pod。

说明:
kubelet 在终止最终用户 Pod 之前会尝试回收节点级资源。例如,它会在磁盘资源不足时删除未使用的容器镜像。

kubelet 使用各种参数来做出驱逐决定,如下所示:

  • 驱逐信号
  • 驱逐条件
  • 监控间隔

驱逐信号

驱逐信号是特定资源在特定时间点的当前状态。kubelet 使用驱逐信号,通过将信号与驱逐条件进行比较来做出驱逐决定,驱逐条件是节点上应该可用资源的最小量。

kubelet 使用以下驱逐信号:

驱逐信号
描述
memory.available
memory.available := node.status.capacity[memory] - node.stats.memory.workingSet
nodefs.available
nodefs.available := node.stats.fs.available
nodefs.inodesFree
nodefs.inodesFree := node.stats.fs.inodesFree
imagefs.available
imagefs.available := node.stats.runtime.imagefs.available
imagefs.inodesFree
imagefs.inodesFree := node.stats.runtime.imagefs.inodesFree
pid.available
pid.available := node.stats.rlimit.maxpid - node.stats.rlimit.curproc

在上表中,描述列显示了 kubelet 如何获取信号的值。每个信号支持百分比值或者是字面值。kubelet 计算相对于与信号有关的总量的百分比值。

memory.available 的值来自 cgroupfs,而不是像 free -m 这样的工具。这很重要,因为 free -m 在容器中不起作用,如果用户使用节点可分配资源这一功能特性,资源不足的判定是基于 CGroup 层次结构中的用户 Pod 所处的局部及 CGroup 根节点作出的。这个脚本重现了 kubelet 为计算 memory.available 而执行的相同步骤。kubelet 在其计算中排除了 inactive_file(即非活动 LRU 列表上基于文件来虚拟的内存的字节数),因为它假定在压力下内存是可回收的。

kubelet 支持以下文件系统分区:

  1. nodefs:节点的主要文件系统,用于本地磁盘卷、emptyDi

    以上是关于[K8s]Kubernetes-调度抢占驱逐的主要内容,如果未能解决你的问题,请参考以下文章

    K8s Pod 驱逐调度时间窗口(按需调整参数使节点故障后可以快速转移Pod)

    解决k8s调度不均衡问题

    解决k8s调度不均衡问题

    解决k8s调度不均衡问题

    k8s容器-运维管理篇

    FreeRTOS——任务调度—抢占式,时间片和合作式