Pod Topology Spread Constraints介绍

微信扫一扫,分享到朋友圈

Pod Topology Spread Constraints介绍
  • 1. 术语解释

  • 2. 背景介绍

    • 2.1 概述

    • 2.2 目标

  • 3. 设计细节

    • 3.1 API 变化

    • 3.2 一个简单的例子

    • 3.3 更多复杂的例子

    • 3.3.1 与 NodeSelector / NodeAffinity 组合使用

    • 3.3.2 多 TopologySpreadConstraints

    • 3.3.3 拓扑域不可调度

  • 4. 实现

  • 5. 参考

1. 术语解释

拓扑域:就 Kubernetes 而言,它们是按节点标签定义和分组的一系列 Node,例如属于相同区域/可用区/机架/主机名都可以作为划分拓扑域的依据。

亲和性:在本文指的是 NodeAffinity、 PodAffinity 以及 PodAntiAffinity。

2. 背景介绍

2.1 概述

在 k8s 集群调度领域,“亲和性”相关的概念本质上都是控制 Pod 如何被调度—— 堆叠或是打散。
目前 k8s 提供了  podAffinity 以及 podAntiAffinity 两个特性对 Pod 在不同拓扑域的分布进行了一些控制,podAffinity 可以将无数个 Pod 调度到特定的某一个拓扑域,这是堆叠的体现;podAntiAffinity 则可以控制一个拓扑域只存在一个 Pod,这是打散的体现。但这两种情况都太极端了,在不少场景下都无法达到理想的效果,例如为了实现容灾和高可用,将业务 Pod 尽可能均匀得分布在不同可用区就很难实现。

+---------------+---------------+
|     zoneA     |     zoneB     |
+-------+-------+-------+-------+
| node1 | node2 | node3 | node4 |
+-------+-------+-------+-------+
|  P P  | P P P |   P   |       |       
+-------+-------+-------+-------+

2.2 目标


PodTopologySpread 特性的提出正是为了对 Pod 的调度分布提供更精细的控制,以提高服务可用性以及资源利用率。PodTopologySpread 由  EvenPodsSpread 特性门控所控制,在 v1.16 版本第一次发布,并在 v1.18 版本进入 beta 阶段默认启用。

PodTopologySpread 特性的目标包括:

  • Pod Topology Spread Constraints 以 Pod 级别为粒度进行调度控制;

  • Pod Topology Spread Constraints 既可以是 filter,也可以是 score;

3. 设计细节

3.1 API 变化

在 Pod 的 spec 中新增了一个字段 ` topologySpreadConstraints
` :

spec:

  
topologySpreadConstraints:

  
- maxSkew: <integer>

    
topologyKey: <string>

    
whenUnsatisfiable: <string>

    
labelSelector: <object>

由于这个新增的字段是在 Pod spec 层面添加,因此更高层级的控制 app (Deployment、DaemonSet、StatefulSet) 也能使用  PodTopologySpread 功能。


让我们结合上图来理解 ` topologySpreadConstraints
` 中各个字段的含义和作用:

  • labelSelector 用来查找匹配的 Pod。我们能够计算出每个拓扑域中匹配该  label selector 的 Pod 数量;在上图中,假如 label selector 是 “app:foo”,那么 zone1 的匹配个数为 2, zone2 的匹配个数为 0。

  • topologyKey是 Node label 的 key。如果两个 Node 的 label 同时具有该 key 并且 label 值相同,就说它们在同一个拓扑域。在上图中,指定 topologyKey 为 zone, 具有 “zone=zone1”  标签的 Node 被分在一个拓扑域,具有 “zone=zone2”  标签的 Node 被分在另一个拓扑域。

  • maxSkew 描述了 Pod 在不同拓扑域中不均匀分布的最大程度,maxSkew 的取值必须大于 0。每个拓扑域都有一个 skew,计算的公式是:skew[i] = 拓扑域[i]中匹配的 Pod 个数 – min{拓扑域[*]中匹配的 Pod 个数}。在上图中,我们新建一个带有 “app=foo” label 的 Pod:

    • 如果该 Pod 被调度到 zone1,那么 zone1 中 Node 的 skew 值变为 3,zone2 中 Node 的 skew 值变为 0 (zone1 有 3 个匹配的 Pod,zone2 有 0 个匹配的 Pod );

    • 如果该 Pod 被调度到 zone2,那么 zone1 中 Node 的 skew 值变为 1,zone2 中 Node 的 skew 值变为 0 (zone2 有 1 个匹配的 Pod,拥有全局最小匹配 Pod 数的拓扑域正是 zone2 自己 );

  • whenUnsatisfiable
      描述了如果 Pod 不满足分布约束条件该采取何种策略:

    • DoNotSchedule (默认) 告诉调度器不要调度该 Pod,因此 DoNotSchedule 也可以叫作硬需求 ( hard requirement
      );

    • ScheduleAnyway 告诉调度器根据每个 Node 的 skew 值打分排序后仍然调度,因此 ScheduleAnyway 也可以叫作软需求 ( soft requirement
      );

