# kubernetes进阶-利用 Node 和 Pod 亲和性提高服务可用性


利用 Node 和 Pod 亲和性，将 Pod 分布到不同的节点上，提高服务可用性.

<!--more-->

## 动机

1. 在生产环境中，为保证服务的高可用性，需要将 Pod 的副本数设置为多个（2个以上）。但是如果该服务的所有 Pod 都刚好被调度到某一个节点上，当这个节点刚好出故障的时候，就会造成该服务完全无法访问，影响业务的稳定性。

2. 为防止这种情况的发生，我们可使用 PodAntiAffinity **(Pod)反亲和**性调度策略来将同一个 Deployment 控制器创建的 Pod 分布到不同的节点上。如果其中一个节点出现故障，造成故障节点上的 Pod 无法访问，分布在其他节点上的 Pod 依然可以正常提供服务，这样一来就保证服务的稳定性和可用性。

3. 我们通过合理利用亲和性的能力，根据业务特性配置合理的亲和性匹配规则，也能有效提高集群中的资源利用率。如某个业务服务是 CPU 密集型，有可能 Pod 会被调度到内存密集型的服务器上，导致内存密集型的 CPU 被占满，但内存几乎没怎么用，会造成较大的资源浪费。如果我们配置相应的亲和性规则来配合调度，让 CPU 密集型的服务 Pod 寻找 CPU 密集型的节点，将有效提升资源利用率。

## 概念理解

在正式开始之前，我们先了解一下与之相关概念和名词。

### Node 亲和性

> Node 与 Pod 之间的匹配规则。

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

- requiredDuringSchedulingIgnoredDuringExecution 调度器只有在规则被满足的时候才能执行调度。此功能类似于 nodeSelector， 但其语法表达能力更强。

- preferredDuringSchedulingIgnoredDuringExecution 调度器会尝试寻找满足对应规则的节点。如果找不到匹配的节点，调度器仍然会调度该 Pod。

{{< admonition >}}

说明：

在上述类型中，IgnoredDuringExecution 意味着如果节点标签在 Kubernetes 调度 Pod 后发生了变更，Pod 仍将继续运行

{{< /admonition >}}

我们可以使用 Pod 声明中的 .spec.affinity.nodeAffinity 字段来设置节点亲和性。 例如：

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: disktype
            operator: In
            values:
            - ssd            
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
```

在这一示例中，所应用的规则如下：

- 节点必须包含一个键名为 disktype 的标签， 并且该标签的取值必须为 ssd 。

- 可以使用 operator 字段来为 Kubernetes 设置在解释规则时要使用的逻辑操作符。 

- 可以使用 In、NotIn、Exists、DoesNotExist、Gt 和 Lt 之一作为操作符。

<font color=red>**NotIn 和 DoesNotExist 可用来实现节点反亲和性行为。 你也可以使用节点污点将 Pod 从特定节点上驱逐。**</font>

{{< admonition >}}

说明：

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

2. 如果你在与 nodeAffinity 类型关联的 nodeSelectorTerms 中指定多个条件， 只要其中一个 nodeSelectorTerms 满足的话，Pod 就可以被调度到节点上。

3. 如果你在与 nodeSelectorTerms 中的条件相关联的单个 matchExpressions 字段中指定多个表达式， 则只有当所有表达式都满足时，Pod 才能被调度到节点上。

4. weight 字段，其取值范围是 1 到 100。总分最高的节点的优先级也最高。

{{< /admonition >}}

### Pod 亲和性

> Pod 与 Pod 之间的匹配规则。

#### 1.亲和性类型

Pod 间亲和性的两种类型：

- PodAffinity

  即将关联性较强的多个 Pod 调度到一起。例如，一个 Pod 提供了 Mysql 数据库服务，另一个 Pod 提供 Nginx 服务，Nginx 需要调用 Mysql 服务，如果两个 Pod 被调度到不同的机房或区域，那么 Pod 之间的延迟可能会存在一定的影响，为了降低影响，我们更倾向于将两个 Pod 调度到相同地方。

- PodAntiAffinity

  与 PodAffinity 相反，即不希望两个（或者多个）Pod 被调度到一起。

#### 2.规则类型

Pod 的亲和性与反亲和性都有两种规则类型：

- requiredDuringSchedulingIgnoredDuringExecution 告诉调度器， 将两个服务的 Pod 放到同一个拓扑域内，如果找不到满足规则的节点，则 Pod 将无法被调度。 

- preferredDuringSchedulingIgnoredDuringExecution 告诉调度器， 将两个服务的 Pod 放到同一个拓扑域内，但如果找不到满足规则的节点，调度器仍然会调度该 Pod 。 

为了便于书写和表达，我们约定一下用语。

- 将包含 requiredDuringSchedulingIgnoredDuringExecution 子句的亲和性类型称之为硬亲和。

- 将包含 preferredDuringSchedulingIgnoredDuringExecution子句的亲和性类型称之为软亲和。

#### 3.拓扑域

"如果 X 上已经运行了一个或多个满足规则 Y 的 Pod， 则这个 Pod 应该（或者在反亲和性的情况下不应该）运行在 X 上"。 这里的 X 可以是节点、机架、云提供商可用区或地理区域或类似的拓扑域， Y 则是 Kubernetes 尝试满足的规则。

```yaml
apiVersion: v1
kind: Node
Metadata:
  ...
  labels:
    kubernetes.io/hostname: cn-hangzhou.172.16.4.23
    # 仅当你使用 cloudprovider 时才会设置以下标签。
    topology.kubernetes.io/region: cn-hangzhou
    topology.kubernetes.io/zone: cn-hangzhou-g
  ...
