Kubernetes Namespace 和 Node 做亲和部署

背景

在共享集群(多租户共享底层硬件资源)中, 遇到特殊租户需要独享特定资源(比如:独占GPU资源、独占Node节点等)时候,可以对namespace下的Pod请求统一做亲和性操作

Kubernetes 具体使用和实现

kubernetes中,最最核心部署通通过: Pod节点亲和性Affinity 和 Pod污点Taint和容忍度Toleration 组合方式实现 Pod真实部署在具体那个Node上

节点亲和性Affinity

亲和性主要分为两类:nodeAffinitypodAffinity

nodeSelector

Labelkubernetes中一个非常重要的概念,用户可以非常灵活的利用 label 来管理集群中的资源,比如最常见的一个就是 service 通过匹配 label 去选择 POD 的。而 POD 的调度也可以根据节点的 label 进行特定的部署。

$ kubectl label nodes docker-desktop key=value
node/docker-desktop labeled

$ kubectl get nodes --show-labels             
NAME             STATUS   ROLES                  AGE    VERSION   LABELS
docker-desktop   Ready    control-plane,master   109d   v1.22.5   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,key=value,kubernetes.io/arch=amd64,kubernetes.io/hostname=docker-desktop,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=,node-role.kubernetes.io/master=,node.kubernetes.io/exclude-from-external-load-balancers=

可以对Pod进行nodeSelector亲和性部署

apiVersion: v1
kind: Pod
metadata:
  labels:
    app: busybox
  name: test
spec:
  containers:
  - command:
    - sleep
    - "1d"
    image: busybox
    name: test
  nodeSelector:  
    # 通过nodeSelector强制进行亲和部署,对于有 key: value label的node没有资源等限制,Pod会直接失败
    key: value

nodeSelector的方式比较直观,但是还够灵活,控制粒度偏大

nodeAffinity

nodeAffinity就是节点亲和性, 可以进行一些简单的逻辑组合了,不只是简单的相等匹配. nodeAffinity调度可以分成软策略硬策略两种方式.

软策略就是如果你没有满足调度要求的节点的话,POD 就会忽略这条规则,继续完成调度过程,说白了就是满足条件最好了,没有的话也无所谓了的策略; 硬策略就比较强硬了,如果没有满足条件的节点的话,就不断重试直到满足条件为止,简单说就是你必须满足我的要求,不然我就不干的策略。

apiVersion: v1
kind: Pod
metadata:
  name: node-affinity
  labels:
    app: node-affinity-test
spec:
  containers:
  - name: node-affinity
    image: nginx
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution: # 硬策略
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/os
            operator: In  #可以有 In、NotIn、Gt、Lt、Exists、DoesNotExist
            values:
            - Linux
        - matchExpressions:
          - key: kubernetes.io/os
            operator: In
            values:
            - Window
      preferredDuringSchedulingIgnoredDuringExecution:  # 软策略
      - weight: 1
        preference:
          matchExpressions:
          - key: key
            operator: In
            values:
            - value
            - value1

如果nodeSelectorTerms下面有多个选项的话,满足任何一个条件就可以了;如果matchExpressions有多个选项的话,则必须同时满足这些条件才能正常调度 POD

污点Taint和容忍度Toleration

Taint在一类服务器上打上污点,让不能容忍这个污点的Pod不能部署在打了污点的服务器上。(锁) Toleration是让Pod容忍节点上配置的污点,可以让一些需要特殊配置的Pod能够调用到具有污点和特殊配置的节点上。(钥匙)

污点Taint

Taint EffectNodeLabel 包括以下3种:

NoSchedule:禁止调度到该节点,已经在该节点上的Pod不受影响

NoExecute:禁止调度到该节点,如果不符合这个污点,会立马被驱逐(或在一段时间后驱逐,默认300s,可单独设置驱逐时间)

PreferNoSchedule:尽量避免将Pod调度到指定的节点上,如果没有更合适的节点,可以部署到该节点