3.2 一个简单的例子

假设我们有一个 4 Node 的集群分布在两个可用区,并且有着如下的标签:

NAME STATUS ROLES AGE VERSION LABELS

node1 Ready <none> 4m26s v1.18.2 node=node1,zone=zoneA

node2 Ready <none> 3m58s v1.18.2 node=node2,zone=zoneA

node3 Ready <none> 3m17s v1.18.2 node=node3,zone=zoneB

node4 Ready <none> 2m43s v1.18.2 node=node4,zone=zoneB


集群中有 3 个打有 ` foo:bar` 标签的
Pod, 分别位于 node1、 node2 以及 node3:

+---------------+---------------+
|     zoneA     |     zoneB     |
+-------+-------+-------+-------+
| node1 | node2 | node3 | node4 |
+-------+-------+-------+-------+
|   P   |   P   |   P   |       |
+-------+-------+-------+-------+   

如果我们想再添加一个 Pod 并且均匀分布不同可用区,我们可以提交如下 yaml:

kind: Pod

apiVersion: v1

metadata:

  
name: mypod

  
labels:

    
foo: bar

spec:

  
topologySpreadConstraints:

  
- maxSkew: 
1

    
topologyKey: zone

    
whenUnsatisfiable: DoNotSchedule

    
labelSelector:

      
matchLabels:

        
foo: bar

  
containers:

  
- name: nginx

    
image: nginx

`topologyKey: zone` 表明了 Pod 均匀分布的应用范围只局限于拥有 ” zone:<any value>
” 标签的 Node。` whenUnsatisfiable: DoNotSchedule
` 则告诉调度器如果 Pod 无法满足均匀分布的约束条件,就一直让它 pending。

如果调度器把新建的 Pod 调度到 zoneA,那么 Pod 分布将会变成 3/1,zoneA 的实际 skew 变成 2 (3 – 1),因此不满足 ` maxSkew: 1
` 的约束条件,因此该 Pod 只能被调度到 zoneB,可能的 Pod 分布有两种:

+---------------+---------------+      +---------------+---------------+
|     zoneA     |     zoneB     |      |     zoneA     |     zoneB     |
+-------+-------+-------+-------+      +-------+-------+-------+-------+
| node1 | node2 | node3 | node4 |  OR  | node1 | node2 | node3 | node4 |
+-------+-------+-------+-------+      +-------+-------+-------+-------+
|   P   |   P   |   P   |   P   |      |   P   |   P   |  P P  |       |
+-------+-------+-------+-------+      +-------+-------+-------+-------+

我们也可以调整 Pod 的 spec 来满足不同需求:

  • 调整 `maxSkew` 为 2,新建的 Pod 就能被调度到 zoneA 了。

  • 调整 ` topologyKey
    ` 为 “node”,就可以让 Pod 均匀分布在不同 Node 上。如果上面例子 `maxSkew` 保持为1,新建的 Pod 只能被调度到 node4。

  • 调整 `whenUnsatisfiable: DoNotSchedule` 为 `whenUnsatisfiable: ScheduleAnyway`,新建的 Pod 将永远会被调度,并且调度到拥有匹配 Pod 数量较少的拓扑域中的可能性更大。