```

画图可能更好理解。如果以 topology.kubernetes.io/zone 为拓扑域：

当 cn-hangzhou-l 可用区中某个节点有一个或多个 Pod 满足 myPod 上设置的规则时，Pod 会被调度到该可用区上的某个节点（可以是 node3 ，也可以是 node4 ，因为他们都是在一个拓扑域上）。

{{< image src="拓扑域.png" caption="拓扑域" >}}

### Pod 拓扑分布约束

拓扑分布约束（Topology Spread Constraints），控制 Pod 在集群内故障域之间的分布， 例如区域（Region）、可用区（Zone）、节点和其他用户自定义拓扑域。 这样做有助于实现高可用并提升资源利用率。

你可以将集群级约束设为默认值，或为个别工作负载配置拓扑分布约束。

#### 适用场景

例如我们有一个拥有 3 可用区的集群，可用区分别是 zoneA 、 zoneB 和 zoneC 。我们其中一个服务是 3 副本，我们不希望这个服务的 3 个副本全部都集中在其中一个可用区，因为这样可能会有潜在的整个可用区出现故障而导致服务无法访问的风险。

#### 约束字段

Pod API 包括一个 spec.topologySpreadConstraints 字段。这个字段的用法如下所示：

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: example-pod
spec:
  # 配置一个拓扑分布约束
  topologySpreadConstraints:
    - maxSkew: <integer> # 必须大于0，全局最小差值，就是各个拓扑域之间 Pod 的最大差值
      minDomains: <integer> # 可选；自从 v1.25 开始成为 Beta
      topologyKey: <string> # 是节点标签的键
      whenUnsatisfiable: <string> # 不满足分布约束时如何处理
      labelSelector: <object> # 用于查找匹配的 Pod
      matchLabelKeys: <list> # 可选；自从 v1.27 开始成为 Beta
      nodeAffinityPolicy: [Honor|Ignore] # 可选；自从 v1.26 开始成为 Beta
      nodeTaintsPolicy: [Honor|Ignore] # 可选；自从 v1.26 开始成为 Beta
  ### 其他 Pod 字段置于此处
```

#### 分布约束定义

你可以定义一个或多个 topologySpreadConstraints 条目以指导 kube-scheduler 如何将每个新来的 Pod 与跨集群的现有 Pod 相关联。这些字段包括：

- maxSkew 描述这些 Pod 可能被均匀分布的程度。你必须指定此字段且该数值必须大于零。 例如，如果你有 3 个可用区，分别有 2、2 和 1 个匹配的 Pod，则 MaxSkew 设为 1， 且全局最小值为 1。

- minDomains 表示符合条件的域的最小数量。此字段是可选的。域是拓扑的一个特定实例。 符合条件的域是其节点与节点选择器匹配的域。

- topologyKey 是节点标签的键。参考节点 Pod 亲和性。

- whenUnsatisfiable 指示如果 Pod 不满足分布约束时如何处理：DoNotSchedule/ScheduleAnyway 。

- labelSelector 用于查找匹配的 Pod

- matchLabelKeys 是一个 Pod 标签键的列表，这些键值标签与 labelSelector 进行逻辑与运算。matchLabelKeys 和 labelSelector 中禁止存在相同的键。 未设置 labelSelector 时无法设置 matchLabelKeys。Pod 标签中不存在的键将被忽略。 null 或空列表意味着仅与 labelSelector 匹配。