$ kubectl get node
NAME             STATUS   ROLES                  AGE    VERSION
docker-desktop   Ready    control-plane,master   109d   v1.22.5

$ kubectl taint nodes docker-desktop key=value:PreferNoSchedule
node/docker-desktop tainted

$ kubectl get node docker-desktop -o go-template --template {{.spec.taints}}
[map[effect:PreferNoSchedule key:key value:value]]  

Node打了基于key=valuePreferNoSchedule类型污点

容忍度Toleration

容忍度Toleration 就是Pod对 Node Taints的选择关系

  1. 完全匹配
tolerations:
- key: "keu"
  operator: "Equal"
  value: "value"
  effect: "PreferNoSchedule"
  1. 不完全匹配
tolerations:
- key: "key"
  operator: "Exists"
  effect: "PreferNoSchedule"
  tolerationSeconds: 600 # 节点不健康,600秒后再驱逐
  • node.kubernetes.io/not-ready:节点未准备好,相当于节点状态Ready的值为False
  • node.kubernetes.io/unreachable:Node Controller访问不到节点,相当于节点状态Ready的值为Unknown
  • node.kubernetes.io/out-of-disk:节点磁盘耗尽
  • node.kubernetes.io/memory-pressure:节点存在内存压力
  • node.kubernetes.io/disk-pressure:节点存在磁盘压力
  • node.kubernetes.io/network-unavailable:节点网络不可达

Namespace 和 Node 做亲和部署设计

首先,对于特定的Namespace 通过 Label, 通过namespace-node-affinity: enabled进行开启。

apiVersion: v1
kind: Namespace
metadata:
  name: test-demo
  labels:
    # namespace级生效,对本namespace开启
    namespace-node-affinity: enabled

通过configmap对namespace下的亲和性最全局配置

apiVersion: v1
kind: ConfigMap
metadata:
  name: namespace-node-affinity-webhook
  namespace: kube-system  # 这个namespace是 webhook所在的namespace
data:
  # namespace 名称: test-demo、default
  setting-namespace-name: |  
    nodeSelectorTerms:  #做 nodeAffinity 下的 硬策略
      - matchExpressions:
        - key: "kubernetes.io/os"
          operator: In
          values:
          - "linux"
    tolerations:  #做 tolerations 策略
      - key: "example-key"
        operator: "Exists"
        effect: "NoSchedule"
    excludedLabels:  #如果匹配其中全部key-value 可跳过强绑定
      namespace-node-affinity.cmss.com: disabled

对于PodDeployment, 可以通过统一标签,跳过以上白名单,namespace-node-affinity.cmss.com: disabled

# Pod
apiVersion: v1
kind: Pod
metadata:
  name: xxxx
  annotations:
    namespace-node-affinity.cmss.com: disabled
...


# Deployment
...
 spec:
  template:
    metadata:
      #添加注解
      annotations:
        namespace-node-affinity.cmss.com: disabled
...

逻辑: 通过webhook监听PodCreate事件,如果监听到事件后,先判断Pod中是否有亲和性策略,并且判断namespace的 node亲和性是否开启, 如果都开启,将亲和性指令merge一起

//...
func buildNodeSelectorTermsPath(podSpec coreV1.PodSpec) PatchPath {
	var path PatchPath
	//pod没有亲和性设置
	if podSpec.Affinity == nil {
		path = CreateAffinity
	} else if podSpec.Affinity != nil && podSpec.Affinity.NodeAffinity == nil { //pod有亲和性设置,但是没有NodeAffinity
		path = CreateNodeAffinity
	} else if podSpec.Affinity.NodeAffinity != nil && podSpec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution == nil { //pod有亲和性设置,且有NodeAffinity,但是没有强策略
		path = AddRequiredDuringScheduling
	} else {
		path = AddNodeSelectorTerms
	}

	return path
}
//...
func buildTolerationsPath(podSpec coreV1.PodSpec) PatchPath {
	// pod是否有Tolerations
	if podSpec.Tolerations == nil {
		return CreateTolerations
	}
	return AddTolerations
}