3.3 更多复杂的例子

3.3.1 与 NodeSelector / NodeAffinity 组合使用


`topologySpreadConstraints` 
默认遍历搜索所有 Node 并且根据 topologyKey 将 Node 分成不同拓扑域,但在某些场景下并不需要遍历所有 Node。例如,假设有一个集群中的节点分别被标记为 “env=Prod”、”env=staging” 以及 “env=qa”,然后我们仅仅想要让 Pod 均匀分布在 qa 环境的多个 zone 。我们可以利用 NodeSelector 或者 NodeAffinity spec,通过指定 ` spec.affinity.nodeAffinity
`,可以限制搜索域为 qa 环境。在上图中,新建的 Pod 遵循 topology spread constraint 只能被调度到 zone2。 

3.3.2 多 TopologySpreadConstraints

多个 `
topologySpreadConstraints`  共同约束也是一个常见需求。上图中,如果我们想要在调度 Pod 时候同时满足以下两个约束条件:

  • Pod 均匀分布在不同 zone

  • Pod 均匀分布在不同 Node

对于第一个约束条件,zone1 有 3 个 Pod,zone2 有 2 个 Pod,所以新建的 Pod 只能被调度到 zone2 以满足 `maxSkew = 1` 的约束条件。换句话说,第一个约束条件的结果集合是 nodeX 与 nodeY。

对于第二个约束条件,nodeB 和 nodeX 分别有 3 个和 2 个 Pod,因此新建的 Pod 只能被调度到 nodeA 和 nodeY 上。

将两个约束的结果集合做交集,我们就可以得出结论,同时符合两个约束条件的节点只能是 nodeY。

3.3.3 拓扑域不可调度

+-------------+-------------+--------------------+

|    zone1    |    zone2    | zone3 (infeasible) |                   

+-------------+-------------+--------------------+

| pod,pod,pod | pod,pod,pod |                    |

+-------------+-------------+--------------------+

spec:

  
topologySpreadConstraints:

  
- maxSkew: 
1

    
topologyKey: zone

    
whenUnsatisfiable: <Action>

    
labelSelector:

      
matchLabels:

        
foo: bar

假设有一个集群分布在 3 个 zone,所有 Pod 都含有标签 “foo: bar”,目前 Pod 在 3 个 zone 的分布是 3/3/0,但是 zone3 所有的 Node 由于 taint 或者资源不足无法被调度。现在有一个新建的 Pod 含有标签 “foo: bar” 并且  “maxSkew: 1″。

如果 Action 是 DoNotSchedule (也就是硬需求),调度器则会认为拓扑域全局的最小匹配数是 0,所以该 Pod 无法被调度。

如果 Action 是 ScheduleAnyway (也就是软需求),调度器则会认为拓扑域全局的最小匹配数是 3,所以该 Pod 会以同样的概率调度到 zone1 或者 zone2。

4. 实现


PodTopologySpread 特性的实现主要分为两个部分: Filter
和  Score
,这部分代码在 v1.17~v.1.19 间变动较大,但背后的思想却是一致的,用伪代码可以清晰描述这两个关键流程的步骤。

Filter

for each candidate node; do

if "TopologySpreadConstraint" is enabled for the pod being scheduled; then

# minMatching num is globally calculated

count number of matching pods on the topology domain this node belongs to

if "matching num - minMatching num" < "MaxSkew"; then

approve it

fi

fi

done


Score

for each candidate node; do

if "TopologySpreadConstraint" is enabled for the pod being scheduled; then

# minMatching num is calculated across node list filtered by Predicate phase

count number of matching pods on the topology domain this node belongs to

calculate the value of "matching num - minMatching num" minus "MaxSkew"

the lower, the higher score this node is ranked

fi

done

由于篇幅限制,我们只介绍 PodTopologySpread Filter 部分的核心代码,k8s 版本为 v1.18.2。

// 代码摘自 kubernetes/pkg/scheduler/framework/plugins/podtopologyspread/filtering.go



// Filter invoked at the filter extension point.

