专栏首页CNCFPod Topology Spread Constraints介绍

Pod Topology Spread Constraints介绍

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: 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 间变动较大,但背后的思想却是一致的,用伪代码可以清晰描述这两个关键流程的步骤。

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

文章转载自云原生计算。

本文分享自微信公众号 - CNCF(lf_cncf)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-06-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Kubernetes模式:容量规划

    一个优秀的云原生应用程序设计应该声明它需要正确操作的任何特定资源。Kubernetes使用这些需求来做出最有效的决策,以确保应用程序的最大性能和可用性。

    CNCF
  • K8s中优雅停机和零宕机部署

    创建、删除 Pod 是 K8s 中最常见的任务之一。本文介绍了 Pod 在响应创建、删除请求时发生的内部流程,还讨论了如何在 Pod 启动或关闭时防止断开连接,...

    CNCF
  • Kubernetes 中如何保证优雅地停止 Pod

    一直以来我对优雅地停止 Pod 这件事理解得很单纯:不就利用是 PreStop Hook 做优雅退出吗?但最近发现很多场景下 PreStop Hook 并不能很...

    CNCF
  • 快速了解Kubernetes

    Kubernetes 源自于 Google 内部的服务编排系统 - Borg,诞生于2014年。它汲取了Google 十五年生产环境的经验积累,并融合了社区优秀...

    端碗吹水
  • 了解Kubernetes主体架构(二十七)

    接下来还会逐步完善本教程,比如Helm、ELK、Windows Server容器等等。

    雪雁-心莱科技
  • 了解Kubernetes主体架构(二十八)

    接下来还会逐步完善本教程,比如Helm、ELK、Windows Server容器等等。

    心莱科技雪雁
  • Kubernetes-核心资源之Pod

    在Kubernetes集群中,Pod是所有业务类型的基础,它是一个或多个容器的组合。这些容器共享存储、网络和命名空间,以及如何运行的规范。在Pod中,所有容器都...

    菲宇
  • 基于Win10单机部署kubernetes应用

    鸽了好久了,终于又一次克服了拖延症,决心写点啥,起因也是因为最近刚好重做了系统,把win10从home版升级到了专业版,可以愉快的安装docker destop...

    麒思妙想
  • Kubernetes Pod 故障归类与排查方法

    Docker 是 Kubernetes Pod 中最常用的容器运行时,但 Pod 也能支持其他的容器运行,比如 rkt、podman等。

    YP小站
  • Kubernetes模式:容量规划

    一个优秀的云原生应用程序设计应该声明它需要正确操作的任何特定资源。Kubernetes使用这些需求来做出最有效的决策,以确保应用程序的最大性能和可用性。

    CNCF

扫码关注云+社区

领取腾讯云代金券