Kubernetes 亲和性 affinity
一、前言
Kubernetes 支持限制 Pod 在指定的 Node 上运行,或者指定更倾向于在某些特定 Node 上运行。
有几种方式可以实现这个功能:
- NodeAffinity: 支持更丰富的配置规则,使用更灵活。
- PodAffinity: 根据已在节点上运行的 Pod 标签来约束 Pod 可以调度到哪些节点,而不是根据 node label。
二、NodeAffinity
nodeSelector 通过 k-v 的方式非常简单的支持了 pod 调度限制到具有特定标签的节点上。而 nodeAffinity 根据亲和力 & 反亲和力极大地扩展了能够表达的约束信息。
nodeAffinity 特性的设计初衷就是为了替代 nodeSelector。 nodeAffinity 当前支持的匹配符号包括:In、NotIn、Exists、DoesNotExists、Gt、Lt 。
nodeAffinity 当前支持两种调度模式:
硬亲和和软亲和说明:(不管是node还是pod都可以用软亲和或者硬亲和) - requiredDuringSchedulingIgnoredDuringExecution(硬亲和):必须满足,如果不满足则不进行调度,则 Pod 创建失败。 - preferredDuringSchedulingIgnoredDuringExecution(软亲和):优先选择满足条件的节点,如果没有找到满足条件的节点,则在其他节点中择优创建 Pod。
两种模式的名字特长,这是 k8s 的命名风格。其中IgnoredDuringExecution的意义就跟 nodeSelector 的实现一样,即使 node label 发生变更,也不会影响之前已经部署且又不满足 affinity rules 的 pods,这些 pods 还会继续在该 node 上运行。换句话说,亲和性选择节点仅在调度 Pod 时起作用。
来个官方示例,看下怎么玩:
apiVersion: v1
kind: Pod
metadata:
name: with-node-affinity
spec:
affinity:
nodeAffinity:
# 必须选择 node label key 为 kubernetes.io/e2e-az-name,
# value 为 e2e-az1 或 e2e-az2.
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/e2e-az-name
operator: In
values:
- e2e-az1
- e2e-az2
# 过滤掉上面的必选项后,再优先选择 node label key 为 another-node-label-key
# value 为 another-node-label-value.
preferredDuringSchedulingIgnoredDuringExecution:
# 如果满足节点亲和,积分加权重(优选算法,会对 nodes 打分)
# weight: 0 - 100
- 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
简单看下 NodeAffinity 的结构体,下面介绍注意事项时会涉及:
type NodeAffinity struct {
RequiredDuringSchedulingIgnoredDuringExecution *NodeSelector
PreferredDuringSchedulingIgnoredDuringExecution []PreferredSchedulingTerm
}
type NodeSelector struct {
NodeSelectorTerms []NodeSelectorTerm
}
type NodeSelectorTerm struct {
MatchExpressions []NodeSelectorRequirement
MatchFields []NodeSelectorRequirement
}
配置相关的注意点:
- 如果 nodeSelector 和 nodeAffinity 两者都指定,那 node 需要两个条件都满足,pod 才能调度。
- 如果指定了多个 NodeSelectorTerms,那 node 只要满足其中一个条件,pod 就可以进行调度。
- 如果指定了多个 MatchExpressions,那必须要满足所有条件,才能将 pod 调度到该 node。
再来个实例:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution: #硬亲和
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/e2e-az-name #NODE的key
operator: In #在
values: #值里面
- e2e-az1
- e2e-az2
preferredDuringSchedulingIgnoredDuringExecution:#软亲和
- weight: 1 //取值范围1-100
preference:
matchExpressions:
- key: another-node-label-key
operator: In
values:
- another-node-label-value
containers:
- name: nginx
image: docker.io/nginx
三、PodAffinity
nodeSelector & nodeAffinity 都是基于 node label 进行调度。而有时候我们希望调度的时候能考虑 pod 之间的关系,而不只是 pod 和 node 的关系。
举个例子,会有需求希望服务 A 和 B 部署在同一个机房、机架或机器上,因为这些服务可能会对网路延迟比较敏感,需要低延时;再比如,希望服务 C 和 D 又希望尽量分开部署,即使一台主机甚至一个机房出了问题,也不会导致两个服务一起挂而影响服务可用性,提升故障容灾的能力。
podAffinity 会基于节点上已经运行的 pod label 来约束新 pod 的调度。 其规则就是“如果 X 已经运行了一个或者多个符合规则 Y 的 Pod,那么这个 Pod 应该(如果是反亲和性,则是不应该)调度到 X 上”。 这里的 Y 是关联 namespace 的 labelSelector,当然 namespace 也可以是 all。和 node 不同,pod 是隶属于 namespace 下的资源,所以基于 pod labelSelector 必须指定具体的 namespace;而 X 则可以理解为一个拓扑域,类似于 node、rack、zone、cloud region 等等,就是前面提到的 内置 Node 标签 ,当然也可以自定义。
看下 pod affinity 涉及的结构体,便于进行功能介绍:
type Affinity struct {
// NodeAffinity 前面介绍了
NodeAffinity *NodeAffinity
// pod 亲和性
PodAffinity *PodAffinity
// pod 反亲和性
PodAntiAffinity *PodAntiAffinity
}
type PodAffinity struct {
// hard 模式, 必选项
RequiredDuringSchedulingIgnoredDuringExecution []PodAffinityTerm
// soft 模式, 进行 node 优先
PreferredDuringSchedulingIgnoredDuringExecution []WeightedPodAffinityTerm
}
type PodAffinityTerm struct {
LabelSelector *metav1.LabelSelector
Namespaces []string
TopologyKey string
}
type WeightedPodAffinityTerm struct {
Weight int32
PodAffinityTerm PodAffinityTerm
}
podAffinity 和 nodeAffinity 有相似的地方,使用了 labelSelector 进行匹配,支持的匹配符号包括:In、NotIn、Exists、DoesNotExists; 也支持两种调度模式 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution, 功能和 nodeAffinity 一样,这里就不在累述。
podAffinity 和 nodeAffinity 也有较大的差异,前面讲了 pod 是 namespace 资源,所以必然会需要配置 namespaces,支持配置多个 namespace。如果省略的话,默认为待调度 pod 所属的 namespace;如果定义了但是值为空,则表示使用 “all” namespaces。
还有一个较大的差别 TopologyKey, 便于理解进行单独介绍。
3.1、TopologyKey
TopologyKey 用于定义 in the same place,前面也介绍了是拓扑域的概念。
看下面的图,这两个 pod 到底该如何算在一个拓扑域?
如果我们使用k8s.io/hostname,in the same place 则意味着在同一个 node,那下图的 pods 就不在一个 place:
如果我们使用failure-domain.k8s.io/zone 来表示一个 place,那下图的 pods 就表示在一个 zone:
当然我们也可以自定义 node labels 作为 TopologyKey。比如我们可以给一组 node 打上 rack 标签,那下图的 pods 表示在同一个 place:
原则上,topologyKey 可以是任何合法的 label key。但是出于性能和安全考虑,topologyKey 存在一些限制:
- 对于亲和性和反亲和性的 requiredDuringSchedulingIgnoredDuringExecution 模式,topologyKey 不能为空
- pod 反亲和性 requiredDuringSchedulingIgnoredDuringExecution 模式下,LimitPodHardAntiAffinityTopology 权限控制器会限制 topologyKey 只能设置为 kubernetes.io/hostname。当然如果你想要使用自定义 topology,那可以简单禁用即可。
- pod 反亲和性 preferredDuringSchedulingIgnoredDuringExecution 模式下,topologyKey 为空则表示所有的拓扑域。截止 v1.12 版本,所有的拓扑域还只能是 kubernetes.io/hostname, failure-domain.beta.kubernetes.io/zone 和 failure-domain.beta.kubernetes.io/region 的组合。
- 除此之外,topologyKey 可以是任何合法的 label key。
3.2、示例:
来个官方示例,有三节点集群,需要分别部署 3 份 web 和 redis 服务。希望 web 与 redis 服务共存,但需要保证各个服务的副本分散部署。
先创建 redis 集群:
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-cache
spec:
selector:
matchLabels:
app: store
replicas: 3
template:
metadata:
labels:
app: store
spec:
affinity:
// pod 反亲和性, 打散 redis 各个副本
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- store
topologyKey: "kubernetes.io/hostname"
containers:
- name: redis-server
image: redis:3.2-alpine
再部署 web 服务,需要打散并且与 redis 服务共存,配置如下:
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.12-alpine
注意1: pod affinity 需要进行大量处理,所以会明显减慢大型集群的调度时间,不建议在大于几百个节点的集群中使用该功能。
注意2: pod antiAffinity 要求对节点进行一致标志,即集群中的所有节点都必须具有适当的标签用于配置给 topologyKey,如果节点缺少指定的 topologyKey 指定的标签,则可能会导致意外行为。
3.3、再来些强有力的例子
先定义一个参照目标pod:
apiVersion: v1
kind: Pod
metadata:
name: pod-flag
labels:
security: "S1"
app: "nginx"
spec:
containers:
- name: nginx
image: nginx
podAffinity:pod和pod运行在一起
apiVersion: v1
kind: Pod
metadata:
name: with-pod-affinity
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution: #硬亲和,必须满足
- labelSelector: #pod标签选择器
matchExpressions:
- key: security #key
operator: In #在
values:
- S1 #value的列表
topologyKey: kubernetes.io/hostname #通过node的hostname判断是否是相同节点,节点名一定不能一样
containers:
- name: with-pod-affinity
image: k8s.gcr.io/pause:2.0
创建后可以看到这个pod与上面那个参照的pod位于同一个node上,另外,如果将这个node上的kubernetes.io/hostname标签干掉,将会发现pod会一直处于pending状态,这是因为找不到满足条件的node了。
pod互斥性调度
下面是一个互斥性调度的示例:
apiVersion: v1
kind: Pod
metadata:
name: with-pod-affinity
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S1
topologyKey: "failure-domain.beta.kubernetes.io/zone"
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S2
topologyKey: kubernetes.io/hostname
containers:
- name: with-pod-affinity
image: gcr.io/google_containers/pause:2.0
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!