// cycleState 类似 golang 中的 context,在整个调度 framework 流程中传递数据

// pod 待调度的 Pod

// nodeInfo 候选节点的信息

func (pl *PodTopologySpread) Filter(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod, nodeInfo *nodeinfo.NodeInfo) *framework.Status {

node := nodeInfo.Node()

if node == nil {

return framework.NewStatus(framework.Error, "node not found")

}

// 获取 preFilterState

// preFilterState 在 PreFilter 扩展点通过扫描所有节点计算得出,包含了每个拓扑域的匹配数和全局最小匹配数

s, err := getPreFilterState(cycleState)

if err != nil {

return framework.NewStatus(framework.Error, err.Error())

}


// However, "empty" preFilterState is legit which tolerates every toSchedule Pod.

// 如果 Pod Spec 没有指定 PodTopologySpread Constraint,直接通过过滤

if len(s.TpPairToMatchNum) == 0 || len(s.Constraints) == 0 {

return nil

}


podLabelSet := labels.Set(pod.Labels)

for _, c := range s.Constraints {

tpKey := c.TopologyKey

tpVal, ok := node.Labels[c.TopologyKey]

if !ok {

klog.V(5).Infof("node '%s' doesn't have required label '%s'", node.Name, tpKey)

return framework.NewStatus(framework.Unschedulable, ErrReasonConstraintsNotMatch)

}


selfMatchNum := int32(0)

// 如果待调度 Pod 也符合 PodTopologySpread Constraint 中的 labelSelector, 则 selfMatchNum = 1

if c.Selector.Matches(podLabelSet) {

selfMatchNum = 1

}


// 组装节点所在 topology 的结构体

pair := topologyPair{key: tpKey, value: tpVal}

paths, ok := s.TpKeyToCriticalPaths[tpKey]

if !ok {

// error which should not happen

klog.Errorf("internal error: get paths from key %q of %#v", tpKey, s.TpKeyToCriticalPaths)

continue

}

// judging criteria:

// 'existing matching num' + 'if self-match (1 or 0)' - 'global min matching num' <= 'maxSkew'

// 核心判定条件

minMatchNum := paths[0].MatchNum // 全局最小匹配数

matchNum := s.TpPairToMatchNum[pair] // 本节点所在拓扑域的匹配数量

skew := matchNum + selfMatchNum - minMatchNum // Pod 调度到本节点后的实际 skew 值

// 实际 skew 大于 maxSkew,则本节点未通过过滤

if skew > c.MaxSkew {

klog.V(5).Infof("node '%s' failed spreadConstraint[%s]: MatchNum(%d) + selfMatchNum(%d) - minMatchNum(%d) > maxSkew(%d)", node.Name, tpKey, matchNum, selfMatchNum, minMatchNum, c.MaxSkew)

return framework.NewStatus(framework.Unschedulable, ErrReasonConstraintsNotMatch)

}

}


// 如果符合所有 Constraint,节点通过本次过滤

return nil

}

5. 参考

https://kubernetes.io/blog/2020/05/introducing-podtopologyspread/

https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/

https://github.com/kubernetes/enhancements/tree/master/keps/sig-scheduling/895-pod-topology-spread#goals

https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity

文章转载自
云原生计算


点击这里阅读原文了解更多

2020年7月30日-8月1日





线上峰会介绍




这里有封H5【邀请函】



大会网站,https://cncf.lfasiallc.cn/

扫描二维码联系我们!

CNCF (Cloud Native Computing Foundation)成立于2015年12月,隶属于Linux  Foundation,是非营利性组织。 



CNCF








云原生计算基金会

)致力于培育和维护一个厂商中立的开源生态系统,来推广云原生技术。我们通过将最前沿的模式民主化,让这些创新为大众所用。请长按以下二维码进行关注。

中国近视人口将达7亿?爱眼日眼科专家教你辨别真假近视科学防控

上一篇

万物皆可 Serverless 之使用 SCF+COS 快速开发全栈应用

下一篇

你也可能喜欢

Pod Topology Spread Constraints介绍

长按储存图像,分享给朋友