通常情况下在Kubernetes 集群中部署一个Pod, 默认调度器将会自动进行合理的调度(例如, 将Pod分散到节点上, 根据节点上的资源情况进行分配), 但是有时候我们需要更加细粒度的控制pod的调度. 比如一组pod需要最终调度到拥有SSD/GPU的硬盘的机器上,或者将两个不同的服务(服务间直接通信比较频繁)的pod 调度到同样的节点上 (比如gitlab.这里就需要 Kubernetes里面的亲和性
来解决,亲和性分为2类: nodeAffinity
和 podAffinity
.
nodeSelector
最简单的推荐形式是使用nodeSelector
来满足我们的需求. label
是 kubernetes
中一个非常重要的一个概念. 使用label
可以非常方便的管理集群中资源. 例如常见的使用label
对node节点打标签方面对于不同计算资源的pod进行调度。还有一个是常见的service 就是通过匹配label 进行选择pod。pod的调度也可以根据节点标签进行选择部署. nodeSelector
是 PodSpec 的一个字段。 它包含键值对的映射。为了使 pod 可以在某个节点上运行,该节点的标签中必须包含这里的每个键值对(它也可以具有其他标签)。最常见的用法的是一对键值对。
TKE中查看和添加标签
在控制台中查看标签: 节点管理--> 节点--> 详情
在控制台中添加/修改/删除标签: 节点管理--> 节点--> 编辑标签
备注: TKE node节点上默认标签不允许修改
在命令行中查看/添加/删除标签
通过 --show-labels 可以查看当前 nodes 的 labels
代码语言:txt复制 ~ kubectl get nodes --show-labels 日 9/ 6 15:49:15 2020
NAME STATUS ROLES AGE VERSION LABELS
10.0.0.10 Ready <none> 82d v1.16.3-tke.10 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=QCLOUD,beta.kubernetes.io/os=linux,cloud.tencent.com/node-instance-id=ins-ea2t8uz1,diqu=bj,failure-domain.beta.kubernetes.io/region=bj,failure-domain.beta.kubernetes.io/zone=800001,group=devops,kubernetes.io/arch=amd64,kubernetes.io/hostname=10.0.0.10,kubernetes.io/os=linux
我们可以通过 kubectl label node 命令给指定 node 添加 labels
代码语言:txt复制 ~ kubectl label nodes 10.0.0.10 disk=ssd 1159ms 日 9/ 6 15:51:24 2020
node/10.0.0.10 labeled
~ kubectl get nodes 10.0.0.10 --show-labels 383ms 日 9/ 6 15:51:36 2020
NAME STATUS ROLES AGE VERSION LABELS
10.0.0.10 Ready <none> 82d v1.16.3-tke.10 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=QCLOUD,beta.kubernetes.io/os=linux,cloud.tencent.com/node-instance-id=ins-ea2t8uz1,diqu=bj,disk=ssd,failure-domain.beta.kubernetes.io/region=bj,failure-domain.beta.kubernetes.io/zone=800001,group=devops,kubernetes.io/arch=amd64,kubernetes.io/hostname=10.0.0.10,kubernetes.io/os=linux
实践案例--测试 pod 并指定 nodeSelector 选项绑定节点
使用命令行进行创建:
创建 test-nodeselector.yaml 文件
代码语言:txt复制apiVersion: apps/v1beta2
kind: Deployment
metadata:
labels:
k8s-app: test-nodeselector
qcloud-app: test-nodeselector
name: test-nodeselector
spec:
progressDeadlineSeconds: 600
replicas: 1
selector:
matchLabels:
k8s-app: test-nodeselector
qcloud-app: test-nodeselector
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
k8s-app: test-nodeselector
qcloud-app: test-nodeselector
spec:
containers:
- image: nginx
imagePullPolicy: Always
name: test-nodeselector
resources:
limits:
cpu: 500m
memory: 1Gi
requests:
cpu: 250m
memory: 256Mi
securityContext:
privileged: false
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
nodeSelector:
disk: ssd
imagePullSecrets:
- name: qcloudregistrykey
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
kubectl apply -f test-nodeselector.yaml
这个例子中:
代码语言:txt复制 nodeSelector:
disk: ssd
这个例子就是告诉 kubernetes 调度的时候把 pod 放到有 SSD 磁盘的机器上。除了自己定义的 label 之外,kubernetes 还会自动给集群中的节点添加一些 label,比如:
- kubernetes.io/hostname:节点的 hostname 名称
- beta.kubernetes.io/os: 节点安装的操作系统
- beta.kubernetes.io/arch:节点的架构类型
- …… 不同版本添加的 label 会有不同,这些 label 和手动添加的没有区别,可以通过 --show-labels 查看,也能够用在 nodeSelector 中。
nodeSelector 的方式比较简单直观,但是不够灵活。我们推荐使用下面讲到的 Node Affinity ,请关注每个版本的 release note。
Node Affinity
nodeAffinity
节点亲和性, 与之相对应的是Anti-Affinity
, 这个方式比nodeSelector
更加灵活, 有以下优势:
- 匹配更多的逻辑组合(不仅仅是"完全相等")
- 调度分为硬策略和软策略
硬策略
requiredDuringSchedulingIgnoredDuringExecution
: 必须满足的条件,如果没有满足条件的节点, 就不断重试直到满足条件为止. 软策略preferredDuringSchedulingIgnoredDuringExecution
: 调度器尝试执行如果没有找到满足要求的节点的话,Pod就会忽略这条规则, 继续完成调度,简单来说就是能满足条件最好, 没有满足就忽略,继续往下执行.gnoredDuringExecution
部分意味着,类似于 nodeSelector 的工作原理,如果节点的标签在运行时发生变更,从而不再满足 pod 上的亲和规则,那么 pod 将仍然继续在该节点上运行。与之对应的是requiredDuringSchedulingRequiredDuringExecution. 如果运行的pod所在的节点不再满足条件, kubernetes 会把pod从该节点删除, 重新选择符合要求的节点.
实践案例 -- nodeAffinity
例如: 我们部署一个pod 要求调度到满足以下条件
必须满足以下条件(标签):
- disk=ssd
- device=gpu
尽量满足的条件(标签):
- zone=bj
Node节点 标签如下:
代码语言:txt复制NAME STATUS ROLES AGE VERSION LABELS
10.0.0.10 Ready <none> 83d v1.16.3-tke.10 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=QCLOUD,beta.kubernetes.io/os=linux,cloud.tencent.com/node-instance-id=ins-ea2t8uz1,diqu=bj,disk=ssd,failure-domain.beta.kubernetes.io/region=bj,failure-domain.beta.kubernetes.io/zone=800001,group=devops,kubernetes.io/arch=amd64,kubernetes.io/hostname=10.0.0.10,kubernetes.io/os=linux
10.0.0.14 Ready master 83d v1.16.3-tke.8 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=QCLOUD,beta.kubernetes.io/os=linux,cloud.tencent.com/node-instance-id=ins-ep4ulpc5,failure-domain.beta.kubernetes.io/region=bj,failure-domain.beta.kubernetes.io/zone=800001,kubernetes.io/arch=amd64,kubernetes.io/hostname=10.0.0.14,kubernetes.io/node-role-etcd=true,kubernetes.io/os=linux,node-role.kubernetes.io/master=true
10.0.0.30 Ready <none> 31d v1.16.3-tke.10 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=QCLOUD,beta.kubernetes.io/os=linux,cloud.tencent.com/node-instance-id=ins-9o9sx4jz,diqu=sh,failure-domain.beta.kubernetes.io/region=bj,failure-domain.beta.kubernetes.io/zone=800001,gitlab=dev,group=devops,kubernetes.io/arch=amd64,kubernetes.io/hostname=10.0.0.30,kubernetes.io/os=linux
10.0.0.31 Ready <none> 28d v1.16.3-tke.10 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=QCLOUD,beta.kubernetes.io/os=linux,cloud.tencent.com/node-instance-id=ins-3jd96e1x,device=gpu,failure-domain.beta.kubernetes.io/region=bj,failure-domain.beta.kubernetes.io/zone=800001,kubernetes.io/arch=amd64,kubernetes.io/hostname=10.0.0.31,kubernetes.io/os=linux,nvidia-device-enable=enable
10.0.0.34 Ready <none> 28d v1.16.3-tke.10 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=QCLOUD,beta.kubernetes.io/os=linux,cloud.tencent.com/node-instance-id=ins-hodl5idj,device=gpu,failure-domain.beta.kubernetes.io/region=bj,failure-domain.beta.kubernetes.io/zone=800001,kubernetes.io/arch=amd64,kubernetes.io/hostname=10.0.0.34,kubernetes.io/os=linux,nvidia-device-enable=enable
10.0.0.4 Ready master 83d v1.16.3-tke.8 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=QCLOUD,beta.kubernetes.io/os=linux,cloud.tencent.com/node-instance-id=ins-jngdgb43,failure-domain.beta.kubernetes.io/region=bj,failure-domain.beta.kubernetes.io/zone=800001,kubernetes.io/arch=amd64,kubernetes.io/hostname=10.0.0.4,kubernetes.io/node-role-etcd=true,kubernetes.io/os=linux,node-role.kubernetes.io/master=true
10.0.0.45 Ready <none> 28d v1.16.3-tke.10 app=public,beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=QCLOUD,beta.kubernetes.io/os=linux,cloud.tencent.com/node-instance-id=ins-diha6wb3,failure-domain.beta.kubernetes.io/region=bj,failure-domain.beta.kubernetes.io/zone=800001,kubernetes.io/arch=amd64,kubernetes.io/hostname=10.0.0.45,kubernetes.io/os=linux
10.0.0.8 Ready master 83d v1.16.3-tke.8 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=QCLOUD,beta.kubernetes.io/os=linux,cloud.tencent.com/node-instance-id=ins-lasqw15t,failure-domain.beta.kubernetes.io/region=bj,failure-domain.beta.kubernetes.io/zone=800001,kubernetes.io/arch=amd64,kubernetes.io/hostname=10.0.0.8,kubernetes.io/node-role-etcd=true,kubernetes.io/os=linux,node-role.kubernetes.io/master=true
10.0.0.9 Ready <none> 5d1h v1.16.3-tke.10 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=QCLOUD,beta.kubernetes.io/os=linux,cloud.tencent.com/node-instance-id=ins-kr44unor,device=gpu,disk=ssd,failure-domain.beta.kubernetes.io/region=bj,failure-domain.beta.kubernetes.io/zone=800001,kubernetes.io/arch=amd64,kubernetes.io/hostname=10.0.0.9,kubernetes.io/os=linux,zone=bj
TKE控制台创建:
部署之后pod按照亲和性规则被调度到了10.0.0.9
这个node节点上,符合预期.
在命令行中手动创建:
创建test-nodeaffinity.yaml 文件
代码语言:txt复制apiVersion: apps/v1beta2
kind: Deployment
metadata:
generation: 1
labels:
k8s-app: test-nodeaffinity
qcloud-app: test-nodeaffinity
name: test-nodeaffinity
namespace: default
spec:
progressDeadlineSeconds: 600
replicas: 2
revisionHistoryLimit: 10
selector:
matchLabels:
k8s-app: test-nodeaffinity
qcloud-app: test-nodeaffinity
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
template:
metadata:
labels:
k8s-app: test-nodeaffinity
qcloud-app: test-nodeaffinity
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- preference:
matchExpressions:
- key: zone
operator: In
values:
- bj
weight: 1
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disk
operator: In
values:
- ssd
- key: device
operator: In
values:
- gpu
containers:
- image: nginx
imagePullPolicy: Always
name: test-nodeaffinity
resources:
limits:
cpu: 500m
memory: 1Gi
requests:
cpu: 250m
memory: 256Mi
securityContext:
部署: kubectl apply -f test-nodeaffinity.yaml
需要注意的是
nodeSelectorTerms
下的多个matchExpressions
是OR
的关系,就是说其中一个满足的话pod就可以调度在该节点, key 之间的关系是AND
必须同时满足 如下图所示(1和2 之间是OR, A和B之间是AND)
这里的匹配逻辑是 label 的值在某个列表中,可选的操作符有:
- In:label 的值在某个列表中
- NotIn:label 的值不在某个列表中
- Exists:某个 label 存在
- DoesNotExist: 某个 label 不存在
- Gt:label 的值大于某个值(字符串比较)
- Lt:label 的值小于某个值(字符串比较)
preferredDuringSchedulingIgnoredDuringExecution 中的 weight 字段值的范围是 1-100。对于每个符合所有调度要求(资源请求,RequiredDuringScheduling 亲和表达式等)的节点,调度器将遍历该字段的元素来计算总和,并且如果节点匹配对应的MatchExpressions,则添加“权重”到总和。然后将这个评分与该节点的其他优先级函数的评分进行组合。总分最高的节点是最优选的。
Pod Affinity
上面的方式是让pod灵活的去选择调度到哪个Node节点上; 但有些场景希望调度的时候需要考虑pod之间的关系,
不仅仅只pod-node的关系.比如服务A和服务B交互频繁, 我们希望尽量调度到同一个机房,地域. 相对应的例如: 服务c和服务d, 尽力互斥, 不让c和d在同一个主机或者地域.
和nodeAffinity
一样, podAffinity
目前有两种类型的pod亲和和反亲和:
- requiredDuringSchedulingIgnoredDuringExecution 亲和的一个示例是“将服务 A 和服务 B 的 pod 放置在同一区域,因为它们之间进行大量交流”
- preferredDuringSchedulingIgnoredDuringExecution 反亲和的示例将是“将此服务的 pod 跨区域分布
Pod 间亲和通过 PodSpec 中 affinity 字段下的 podAffinity 字段进行指定。而 pod 间反亲和通过 PodSpec 中 affinity 字段下的 podAntiAffinity 字段进行指定。
Pod 亲和实践
场景 1:
服务A和服务B, B的标签(db=mysql),期望得调度规则服务A被调度到的节点至少已经运行一个标签为db=mysql的pod 并且该节点拥有failure-domain.beta.kubernetes.io/zone
key的标签.
服务B(test-podaffinity-a.yaml):
代码语言:txt复制apiVersion: apps/v1beta2
kind: Deployment
metadata:
labels:
db: mysql
k8s-app: test-podaffinity-a
qcloud-app: test-podaffinity-a
name: test-podaffinity-a
namespace: default
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
db: mysql
k8s-app: test-podaffinity-a
qcloud-app: test-podaffinity-a
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
db: mysql
k8s-app: test-podaffinity-a
qcloud-app: test-podaffinity-a
spec:
containers:
- image: nginx
imagePullPolicy: Always
name: test-podaffinity-a
resources:
limits:
cpu: 500m
memory: 1Gi
requests:
cpu: 250m
memory: 256Mi
securityContext:
privileged: false
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
imagePullSecrets:
- name: qcloudregistrykey
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
部署: kubectl apply -f test-podaffinity-a.yaml
,
~/d/k/tests kubectl get po test-podaffinity-a-6db995f99b-zlsqt -o wide 1037ms 二 9/ 8 05:43:02 2020
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
test-podaffinity-a-6db995f99b-zlsqt 1/1 Running 0 134m 172.16.1.172 10.0.0.34 <none> <none>
服务A(with-pod-affinity.yaml):
代码语言:txt复制apiVersion: v1
kind: Pod
metadata:
name: with-pod-affinity
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: db
operator: In
values:
- mysql
topologyKey: failure-domain.beta.kubernetes.io/zone
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S2
topologyKey: failure-domain.beta.kubernetes.io/zone
containers:
- name: with-pod-affinity
image: nginx:latest
部署: kubectl apply -f with-pod-affinity.yaml
查看事件日志: kubectl get events --sort-by=.metadata.creationTimestamp -w
LAST SEEN TYPE REASON OBJECT MESSAGE
<unknown> Normal Scheduled pod/with-pod-affinity Successfully assigned default/with-pod-affinity to 10.0.0.34
12m Normal Pulling pod/with-pod-affinity Pulling image "nginx:latest"
12m Normal Pulled pod/with-pod-affinity Successfully pulled image "nginx:latest"
12m Normal Created pod/with-pod-affinity Created container with-pod-affinity
12m Normal Started pod/with-pod-affinity Started container with-pod-affinity
从调度结果上看符合预期
在这个 pod 的 affinity 配置定义了一条 pod 亲和规则和一条 pod 反亲和规则。在此示例中,podAffinity 配置为 requiredDuringSchedulingIgnoredDuringExecution,然而 podAntiAffinity 配置为 preferredDuringSchedulingIgnoredDuringExecution。pod 亲和规则表示,仅当节点和至少一个已运行且有key为“db”且值为“mysql”的标签的 pod 处于同一区域时,才可以将该 pod 调度到节点上。(更确切的说,如果节点 N 具有带有key failure-domain.beta.kubernetes.io/zone
和某个值 V 的标签,则 pod 有资格在节点 N 上运行,以便集群中至少有一个节点具有key failure-domain.beta.kubernetes.io/zone
和值为 V 的节点正在运行具有key “db”和值“mysql”的标签的 pod。)pod 反亲和规则表示,如果节点已经运行了一个具有key “security”和值“S2”的标签的 pod,则该 pod 不希望将其调度到该节点上。(如果 topologyKey 为 failure-domain.beta.kubernetes.io/zone,则意味着当节点和具有key “security”和值“S2”的标签的 pod 处于相同的区域,pod 不能被调度到该节点上。)
Pod 亲和与反亲和的合法操作符有 In,NotIn,Exists,DoesNotExist
由于性能和安全的原因,topologyKey 有以下限制:
- 对于亲和与 requiredDuringSchedulingIgnoredDuringExecution 要求的 pod 反亲和,topologyKey 不允许为空。
- 对于 requiredDuringSchedulingIgnoredDuringExecution 要求的 pod 反亲和,准入控制器 LimitPodHardAntiAffinityTopology 被引入来限制 topologyKey 不为 kubernetes.io/hostname。如果你想使它可用于自定义拓扑结构,你必须修改准入控制器或者禁用它。
- 对于 preferredDuringSchedulingIgnoredDuringExecution 要求的 pod 反亲和,空的 topologyKey 被解释为“所有拓扑结构”(这里的“所有拓扑结构”限制为 kubernetes.io/hostname,failure-domain.beta.kubernetes.io/zone 和 failure-domain.beta.kubernetes.io/region 的组合)
- 除上述情况外,topologyKey 可以是任何合法的标签键.
场景 2:
Pod 间亲和和反亲和结合Replicasets, StatefulSets,Deployments使用.
例如 一个web服务使用了redis(3副本)作为缓存, 希望web服务尽可能与redis调度到同一节点.
下面是一个简单 redis deployment 的 yaml 代码段,它有三个副本和选择器标签 app=store。Deployment 配置了 PodAntiAffinity,用来确保调度器不会将副本调度到单个节点上。
代码语言:txt复制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 服务器副本不会调度到单个节点上。
代码语言:txt复制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:latest
node1 | node2 | node3 |
---|---|---|
webserver-1 | webserver-2 | webserver-3 |
cache-1 | cache-2 | cache-3 |
查看结果: kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
redis-cache-6bc7d5b59d-sg87g 1/1 Running 0 18m 172.16.1.176 10.0.0.34 <none> <none>
redis-cache-6bc7d5b59d-sjv5m 1/1 Running 0 18m 172.16.2.18 10.0.0.9 <none> <none>
redis-cache-6bc7d5b59d-w5xr7 1/1 Running 0 18m 172.16.1.9 10.0.0.45 <none> <none>
web-server-7dffcf98d7-6fwhq 1/1 Running 0 15m 172.16.1.10 10.0.0.45 <none> <none>
web-server-7dffcf98d7-mhrcg 1/1 Running 0 15m 172.16.1.177 10.0.0.34 <none> <none>
web-server-7dffcf98d7-w6h9g 1/1 Running 0 15m 172.16.2.19 10.0.0.9 <none> <none>
上面的例子使用 PodAntiAffinity 规则和 topologyKey: "kubernetes.io/hostname" 来部署 redis 集群以便在同一主机上没有两个实例。 更多文档请参考官方文档
结束
k8s 提供了亲和反亲和性给我们的调度提供了更细粒度的控制. 这里需要注意的是pod间亲和和反亲和确实带来了不少便利,但是pod间的亲和和反亲和需要大量的处理, 这可能会显著减慢大规模集群中的调度。 不建议在超过数百个节点的集群中使用pod 间 的亲和和反亲和。
欢迎大家关注本栏目,我们专注于Kubernetes生态,持续给大家分享。
参考资料
- https://kubernetes.io/docs/concepts/configuration/assign-pod-node/
- https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
- https://coreos.com/fleet/docs/latest/affinity.html