K8S学习笔录 - 亲和/反亲和性调度

2,137 阅读9分钟

原文地址

Node亲和性的调度策略主要通过Affinity来实现,用于替换NodeSelector的全新调度策略。

为什么要使用亲和/反亲和性调度

nodeSelector 定向调度提供了非常简单的方法来将Pod约束到具有特定标签的节点上。

而亲和/反亲和功能极大地扩展了可以表达的约束类型

主要增加了以下功能

  1. 语言更具表现力。
  2. 可以发布软限制,而非硬性要求,即便没有期望的节点,也可以调度该Pod。
  3. 可以使用节点上的Pod的标签来约束,而非使用节点本身标签,来规定哪些Pod可以放在一起。

配置结构如下

type PodSpec struct {
    // Pod的亲和配置,通过该配置可以将节点调度到指定的环境中
    Affinity *Affinity `json:"affinity,omitempty"`
}
type Affinity struct {
    // 描述了Pod与Node之间的亲和性调度规则
    NodeAffinity *NodeAffinity `json:"nodeAffinity,omitempty"`

    // 描述了Pod与Pod之间的亲和性调度规则
    PodAffinity *PodAffinity `json:"podAffinity,omitempty"`

    // 描述了Pod与Pod之间的反亲和性规则
    PodAntiAffinity *PodAntiAffinity `json:"podAntiAffinity,omitempty"`
}

配置简介

通过配置结构体来学习一下配置项详情

节点亲和

类似于 nodeSelector ,然而可以指定硬性要求和软性要求。

配置结构如下

type NodeAffinity struct {
    // 如果调度时节点未满足Pod的亲和性要求,Pod将不会被调度到此节点上
    // 如果在Pod运行时节点突然不满足亲和要求,系统可能不会(尝试)将其从节点上驱逐
    RequiredDuringSchedulingIgnoredDuringExecution *NodeSelector `json:"requiredDuringSchedulingIgnoredDuringExecution,omitempty"`

    // 与RequiredDuringSchedulingIgnoredDuringExecution类似
    // 不同的在于即使节点与Pod的要求不满足或部分不满足,Pod也可能会被调度到节点上
    // 具体选择是根据满足的条件的权重和来计算的
    PreferredDuringSchedulingIgnoredDuringExecution []PreferredSchedulingTerm `json:"preferredDuringSchedulingIgnoredDuringExecution,omitempty"`
}
type NodeSelector struct {
    // NodeSelector的规则定义
    NodeSelectorTerms []NodeSelectorTerm `json:"nodeSelectorTerms"`
}
type PreferredSchedulingTerm struct {
    // 权重配置,范围为1-100
    Weight int32 `json:"weight"`

    // NodeSelector的规则定义
    Preference NodeSelectorTerm `json:"preference"`
}
type NodeSelectorTerm struct {
    // 匹配节点的Labels
    MatchExpressions []NodeSelectorRequirement `json:"matchExpressions,omitempty"`

    // 匹配节点的字段
    MatchFields []NodeSelectorRequirement `json:"matchFields,omitempty"`
}
type NodeSelectorRequirement struct {
    // label或者field的Key
    Key string `json:"key"`

    // 匹配关系,包括In、NotIn、Exists、DoesNotExist、Gt、Lt
    Operator NodeSelectorOperator `json:"operator"`

    // 匹配的Key所对应的Value
    // operator是In或NotIn,该值必须不为空。在使用过程中发现,该值也只能有一个值。
    // 否则会报错 ... must be only one value when `operator` is 'In' or 'NotIn' for node field selector
    // opertor是Exists或DoesNotExist,该值必须为空
    // operator是Gt或Lt,该值必须有且只能有一个值
    Values []string `json:"values,omitempty"`
}

type NodeSelectorOperator string
const (
    NodeSelectorOpIn           NodeSelectorOperator = "In"
    NodeSelectorOpNotIn        NodeSelectorOperator = "NotIn"
    NodeSelectorOpExists       NodeSelectorOperator = "Exists"
    NodeSelectorOpDoesNotExist NodeSelectorOperator = "DoesNotExist"
    NodeSelectorOpGt           NodeSelectorOperator = "Gt"
    NodeSelectorOpLt           NodeSelectorOperator = "Lt"
)

