前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Pod Topology Spread Constraints介绍

Pod Topology Spread Constraints介绍

作者头像
CNCF
发布2020-06-05 11:25:43
6.7K0
发布2020-06-05 11:25:43
举报
文章被收录于专栏:CNCFCNCF

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 的集群分布在两个可用区,并且有着如下的标签:

代码语言:javascript
复制
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: PodapiVersion: v1metadata: name: mypod labels: foo: barspec: 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 特性的实现主要分为两个部分:FilterScore,这部分代码在 v1.17~v.1.19 间变动较大,但背后的思想却是一致的,用伪代码可以清晰描述这两个关键流程的步骤。

代码语言:javascript
复制
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。

代码语言:javascript
复制
// 代码摘自 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-06-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 CNCF 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 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. 参考
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档