[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:节点的主要文件系统,用于本地磁盘卷、emptyDir、日志存储等。例如,nodefs 包含 /var/lib/kubelet/。
  2. imagefs:可选文件系统,供容器运行时存储容器镜像和容器可写层。

kubelet 会自动发现这些文件系统并忽略其他文件系统。kubelet 不支持其他配置。

说明:
一些 kubelet 垃圾收集功能已被弃用,以支持驱逐。有关已弃用功能的列表,请参阅 kubelet 垃圾收集弃用。

驱逐条件

你可以为 kubelet 指定自定义驱逐条件,以便在作出驱逐决定时使用。

驱逐条件的形式为 [eviction-signal][operator][quantity],其中:

  • eviction-signal 是要使用的驱逐信号。
  • operator 是你想要的关系运算符,比如 <(小于)。
  • quantity 是驱逐条件数量,例如 1Gi。quantity 的值必须与 Kubernetes 使用的数量表示相匹配。你可以使用文字值或百分比(%)。

例如,如果一个节点的总内存为 10Gi 并且你希望在可用内存低于 1Gi 时触发驱逐,则可以将驱逐条件定义为 memory.available<10% 或 memory.available< 1G。你不能同时使用二者。

你可以配置软和硬驱逐条件。

软驱逐条件

软驱逐条件将驱逐条件与管理员所必须指定的宽限期配对。在超过宽限期之前,kubelet 不会驱逐 Pod。如果没有指定的宽限期,kubelet 会在启动时返回错误。

你可以既指定软驱逐条件宽限期,又指定 Pod 终止宽限期的上限,给 kubelet 在驱逐期间使用。如果你指定了宽限期的上限并且 Pod 满足软驱逐阈条件,则 kubelet 将使用两个宽限期中的较小者。如果你没有指定宽限期上限,kubelet 会立即杀死被驱逐的 Pod,不允许其体面终止。

你可以使用以下标志来配置软驱逐条件:

  • eviction-soft:一组驱逐条件,如 memory.available<1.5Gi,如果驱逐条件持续时长超过指定的宽限期,可以触发 Pod 驱逐。
  • eviction-soft-grace-period:一组驱逐宽限期,如 memory.available=1m30s,定义软驱逐条件在触发 Pod 驱逐之前必须保持多长时间。
  • eviction-max-pod-grace-period:在满足软驱逐条件而终止 Pod 时使用的最大允许宽限期(以秒为单位)。

硬驱逐条件

硬驱逐条件没有宽限期。当达到硬驱逐条件时,kubelet 会立即杀死 pod,而不会正常终止以回收紧缺的资源。

你可以使用 eviction-hard 标志来配置一组硬驱逐条件,例如 memory.available<1Gi。

kubelet 具有以下默认硬驱逐条件:

  • memory.available<100Mi
  • nodefs.available<10%
  • imagefs.available<15%
  • nodefs.inodesFree<5%(Linux 节点)

驱逐监测间隔

kubelet 根据其配置的 housekeeping-interval(默认为 10s)评估驱逐条件。

节点条件

kubelet 报告节点状况以反映节点处于压力之下,因为满足硬或软驱逐条件,与配置的宽限期无关。

kubelet 根据下表将驱逐信号映射为节点状况:

节点条件
驱逐信号
描述
MemoryPressure
memory.available
节点上的可用内存已满足驱逐条件
DiskPressure
nodefs.available、nodefs.inodesFree、imagefs.available 或 imagefs.inodesFree
节点的根文件系统或映像文件系统上的可用磁盘空间和 inode 已满足驱逐条件
PIDPressure
pid.available
(Linux) 节点上的可用进程标识符已低于驱逐条件

kubelet 根据配置的 --node-status-update-frequency 更新节点条件,默认为 10s。

节点条件振荡

在某些情况下,节点在软驱逐条件上下振荡,而没有保持定义的宽限期。这会导致报告的节点条件在 true 和 false 之间不断切换,从而导致错误的驱逐决策。

为了防止振荡,你可以使用 eviction-pressure-transition-period 标志,该标志控制 kubelet 在将节点条件转换为不同状态之前必须等待的时间。过渡期的默认值为 5m。

回收节点级资源

kubelet 在驱逐最终用户 Pod 之前会先尝试回收节点级资源。

当报告 DiskPressure 节点状况时,kubelet 会根据节点上的文件系统回收节点级资源。

有 imagefs

如果节点有一个专用的 imagefs 文件系统供容器运行时使用,kubelet 会执行以下操作:

  • 如果 nodefs 文件系统满足驱逐条件,kubelet 垃圾收集死亡 Pod 和容器。
  • 如果 imagefs 文件系统满足驱逐条件,kubelet 将删除所有未使用的镜像。

没有 imagefs

如果节点只有一个满足驱逐条件的 nodefs 文件系统,kubelet 按以下顺序释放磁盘空间:

  1. 对死亡的 Pod 和容器进行垃圾收集
  2. 删除未使用的镜像

kubelet 驱逐时 Pod 的选择

如果 kubelet 回收节点级资源的尝试没有使驱逐信号低于条件,则 kubelet 开始驱逐最终用户 Pod。

kubelet 使用以下参数来确定 Pod 驱逐顺序:

  1. Pod 的资源使用是否超过其请求
  2. Pod 优先级
  3. Pod 相对于请求的资源使用情况

因此,kubelet 按以下顺序排列和驱逐 Pod:

  1. 首先考虑资源使用量超过其请求的 BestEffort 或 Burstable Pod。这些 Pod 会根据它们的优先级以及它们的资源使用级别超过其请求的程度被逐出。
  2. 资源使用量少于请求量的 Guaranteed Pod 和 Burstable Pod 根据其优先级被最后驱逐。

说明:
kubelet 不使用 Pod 的 QoS 类来确定驱逐顺序。在回收内存等资源时,你可以使用 QoS 类来估计最可能的 Pod 驱逐顺序。QoS 不适用于临时存储(EphemeralStorage)请求,因此如果节点在 DiskPressure 下,则上述场景将不适用。

仅当 Guaranteed Pod 中所有容器都被指定了请求和限制并且二者相等时,才保证 Pod 不被驱逐。这些 Pod 永远不会因为另一个 Pod 的资源消耗而被驱逐。如果系统守护进程(例如 kubelet、docker 和 journald)消耗的资源比通过 system-reserved 或 kube-reserved 分配保留的资源多,并且该节点只有 Guaranteed 或 Burstable Pod 使用的资源少于其上剩余的请求,那么 kubelet 必须选择驱逐这些 Pod 中的一个以保持节点稳定性并减少资源匮乏对其他 Pod 的影响。在这种情况下,它会选择首先驱逐最低优先级的 Pod。

当 kubelet 因 inode 或 PID 不足而驱逐 pod 时,它使用优先级来确定驱逐顺序,因为 inode 和 PID 没有请求。

kubelet 根据节点是否具有专用的 imagefs 文件系统对 Pod 进行不同的排序:

有 imagefs

如果 nodefs 触发驱逐,kubelet 会根据 nodefs 使用情况(本地卷 + 所有容器的日志)对 Pod 进行排序。

如果 imagefs 触发驱逐,kubelet 会根据所有容器的可写层使用情况对 Pod 进行排序。

没有 imagefs

如果 nodefs 触发驱逐,kubelet 会根据磁盘总用量(本地卷 + 日志和所有容器的可写层)对 Pod 进行排序。

最小驱逐回收

在某些情况下,驱逐 Pod 只会回收少量的紧俏资源。这可能导致 kubelet 反复达到配置的驱逐条件并触发多次驱逐。

你可以使用 --eviction-minimum-reclaim 标志或 kubelet 配置文件为每个资源配置最小回收量。当 kubelet 注意到某个资源耗尽时,它会继续回收该资源,直到回收到你所指定的数量为止。

例如,以下配置设置最小回收量:

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
evictionHard:
  memory.available: "500Mi"
  nodefs.available: "1Gi"
  imagefs.available: "100Gi"
evictionMinimumReclaim:
  memory.available: "0Mi"
  nodefs.available: "500Mi"
  imagefs.available: "2Gi"

在这个例子中,如果 nodefs.available 信号满足驱逐条件,kubelet 会回收资源,直到信号达到 1Gi 的条件,然后继续回收至少 500Mi 直到信号达到 1.5Gi。

类似地,kubelet 会回收 imagefs 资源,直到 imagefs.available 信号达到 102Gi。

对于所有资源,默认的 eviction-minimum-reclaim 为 0。

节点内存不足行为

如果节点在 kubelet 能够回收内存之前遇到内存不足(OOM)事件,则节点依赖 oom_killer 来响应。

kubelet 根据 Pod 的服务质量(QoS)为每个容器设置一个 oom_score_adj 值。

服务质量
oom_score_adj
Guaranteed
-997
BestEffort
1000
Burstable
min(max(2, 1000 - (1000 * memoryRequestBytes) / machineMemoryCapacityBytes), 999)

说明:
kubelet 还将具有 system-node-critical 优先级 的 Pod 中的容器 oom_score_adj 值设为 -997。

如果 kubelet 在节点遇到 OOM 之前无法回收内存,则 oom_killer 根据它在节点上使用的内存百分比计算 oom_score,然后加上 oom_score_adj 得到每个容器有效的 oom_score。然后它会杀死得分最高的容器。

这意味着低 QoS Pod 中相对于其调度请求消耗内存较多的容器,将首先被杀死。

与 Pod 驱逐不同,如果容器被 OOM 杀死,kubelet 可以根据其 RestartPolicy 重新启动它。

最佳实践

以下部分描述了驱逐配置的最佳实践。

可调度的资源和驱逐策略

当你为 kubelet 配置驱逐策略时,你应该确保调度程序不会在 Pod 触发驱逐时对其进行调度,因为这类 Pod 会立即引起内存压力。

考虑以下场景:

  • 节点内存容量:10Gi
  • 操作员希望为系统守护进程(内核、kubelet 等)保留 10% 的内存容量
  • 操作员希望驱逐内存利用率为 95% 的Pod,以减少系统 OOM 的概率。

为此,kubelet 启动设置如下:

--eviction-hard=memory.available<500Mi
--system-reserved=memory=1.5Gi

在此配置中,--system-reserved 标志为系统预留了 1.5Gi 的内存,即总内存的 10% + 驱逐条件量。

如果 Pod 使用的内存超过其请求值或者系统使用的内存超过 1Gi,则节点可以达到驱逐条件,这使得 memory.available 信号低于 500Mi 并触发条件。

DaemonSet

Pod 优先级是做出驱逐决定的主要因素。如果你不希望 kubelet 驱逐属于 DaemonSet 的 Pod,请在 Pod 规约中为这些 Pod 提供足够高的 priorityClass。你还可以使用优先级较低的 priorityClass 或默认配置,仅在有足够资源时才运行 DaemonSet Pod。

已知问题

以下部分描述了与资源不足处理相关的已知问题。

kubelet 可能不会立即观察到内存压力

默认情况下,kubelet 轮询 cAdvisor 以定期收集内存使用情况统计信息。如果该轮询时间窗口内内存使用量迅速增加,kubelet 可能无法足够快地观察到 MemoryPressure,但是 OOMKiller 仍将被调用。

你可以使用 --kernel-memcg-notification 标志在 kubelet 上启用 memcg 通知 API,以便在超过条件时立即收到通知。

如果你不是追求极端利用率,而是要采取合理的过量使用措施,则解决此问题的可行方法是使用 --kube-reserved 和 --system-reserved 标志为系统分配内存。

active_file 内存未被视为可用内存

在 Linux 上,内核跟踪活动 LRU 列表上的基于文件所虚拟的内存字节数作为 active_file 统计信息。kubelet 将 active_file 内存区域视为不可回收。对于大量使用块设备形式的本地存储(包括临时本地存储)的工作负载,文件和块数据的内核级缓存意味着许多最近访问的缓存页面可能被计为 active_file。如果这些内核块缓冲区中在活动 LRU 列表上有足够多,kubelet 很容易将其视为资源用量过量并为节点设置内存压力污点,从而触发 Pod 驱逐。

你可以通过为可能执行 I/O 密集型活动的容器设置相同的内存限制和内存请求来应对该行为。你将需要估计或测量该容器的最佳内存限制值。

7 - API 发起的驱逐

API 发起的驱逐是一个先调用 Eviction API 创建驱逐对象,再由该对象体面地中止 Pod 的过程。

你可以通过 kube-apiserver 的客户端,比如 kubectl drain 这样的命令,直接调用 Eviction API 发起驱逐。此操作创建一个 Eviction 对象,该对象再驱动 API 服务器终止选定的 Pod。

API 发起的驱逐将遵从你的 PodDisruptionBudgets 和 terminationGracePeriodSeconds 配置。

8 - 扩展资源的资源装箱

FEATURE STATE: Kubernetes 1.16 [alpha]

使用 RequestedToCapacityRatioResourceAllocation 优先级函数,可以将 kube-scheduler 配置为支持包含扩展资源在内的资源装箱操作。优先级函数可用于根据自定义需求微调 kube-scheduler 。

使用 RequestedToCapacityRatioResourceAllocation 启用装箱

Kubernetes 允许用户指定资源以及每类资源的权重,以便根据请求数量与可用容量之比率为节点评分。这就使得用户可以通过使用适当的参数来对扩展资源执行装箱操作,从而提高了大型集群中稀缺资源的利用率。RequestedToCapacityRatioResourceAllocation 优先级函数的行为可以通过名为 RequestedToCapacityRatioArgs 的配置选项进行控制。该标志由两个参数 shape 和 resources 组成。shape 允许用户根据 utilization 和 score 值将函数调整为最少请求(least requested)或最多请求(most requested)计算。resources 包含由 name 和 weight 组成,name 指定评分时要考虑的资源,weight 指定每种资源的权重。

以下是一个配置示例,该配置将 requestedToCapacityRatioArguments 设置为对扩展资源 intel.com/foo 和 intel.com/bar 的装箱行为

apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
profiles:
# ...
  pluginConfig:
  - name: RequestedToCapacityRatio
    args:
      shape:
      - utilization: 0
        score: 10
      - utilization: 100
        score: 0
      resources:
      - name: intel.com/foo
        weight: 3
      - name: intel.com/bar
        weight: 5

使用 kube-scheduler 标志 --config=/path/to/config/file 引用 KubeSchedulerConfiguration 文件将配置传递给调度器。

默认情况下此功能处于被禁用状态

调整 RequestedToCapacityRatioResourceAllocation 优先级函数

shape 用于指定 RequestedToCapacityRatioPriority 函数的行为。

 {"utilization": 0, "score": 0},
 {"utilization": 100, "score": 10}

上面的参数在 utilization 为 0% 时给节点评分为 0,在 utilization 为 100% 时给节点评分为 10,因此启用了装箱行为。要启用最少请求(least requested)模式,必须按如下方式反转得分值。

 {"utilization": 0, "score": 10},
 {"utilization": 100, "score": 0}

resources 是一个可选参数,默认情况下设置为:

"resources": [
    {"name": "CPU", "weight": 1},
    {"name": "Memory", "weight": 1}
]

它可以用来添加扩展资源,如下所示:

"resources": [
    {"name": "intel.com/foo", "weight": 5},
    {"name": "CPU", "weight": 3},
    {"name": "Memory", "weight": 1}
]

weight 参数是可选的,如果未指定,则设置为 1。同时,weight 不能设置为负值。

RequestedToCapacityRatioResourceAllocation 优先级函数如何对节点评分

本节适用于希望了解此功能的内部细节的人员。以下是如何针对给定的一组值来计算节点得分的示例。

请求的资源

intel.com/foo: 2
Memory: 256MB
CPU: 2

资源权重

intel.com/foo: 5
Memory: 1
CPU: 3

FunctionShapePoint {{0, 0}, {100, 10}}

节点 Node 1 配置

可用:
  intel.com/foo : 4
  Memory : 1 GB
  CPU: 8

已用:
  intel.com/foo: 1
  Memory: 256MB
  CPU: 1

节点得分:

intel.com/foo  = resourceScoringFunction((2+1),4)
               = (100 - ((4-3)*100/4)
               = (100 - 25)
               = 75
               = rawScoringFunction(75)
               = 7

Memory         = resourceScoringFunction((256+256),1024)
               = (100 -((1024-512)*100/1024))
               = 50
               = rawScoringFunction(50)
               = 5

CPU            = resourceScoringFunction((2+1),8)
               = (100 -((8-3)*100/8))
               = 37.5
               = rawScoringFunction(37.5)
               = 3

NodeScore   =  (7 * 5) + (5 * 1) + (3 * 3) / (5 + 1 + 3)
            =  5


节点 Node 2 配置

可用:
  intel.com/foo: 8
  Memory: 1GB
  CPU: 8

已用:
  intel.com/foo: 2
  Memory: 512MB
  CPU: 6

节点得分:

intel.com/foo  = resourceScoringFunction((2+2),8)
               = (100 - ((8-4)*100/8)
               = (100 - 50)
               = 50
               = rawScoringFunction(50)
               = 5

Memory         = resourceScoringFunction((256+512),1024)
               = (100 -((1024-768)*100/1024))
               = 75
               = rawScoringFunction(75)
               = 7

CPU            = resourceScoringFunction((2+6),8)
               = (100 -((8-8)*100/8))
               = 100
               = rawScoringFunction(100)
               = 10

NodeScore   =  (5 * 5) + (7 * 1) + (10 * 3) / (5 + 1 + 3)
            =  7

9 - 调度框架

FEATURE STATE: Kubernetes 1.19 [stable]

调度框架是面向 Kubernetes 调度器的一种插件架构,它为现有的调度器添加了一组新的“插件” API。插件会被编译到调度器之中。这些 API 允许大多数调度功能以插件的形式实现,同时使调度“核心”保持简单且可维护。请参考调度框架的设计提案获取框架设计的更多技术信息。

框架工作流程

调度框架定义了一些扩展点。调度器插件注册后在一个或多个扩展点处被调用。这些插件中的一些可以改变调度决策,而另一些仅用于提供信息。

每次调度一个 Pod 的尝试都分为两个阶段,即调度周期绑定周期

调度周期和绑定周期

调度周期为 Pod 选择一个节点,绑定周期将该决策应用于集群。调度周期和绑定周期一起被称为“调度上下文”。

调度周期是串行运行的,而绑定周期可能是同时运行的。

如果确定 Pod 不可调度或者存在内部错误,则可以终止调度周期或绑定周期。Pod 将返回队列并重试。

扩展点

下图显示了一个 Pod 的调度上下文以及调度框架公开的扩展点。在此图片中,“过滤器”等同于“断言”,“评分”相当于“优先级函数”。

一个插件可以在多个扩展点处注册,以执行更复杂或有状态的任务。

image

调度框架扩展点

队列排序

队列排序插件用于对调度队列中的 Pod 进行排序。队列排序插件本质上提供 less(Pod1, Pod2) 函数。一次只能启动一个队列插件。

前置过滤

前置过滤插件用于预处理 Pod 的相关信息,或者检查集群或 Pod 必须满足的某些条件。如果 PreFilter 插件返回错误,则调度周期将终止。

过滤

过滤插件用于过滤出不能运行该 Pod 的节点。对于每个节点,调度器将按照其配置顺序调用这些过滤插件。如果任何过滤插件将节点标记为不可行,则不会为该节点调用剩下的过滤插件。节点可以被同时进行评估。

后置过滤

这些插件在筛选阶段后调用,但仅在该 Pod 没有可行的节点时调用。插件按其配置的顺序调用。如果任何后过滤器插件标记节点为“可调度”,则其余的插件不会调用。典型的后筛选实现是抢占,试图通过抢占其他 Pod 的资源使该 Pod 可以调度。

前置评分

前置评分插件用于执行 “前置评分” 工作,即生成一个可共享状态供评分插件使用。如果 PreScore 插件返回错误,则调度周期将终止。

评分

评分插件用于对通过过滤阶段的节点进行排名。调度器将为每个节点调用每个评分插件。将有一个定义明确的整数范围,代表最小和最大分数。在标准化评分阶段之后,调度器将根据配置的插件权重合并所有插件的节点分数。

标准化评分

标准化评分插件用于在调度器计算节点的排名之前修改分数。在此扩展点注册的插件将使用同一插件的评分结果被调用。每个插件在每个调度周期调用一次。

例如,假设一个 BlinkingLightScorer 插件基于具有的闪烁指示灯数量来对节点进行排名。

func ScoreNode(_ *v1.pod, n *v1.Node) (int, error) {
   return getBlinkingLightCount(n)
}

然而,最大的闪烁灯个数值可能比 NodeScoreMax 小。要解决这个问题,BlinkingLightScorer 插件还应该注册该扩展点。

func NormalizeScores(scores map[string]int) {
   highest := 0
   for _, score := range scores {
      highest = max(highest, score)
   }
   for node, score := range scores {
      scores[node] = score*NodeScoreMax/highest
   }
}

如果任何 NormalizeScore 插件返回错误,则调度阶段将终止。

说明: 希望执行“预保留”工作的插件应该使用 NormalizeScore 扩展点。

Reserve

Reserve 是一个信息性的扩展点。管理运行时状态的插件(也成为“有状态插件”)应该使用此扩展点,以便调度器在节点给指定 Pod 预留了资源时能够通知该插件。这是在调度器真正将 Pod 绑定到节点之前发生的,并且它存在是为了防止在调度器等待绑定成功时发生竞争情况。

这个是调度周期的最后一步。一旦 Pod 处于保留状态,它将在绑定周期结束时触发不保留插件(失败时)或绑定后插件(成功时)。

Permit

Permit 插件在每个 Pod 调度周期的最后调用,用于防止或延迟 Pod 的绑定。一个允许插件可以做以下三件事之一:

  • 批准
    一旦所有 Permit 插件批准 Pod 后,该 Pod 将被发送以进行绑定。
  • 拒绝
    如果任何 Permit 插件拒绝 Pod,则该 Pod 将被返回到调度队列。这将触发Unreserve 插件。
  • 等待(带有超时)
    如果一个 Permit 插件返回 “等待” 结果,则 Pod 将保持在一个内部的 “等待中” 的 Pod 列表,同时该 Pod 的绑定周期启动时即直接阻塞直到得到批准。如果超时发生,等待变成拒绝,并且 Pod 将返回调度队列,从而触发 Unreserve 插件。

说明: 尽管任何插件可以访问 “等待中” 状态的 Pod 列表并批准它们 (查看 FrameworkHandle)。我们期望只有允许插件可以批准处于 “等待中” 状态的预留 Pod 的绑定。一旦 Pod 被批准了,它将发送到预绑定阶段。

预绑定

预绑定插件用于执行 Pod 绑定前所需的任何工作。例如,一个预绑定插件可能需要提供网络卷并且在允许 Pod 运行在该节点之前将其挂载到目标节点上。

如果任何 PreBind 插件返回错误,则 Pod 将被拒绝并且退回到调度队列中。

Bind

Bind 插件用于将 Pod 绑定到节点上。直到所有的 PreBind 插件都完成,Bind 插件才会被调用。各绑定插件按照配置顺序被调用。绑定插件可以选择是否处理指定的 Pod。如果绑定插件选择处理 Pod,剩余的绑定插件将被跳过。

绑定后

这是个信息性的扩展点。绑定后插件在 Pod 成功绑定后被调用。这是绑定周期的结尾,可用于清理相关的资源。

Unreserve

这是个信息性的扩展点。如果 Pod 被保留,然后在后面的阶段中被拒绝,则 Unreserve 插件将被通知。Unreserve 插件应该清楚保留 Pod 的相关状态。

使用此扩展点的插件通常也使用 Reserve。

插件 API

插件 API 分为两个步骤。首先,插件必须完成注册并配置,然后才能使用扩展点接口。扩展点接口具有以下形式。

type Plugin interface {
   Name() string
}

type QueueSortPlugin interface {
   Plugin
   Less(*v1.pod, *v1.pod) bool
}

type PreFilterPlugin interface {
   Plugin
   PreFilter(context.Context, *framework.CycleState, *v1.pod) error
}

// ...

插件配置

你可以在调度器配置中启用或禁用插件。如果你在使用 Kubernetes v1.18 或更高版本,大部分调度插件都在使用中且默认启用。

除了默认的插件,你还可以实现自己的调度插件并且将它们与默认插件一起配置。你可以访问scheduler-plugins 了解更多信息。

如果你正在使用 Kubernetes v1.18 或更高版本,你可以将一组插件设置为一个调度器配置文件,然后定义不同的配置文件来满足各类工作负载。了解更多关于多配置文件。

10 - 调度器性能调优

FEATURE STATE: Kubernetes 1.14 [beta]

作为 kubernetes 集群的默认调度器,kube-scheduler 主要负责将 Pod 调度到集群的 Node 上。

在一个集群中,满足一个 Pod 调度请求的所有 Node 称之为可调度 Node。调度器先在集群中找到一个 Pod 的可调度 Node,然后根据一系列函数对这些可调度 Node 打分,之后选出其中得分最高的 Node 来运行 Pod。最后,调度器将这个调度决定告知 kube-apiserver,这个过程叫做绑定(Binding)。

这篇文章将会介绍一些在大规模 Kubernetes 集群下调度器性能优化的方式。

在大规模集群中,你可以调节调度器的表现来平衡调度的延迟(新 Pod 快速就位)和精度(调度器很少做出糟糕的放置决策)。

你可以通过设置 kube-scheduler 的 percentageOfNodesToScore 来配置这个调优设置。这个 KubeSchedulerConfiguration 设置决定了调度集群中节点的阈值。

设置阈值

percentageOfNodesToScore 选项接受从 0 到 100 之间的整数值。0 值比较特殊,表示 kube-scheduler 应该使用其编译后的默认值。如果你设置 percentageOfNodesToScore 的值超过了 100,kube-scheduler 的表现等价于设置值为 100。

要修改这个值,先编辑 kube-scheduler 的配置文件然后重启调度器。大多数情况下,这个配置文件是 /etc/kubernetes/config/kube-scheduler.yaml。

修改完成后,你可以执行

kubectl get pods -n kube-system | grep kube-scheduler

来检查该 kube-scheduler 组件是否健康。

节点打分阈值

要提升调度性能,kube-scheduler 可以在找到足够的可调度节点之后停止查找。在大规模集群中,比起考虑每个节点的简单方法相比可以节省时间。

你可以使用整个集群节点总数的百分比作为阈值来指定需要多少节点就足够。kube-scheduler 会将它转换为节点数的整数值。在调度期间,如果 kube-scheduler 已确认的可调度节点数足以超过了配置的百分比数量,kube-scheduler 将停止继续查找可调度节点并继续进行打分阶段。

调度器如何遍历节点详细介绍了这个过程。

默认阈值

如果你不指定阈值,Kubernetes 使用线性公式计算出一个比例,在 100-节点集群下取 50%,在 5000-节点的集群下取 10%。这个自动设置的参数的最低值是 5%。

这意味着,调度器至少会对集群中 5% 的节点进行打分,除非用户将该参数设置的低于 5。

如果你想让调度器对集群内所有节点进行打分,则将 percentageOfNodesToScore 设置为 100。

示例

下面就是一个将 percentageOfNodesToScore 参数设置为 50% 的例子。

apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration
algorithmSource:
  provider: DefaultProvider

...

percentageOfNodesToScore: 50

调节 percentageOfNodesToScore 参数

percentageOfNodesToScore 的值必须在 1 到 100 之间,而且其默认值是通过集群的规模计算得来的。另外,还有一个 50 个 Node 的最小值是硬编码在程序中。

值得注意的是,该参数设置后可能会导致只有集群中少数节点被选为可调度节点,很多节点都没有进入到打分阶段。这样就会造成一种后果,一个本来可以在打分阶段得分很高的节点甚至都不能进入打分阶段。

由于这个原因,这个参数不应该被设置成一个很低的值。通常的做法是不会将这个参数的值设置的低于 10。很低的参数值一般在调度器的吞吐量很高且对节点的打分不重要的情况下才使用。换句话说,只有当你更倾向于在可调度节点中任意选择一个节点来运行这个 Pod 时,才使用很低的参数设置。

调度器做调度选择的时候如何覆盖所有的 Node

如果你想要理解这一个特性的内部细节,那么请仔细阅读这一章节。

在将 Pod 调度到节点上时,为了让集群中所有节点都有公平的机会去运行这些 Pod,调度器将会以轮询的方式覆盖全部的 Node。你可以将 Node 列表想象成一个数组。调度器从数组的头部开始筛选可调度节点,依次向后直到可调度节点的数量达到 percentageOfNodesToScore 参数的要求。在对下一个 Pod 进行调度的时候,前一个 Pod 调度筛选停止的 Node 列表的位置,将会来作为这次调度筛选 Node 开始的位置。

如果集群中的 Node 在多个区域,那么调度器将从不同的区域中轮询 Node,来确保不同区域的 Node 接受可调度性检查。如下例,考虑两个区域中的六个节点:

Zone 1: Node 1, Node 2, Node 3, Node 4
Zone 2: Node 5, Node 6

调度器将会按照如下的顺序去评估 Node 的可调度性:

Node 1, Node 5, Node 2, Node 6, Node 3, Node 4

在评估完所有 Node 后,将会返回到 Node 1,从头开始。

posted @ 2021-12-31 14:48  jpSpaceX  阅读(1651)  评论(0编辑  收藏  举报