Pod亲和与反亲和

Pod间的亲和与反亲和配置可以通过已经在节点上运行的Pod来约束Pod可以调度到的节点上。

规则的格式为 如果 X 节点上已经运行了一个或多个 满足规则 Y 的pod,则这个 pod 应该(或不应该)运行在 X 节点上

// Pod亲和配置,要求与满足条件的Pod部署在同个节点上
type PodAffinity struct {
    // 如果调度时节点上运行的Pod未满足该Pod的亲和性要求,Pod将不会被调度到此节点上
    // 如果在Pod运行时节点上的Pod突然不满足亲和要求,系统可能不会(尝试)将其从节点上驱逐
    // 必须列表中所有的要求都满足才可以
    RequiredDuringSchedulingIgnoredDuringExecution []PodAffinityTerm

    // 与PreferredDuringSchedulingIgnoredDuringExecution类似
    // 不同的在于即使节点上Pod的条件与Pod的要求不满足或部分不满足,Pod也可能会被调度到节点上
    // 具体选择是根据满足的条件的权重和来计算的
    PreferredDuringSchedulingIgnoredDuringExecution []WeightedPodAffinityTerm
}

// Pod互斥配置,拒绝与满足条件的Pod部署在同一个节点上
type PodAntiAffinity struct {
    // 硬限制,必须全部满足才会达成目标,不会驱逐运行中的Pod
    RequiredDuringSchedulingIgnoredDuringExecution []PodAffinityTerm `json:"requiredDuringSchedulingIgnoredDuringExecution,omitempty"`

    // 软限制,必须全部满足才会达成目标,不会驱逐运行中的Pod
    PreferredDuringSchedulingIgnoredDuringExecution []WeightedPodAffinityTerm `json:"preferredDuringSchedulingIgnoredDuringExecution,omitempty"`
}