- nodeAffinityPolicy 表示我们在计算 Pod 拓扑分布偏差时将如何处理 Pod 的 nodeAffinity/nodeSelector。 选项为：

  Honor：只有与 nodeAffinity/nodeSelector 匹配的节点才会包括到计算中。
  
  Ignore：nodeAffinity/nodeSelector 被忽略。所有节点均包括到计算中。
  
  如果此值为 nil，此行为等同于 Honor 策略。

- nodeTaintsPolicy 表示我们在计算 Pod 拓扑分布偏差时将如何处理节点污点。选项为：

  Honor：包括不带污点的节点以及污点被新 Pod 所容忍的节点。

  Ignore：节点污点被忽略。包括所有节点。

  如果此值为 null，此行为等同于 Ignore 策略。

**当 Pod 定义了不止一个 topologySpreadConstraint，这些约束之间是逻辑与的关系。 kube-scheduler 会为新的 Pod 寻找一个能够满足所有约束的节点。**

<font color=red>拓扑分布约束依赖于节点标签来标识每个节点所在的拓扑域。</font>

#### 已知局限性

- 当 Pod 被移除时，无法保证约束仍被满足。例如，缩减某 Deployment 的规模时，Pod 的分布可能不再均衡。

- 你可以使用 Descheduler 来重新实现 Pod 分布的均衡。

- 具有污点的节点上匹配的 Pod 也会被统计。

- 该调度器不会预先知道集群拥有的所有可用区和其他拓扑域。 拓扑域由集群中存在的节点确定。在自动扩缩的集群中，如果一个节点池（或节点组）的节点数量缩减为零， 而用户正期望其扩容时，可能会导致调度出现问题。

## 实际用例

### Node 亲和性

以一个三节点的集群为例。我们使用该集群运行一个内存缓存（例如 Redis）的应用程序。 在此例中，还假设内存缓存应用程序需要调度到专用的内存型机器上，且希望机器的磁盘 IO 延迟应尽可能低。 我们可以使用节点亲和性来尽可能地将该缓存（Redis）调度到最合适它的节点上。

在下面的 Redis 缓存 Deployment 示例中， nodeAffinity 规则告诉调度器强制将该服务的 Pod 调度到带有标签 instance-type ，且标签值值为 memory 的节点上。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-cache
spec:
  selector:
    matchLabels:
      app: store
  replicas: 3
  template:
    metadata:
      labels:
        app: store
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: instance-type
                operator: In
                values:
                - memory
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 1
            preference:
              matchExpressions:
              - key: disktype
                operator: In
                values:
                - ssd
      containers:
      - name: redis-server
        image: redis:3.2-alpine
```

创建前面两个 Deployment 可能会产生如下的集群布局。

{{< image src="Node亲和性实际用例.png" caption="ode亲和性实际用例" >}}

- 当 node1 上面资源不足或其他原因导致 cache-3 没办法往 node1 上调度时，剩余的 Pod 会被调度到 node2 上。

- Pod 不会被调度到 node3 上，因为它不满第一个硬亲和性规则。

### Pod 亲和性

以一个三节点的集群为例。你使用该集群运行一个带有内存缓存（例如 Redis）的 Web 应用程序。 在此例中，还假设 Web 应用程序和内存缓存之间的延迟应尽可能低。 你可以使用 Pod 间的亲和性和反亲和性来尽可能地将该 Web 服务器与缓存（Redis）调度到一起。

在下面的 Redis 缓存 Deployment 示例中，副本上设置了标签 app=store。 podAntiAffinity 规则告诉调度器避免将多个带有 app=store 标签的副本部署到同一节点上。 因此，每个独立节点上会创建一个缓存实例。

```yaml
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
```

下例的 Deployment 为 Web 服务器创建带有标签 app=web-store 的副本。 Pod 亲和性规则告诉调度器将每个副本放到存在标签为 app=store 的 Pod 的节点上。 Pod 反亲和性规则告诉调度器决不要在单个节点上放置多个 app=web-store 服务器。

```yaml
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 会产生如下的集群布局，每个 Web 服务器与一个缓存实例并置， 并分别运行在三个独立的节点上。

{{< image src="Pod亲和性实际用例.png" caption="Pod亲和性实际用例" >}}

##### 1.operator

- operator 字段使用 In、NotIn、Exists、 DoesNotExist、Gt 和 Lt 之一作为操作符。

##### 2.topologyKey 