type PodAffinityTerm struct {
    // 标签选择器,列出Pod要满足的条件
    LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`

    // LabelSelector所作用的命名空间。空列表或者未定义则为当前Pod所在命名空间
    Namespaces []string `json:"namespaces,omitempty"`

    // 目标节点所属的拓扑域,不可为空。
    TopologyKey string `json:"topologyKey"`
}

type LabelSelector struct {
    // 要求Pod的Labels完全包含此处定义的值
    MatchLabels map[string]string `json:"matchLabels,omitempty"`

    // 与MatchLabels类似,只是匹配功能上更丰富
    MatchExpressions []LabelSelectorRequirement `json:"matchExpressions,omitempty"`
}
type LabelSelectorRequirement struct {
    // Pod定义的Label的Key
    Key string `json:"key" patchStrategy:"merge" patchMergeKey:"key"`

    // 运算方法,包括In、NotIn、Exists、DoesNotExist
    Operator LabelSelectorOperator `json:"operator"`

    // operator是In或NotIn时,values不可为空
    // operator为Exists或DoesNotExist时,values必须为空
    Values []string `json:"values,omitempty"`
}
type LabelSelectorOperator string
const (
    LabelSelectorOpIn           LabelSelectorOperator = "In"
    LabelSelectorOpNotIn        LabelSelectorOperator = "NotIn"
    LabelSelectorOpExists       LabelSelectorOperator = "Exists"
    LabelSelectorOpDoesNotExist LabelSelectorOperator = "DoesNotExist"
)

type WeightedPodAffinityTerm struct {
    // 权重,满足条件后计入分数 1-100
    Weight int32 `json:"weight"`

    // 达成目标所需要的条件
    PodAffinityTerm PodAffinityTerm `json:"podAffinityTerm"`
}

TopologyKey

Toplogy翻译为拓扑,所以可以理解为所属拓扑域的标记。

拓扑域为一个范围概念,比如一个节点、机柜、机房,或者同属于一个系列的机器,比如都是SSD的机器、都是Intel CPU的机器。总而言之是一个范围。在K8S中,它表现为节点上的一个标签。

如果设置了topologyKey为 kubernetes.io/hostname ,则表示拓扑域的范围为 kubernetes.io/hostname 范围,即CPU架构范围。 那么 kubernetes.io/hostname 对应的值不同就是不同的拓扑域。 例如此时有3个Pod, kubernetes.io/hostname 的值分别是 abc ,则这三个Pod属于3个不同的拓扑域。

注意事项

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

  1. 对于具有 Pod的亲和性要求与Pod的硬性反亲和性要求 的配置,topologyKey不允许为空。
  2. 对于具有 Pod的硬性反亲和性要求 的配置,准入控制器LimitPodHardAntiAffinityTopology被引入来限制topologyKey不为kubernetes.io/hostname。如果你想使它可用于自定义拓扑结构,你必须修改准入控制器或者禁用它。
  3. 对于具有 Pod的软性反亲和性要求 的配置,空的topologyKey被解释为“所有拓扑域”。 这里的“所有拓扑结构”限制为kubernetes.io/hostname,failure-domain.beta.kubernetes.io/zone和failure-domain.beta.kubernetes.io/region的组合。
  4. 除上述情况外,topologyKey可以是任何合法的标签键。

*硬性要求指的是requiredDuringSchedulingIgnoredDuringExecution

*软性要求指的是preferredDuringSchedulingIgnoredDuringExecution

配置示例

先定义一个Pod,配置文件affinity-nginx.yaml如下

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution: # 必须满足,硬限制
        nodeSelectorTerms:
        - matchFields:
          - key: metadata.name
            operator: NotIn
            values:
            - work-node-abc
      preferredDuringSchedulingIgnoredDuringExecution: # 尽量满足,软限制
      - weight: 20 # 权重
        preference: # 节点偏好设置
          matchExpressions:
          - key: disk
            operator: NotIn
            values:
            - ssd
      - weight: 30 # 权重
        preference: # 节点偏好设置
          matchExpressions:
          - key: disk
            operator: In
            values:
            - ssd
    podAffinity: # Pod亲和性配置,要求与type=log的Pod在一个节点
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: type
            operator: In
            values:
            - log
        topologyKey: proxy
    podAntiAffinity: # Pod反亲和性配置,拒绝与type=app的Pod在一个节点
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: type
            operator: In
            values:
            - app
        topologyKey: proxy

这个配置文件规定

  1. Pod中容器的情况
  2. Pod 只能 部署在metadata.name不为work-node-abc的节点上
  3. Pod 尽量 部署在Labels中disk不包含ssd的节点上,权重30
  4. Pod 尽量 部署在Labels中disk包含ssd的节点上,权重20
  5. Pod 只能 部署在同时满足条件 节点带有proxy标签 且 节点上有标签type值为log的Pod 的节点上
  6. Pod 不能 部署在同时满足条件 节点带有kubernetes.io/hostname标签 且 节点上有标签type值为app的Pod 的节点上

先配置节点标签信息

  1. 为A、B节点添加disk标签,一个值为ssd,一个值为hdd
  2. 为A、B节点添加proxy标签,值为nginx
  3. 为A、B节点部署 type=log 标签的Pod
  4. 为A节点部署 type=app 标签的Pod

A节点tx,B节点ks

为节点打标签

$ kubectl label node tx proxy=nginx
node/tx labeled

$ kubectl label node ks proxy=nginx
node/ks labeled

$ kubectl label node tx disk=ssd
node/tx labeled

$ kubectl label node ks disk=hdd
node/ks labeled

部署协助试验的Pod

apiVersion: v1
kind: Pod
metadata:
  name: log-aaa
  labels:
    type: log
spec:
  containers:
  - name: nginx
    image: nginx
  nodeSelector:
    kubernetes.io/hostname: tx
---
apiVersion: v1
kind: Pod
metadata:
  name: log-bbb
  labels:
    type: log
spec:
  containers:
  - name: nginx
    image: nginx
  nodeSelector:
    kubernetes.io/hostname: ks
---
apiVersion: v1
kind: Pod
metadata:
  name: app-aaa
  labels:
    type: app
spec:
  containers:
  - name: nginx
    image: nginx
  nodeSelector:
    kubernetes.io/hostname: tx

对比Pod的配置要求

  1. Pod中容器的情况 (忽略)
  2. Pod 只能 部署在metadata.name不为kube-system的节点上 (所有节点满足)
  3. Pod 尽量 部署在Labels中disk不包含ssd的节点上,权重20 (B节点20)
  4. Pod 尽量 部署在Labels中disk包含ssd的节点上,权重30 (A节点30)
  5. Pod 只能 部署在同时满足条件 节点带有proxy标签 且 节点上有标签type值为log的Pod 的节点上 (A、B节点都满足要求)
  6. Pod 不能 部署在同时满足条件 节点带有kubernetes.io/hostname标签 且 节点上有标签type值为app的Pod 的节点上(A上有type=app的Pod,B上没有)

所以会部署在节点B上

$ kubectl delete pod nginx && kubectl create -f affinity-nginx.yaml && sleep 3 && kubectl get pods -o wide
pod "nginx" deleted
pod/nginx created
NAME      READY   STATUS              RESTARTS   AGE   IP          NODE   NOMINATED NODE   READINESS GATES
app-aaa   1/1     Running             0          16m   10.46.0.1   tx     <none>           <none>
log-bbb   1/1     Running             0          16m   10.32.0.1   ks     <none>           <none>
log-aaa   1/1     Running             0          16m   10.46.0.0   tx     <none>           <none>
nginx     0/1     ContainerCreating   0          3s    <none>      ks     <none>           <none>

删除掉 podAntiAffinity Pod的反亲和性配置块,此时所有节点都满足需求,则亲和性中的 preferredDuringSchedulingIgnoredDuringExecution 配置将会起作用

此时将会部署在节点A上

$ kubectl delete pod nginx && kubectl create -f affinity-nginx.yaml && sleep 3 && kubectl get pods -o wide
pod "nginx" deleted
pod/nginx created
NAME      READY   STATUS              RESTARTS   AGE   IP          NODE   NOMINATED NODE   READINESS GATES
app-aaa   1/1     Running             0          33m   10.46.0.1   tx     <none>           <none>
log-bbb   1/1     Running             0          33m   10.32.0.1   ks     <none>           <none>
log-aaa   1/1     Running             0          33m   10.46.0.0   tx     <none>           <none>
nginx     0/1     ContainerCreating   0          3s    <none>      tx     <none>           <none>

修改权重值,调大B的权重到50(将配置中低权重20改为50),则会部署在节点B上

$ kubectl delete pod nginx && kubectl create -f affinity-nginx.yaml && sleep 3 && kubectl get pods -o wide
pod "nginx" deleted
pod/nginx created
NAME      READY   STATUS              RESTARTS   AGE   IP          NODE   NOMINATED NODE   READINESS GATES
app-aaa   1/1     Running             0          35m   10.46.0.1   tx     <none>           <none>
log-bbb   1/1     Running             0          35m   10.32.0.1   ks     <none>           <none>
log-aaa   1/1     Running             0          35m   10.46.0.0   tx     <none>           <none>
nginx     0/1     ContainerCreating   0          3s    <none>      ks     <none>           <none>

最后删除所有标签,已经运行的Pod并不会受到任何影响,而重新加入时则会发现条件已经不满足了

$ kubectl label node ks disk-
node/ks labeled

$ kubectl label node tx disk-
node/tx labeled

$ kubectl label node ks proxy-
node/ks labeled

$ kubectl label node tx proxy-
node/tx labeled

$ kubectl get pods
NAME      READY   STATUS    RESTARTS   AGE
app-aaa   1/1     Running   0          36m
log-bbb   1/1     Running   0          36m
log-aaa   1/1     Running   0          36m
nginx     1/1     Running   0          72s

$ kubectl delete pod nginx && kubectl create -f a.yaml && sleep 3 && kubectl get pods -o wide
pod "nginx" deleted
pod/nginx created
NAME      READY   STATUS    RESTARTS   AGE   IP          NODE     NOMINATED NODE   READINESS GATES
app-aaa   1/1     Running   0          66m   10.46.0.1   tx       <none>           <none>
log-bbb   1/1     Running   0          66m   10.32.0.1   ks       <none>           <none>
log-aaa   1/1     Running   0          66m   10.46.0.0   tx       <none>           <none>
nginx     0/1     Pending   0          3s    <none>      <none>   <none>           <none>