原则上，topologyKey 可以是任何合法的标签键。出于性能和安全原因，topologyKey 有一些限制：

- 在 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution 中，topologyKey 不允许为空。

- 对于 requiredDuringSchedulingIgnoredDuringExecution 要求的 Pod 反亲和性， 准入控制器 LimitPodHardAntiAffinityTopology 要求 topologyKey 只能是 kubernetes.io/hostname。如果你希望使用其他定制拓扑逻辑， 你可以更改准入控制器或者禁用之。

- 如果指定的 topologyKey 不存在，在 requiredDuringSchedulingIgnoredDuringExecution 规则中，Pod 将无法被调度。在 preferredDuringSchedulingIgnoredDuringExecution 规则中，Pod 将不会按照预期的结果调度。

##### 3.labelSelector

- 在亲和性场景中，如果 labelSelector 使用该服务 Pod 自身的 label ，如果该服务的 Pod 是在集群内第一次被创建（此时集群内没有节点拥有该 Pod 的标签信息），为防止 Pod 永远处于无法被创建的状态，集群会允许 Pod 通过自身的亲和性检查。

### Pod 拓扑分布约束

假设我们拥有一个 5 节点集群，其中标记为 app: store 的 3 个 Pod 分别位于 node1、node2 和 node3 中：

{{< image src="Pod拓扑分布实际用例-1.png" caption="Pod拓扑分布实际用例-1" >}}

如果你希望新来的 Pod 均匀分布在现有的 zoneA 和 zoneB 可用区域中，则可以按如下设置其清单：

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-cache
spec:
  selector:
    matchLabels:
      app: store
  replicas: 3
  template:
    metadata:
      labels:
        app: store
    spec:
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: store
      - maxSkew: 1
        topologyKey: kubernetes.io/hostname
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: store
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: topology.kubernetes.io/zone
                operator: NotIn
                values:
                - zoneC
      containers:
      - name: redis-server
        image: redis:3.2-alpine
```

从此清单看，Pod 将均匀分布于存在标签键值对为 topology.kubernetes.io/zone: <any value> 的节点 （没有 zone 标签的节点将被跳过）。如果调度器找不到一种方式来满足此约束， 则 whenUnsatisfiable: DoNotSchedule 字段告诉该调度器将新来的 Pod 保持在 pending 状态。

- 如果该调度器将这个新来的 Pod 放到可用区 A，则 Pod 的分布将成为 [3, 1]。 这意味着实际偏差是 2（计算公式为 3 - 1），这违反了 maxSkew: 1 的约定。 

- 如果该调度器将这个新来的 Pod 放到可用区 C，则违反了 nodeAffinity 中的规则。

- 为了满足这个示例的约束和上下文，新来的 Pod 只能放到可用区 B 中的其中一个节点上。

{{< image src="Pod拓扑分布实际用例-2.png" caption="Pod拓扑分布实际用例-2" >}}

- 如果该调度器将这个新来的 Pod 放到可用区 B 的 node3 上，则可用区 B 上节点 Pod 的分布将成为 [2, 0]。 这意味着实际偏差是 2（计算公式为 2 - 0），这违反了第二条拓扑约束 topologyKey: kubernetes.io/hostname 的 maxSkew: 1 的约定。

最后实际分布是这样的：

{{< image src="Pod拓扑分布实际用例-3.png" caption="Pod拓扑分布实际用例-3" >}}

#### 集群级别的默认约束

这一部分需要修改到 kube-scheduler 的配置，实际意义不大，因为大部分生产环境的集群都是托管的，很少说会去改配置的。有兴趣的朋友可以自行到 kubernetes 官网查看。

## 总结

1. nodeSelector 只能选择拥有所有指定标签的节点。 节点亲和性、反亲和性为我们提供对选择逻辑的更强控制能力。

2. pod 亲和性用于控制 Pod 彼此间的调度方式（更密集或更分散）。

3. 要实现更细粒度的控制，我们可以设置拓扑分布约束来将 Pod 分布到不同的拓扑域下，从而实现高可用性或节省成本。 这也有助于工作负载的滚动更新和平稳地扩展副本规模。

4. nodeName 是比亲和性或者 nodeSelector 更为直接的形式。nodeName 是 Pod 规约中的一个字段。如果 nodeName 字段不为空，调度器会忽略该 Pod， 而指定节点上的 kubelet 会尝试将 Pod 放到该节点上。 使用 nodeName 规则的优先级会高于使用 nodeSelector 或亲和性与非亲和性的规则。
