Katalyst 支持 NUMA 级别 Pod 间亲和性与反亲和性调度|社区征文

2023总结KubeWharf

最近参加了字节跳动 Kubewharf 社区的开源项目,为其云原生混部系统 Katalyst 贡献代码,使 Katalyst 能够支持 NUMA 级别的 Pod 间亲和性调度,两个月来利用空余时间完成了这一调度策略的代码实现,并且在机器上通过了测试。这篇文章一方面是分享参与字节这一开源项目的一些体验,另一方面也是为了总结项目的一些技术经验。

我的开源体验

首先还是来介绍一下 Katalyst 这个项目吧,在字节跳动,基础设施面临的是一个规模巨大且持续快速变化的业务场景,而云原生技术体系需要同时聚焦资源效率和研发效率。在资源效率上,云原生要解决的核心问题之一就是如何提高集群的资源利用效率,在这种需求推动下,Katalyst 应运而生,Katalyst 致力于解决云原生场景下的资源不合理利用问题,为资源管理和成本优化提供解决方案:1. QoS-Based 资源模型抽象:提供与业务场景匹配的资源 QoS 模型选择;2. 资源弹性管理:提供灵活可扩展的 HPA/VPA 资源弹性策略;3. 微拓扑及异构设备的调度、摆放:资源整体微拓扑感知调度、摆放,以及动态调整能力;4. 精细化资源分配、隔离:根据业务服务画像提供资源的精细化分配、出让和隔离。整体来说,Katalyst是一个旨在提升云计算资源利用效率的开源项目,有兴趣的同学可以前往Katalyst仓库进一步了解:Katalyst-core

我最初接触 Katalyst 是在今年五月份,由于我研究生的研究方向是云计算业务混部优化,也在国际期刊上发表了相关的论文,因此希望能够了解一下工业界在云原生混部这一块的实际落地情况。经过一段时间的学习,我对字节火山引擎的 Katalyst,以及阿里云的 Koordinator 有了相对全面的了解,个人的感受是,在混部这一领域,学术界和工业界的侧重点有比较大的不同,学术界更关注通过 ML 乃至 DL 等技术来分析业务和提升混部性能,工业界有落地的需求,因而更加务实,它更关注利用已有的成熟技术对资源进行更加弹性和精细的管理。对比来看的话,学术界钟情于设计更好的策略提升指标,但对于实际业务环境的复杂性非常缺乏了解;工业界采用的混部策略逻辑相对简洁不少,但是更多地考虑实际业务的复杂性,策略的可用性是第一位的。以上是我个人的管中窥豹之见,如果大家有更多想法欢迎讨论哈。

在关注到工业界的混部实践之后,我开始对这些项目的源码产生了兴趣,并且有了参与项目贡献代码的想法。我挤时间参加了六月份的 GLCC 开源活动,并且只给字节的 Katalyst 开源项目投去了 Proposal 和个人简历,Katalyst 的曹贺老师觉得我这边整体上还不错,和我聊了几十分钟,对我这边 LLC 和 Memory Bandwidth 的一些研究经历表示了兴趣,不过很可惜最后社区选了另一位同学哈哈。虽然这次没有选上,但是让我感觉自己离开源并不遥远,后续暑假我参与了一个小有名气的开源存储引擎的代码重构,积累了一些开源社区贡献的经历。今年九月中旬,曹贺老师再次联系我,原来 Katalyst 开启了一个开源活动,鼓励高校学生提交 Proposal 和简历,为 Katalyst 的几个enhancement issues 贡献代码,彼时我正被实验室派去蚂蚁做学术实习,这次我还是决定继续参与,用一周时间准备了 Proposal 和简历后,九月底社区通知我申请通过,由我来完成 Katalyst Inter-Pod Affinity and Anti-Affinity at NUMA-Level 这一项目,并且安排了 Katalyst 的汪喆师兄来带我,这里需要特别鸣谢喆哥,喆哥后续为我项目的设计和实现提供了非常多的帮助,喆哥本身对 Katalyst 和 Kubernetes 的源代码非常了解,很多看不懂的地方都能在喆哥的指点下快速通关哈哈。

关于项目的具体设计与实现,我将在下面详细介绍。

Support Inter-Pod Affinity and Anti-Affinity at NUMA level of Katalyst

1.背景

目前,Kubernetes 在节点级别支持 Pod 间亲和性和反亲和性,然而将这种支持扩展到 NUMA 级别的需求逐渐增加。例如,当前字节存在大量的搜广推模型需要训练,在分布式深度学习训练架构 PS-Worker 中,worker 作为高内存带宽消耗业务,会影响同一 NUMA 节点上的参数服务器(PS),因此将这些 pod 分配给不同的 NUMA 节点可以减轻这种干扰。

2.技术方案设计

首先,所有的 Pod 亲和性与反亲和性分析最终都需要在节点侧完成,因此必须考虑如何在节点侧实现 NUMA-Level 的亲和性分析,这里 Katalyst 对 NUMA 资源管理提供了一套新的 HintProvider 策略,并且通过非解耦的方式,在 K8S 中新增 ManagerImpl 来为 topologyManager 的 HintProvider 接口提供了一套新的实现,ManagerImpl 会以 rpc 的方式在 kubelet 中调用 Katalyst 的 HintProvider 策略,那么既然 Katalyst 实现了自己的 HintProvider 策略,我们可以通过在该策略中加入 Pod 亲和性和反亲和性策略对原有的 Hints 进行筛选,从而保证最终提供给聚合部分的 Hints 符合亲和与反亲和要求,设计如图1所示。

picture.image

图1 节点侧设计

其次,关于 NUMA 级别的资源管理,K8S 目前原生支持节点侧的 NUMA 节点管理,主要是支持 NUMA 节点的对齐策略。所谓 NUMA 对齐,也就是在 NUMA 架构的服务器中,一个 NUMA 节点上运行的线程访问本地内存要比其它 NUMA 上的内存更快,如果业务的全部线程的运算和内存分配都在单个 NUMA 节点上,则可以尽量避免访问远端内存的情况,这称之为 NUMA 对齐。

K8S 提供的 NUMA 对齐策略包括 best-effort,restrcted 和 single-numa-node,用户选择其中一种以尽可能避免业务发生额外的访存延迟,具体可以参见K8S TopologyManager。但原生的 NUMA 资源管理存在的一个问题是,它仅仅支持节点侧的 NUMA 管理,而调度侧缺乏相关的管理,这会导致调度侧将 Pod 调度到某个 Node 上,但 Node 上所有 NUMA 节点均不满足 Pod 的要求时,该 Pod 会被置为 terminated 状态,因此我们需要在调度侧实现对 NUMA 的感知,从而保证 Pod 分配到节点后部署成功。

因此,整体的设计如图2所示,我们修改了节点侧的 QRM Plugins,在其中的 HintProvider 部分加入了 inter-pod affinity&anti-affinity 筛选;我们在调度侧新增了插件,使之在调度阶段避免将 Pod 分配到所有 NUMA 节点均不满足亲和性要求的 Node。 picture.image

图2 整体设计

3.API设计

接下来是设计 API,最初的方案考虑采用 K8S 原生的 Inter-Pod Affinity & Anti-Affinity 的 API,后来经过字节内部充分的讨论后,大家认为原生的 K8S API 语义过于复杂,会给 JSON 解析带来比较大的开销,因此决定尽可能简化 API 的设计,并且当前设计暂时只考虑硬性亲和。综合意见之后,API的设计示例如下:

metadata:
  annotations:
    "katalyst.kubewharf.io/microtopology_antiaffinity": '{
      "required": [{"matchLabels": {"antiaffinitykey": "antiaffinityvalue"}, "zone":"numa"}]
    }'

也就是在 Pod annotations 中加入新键值对 katalyst.kubewharf.io/microtopology_affinity或 katalyst.kubewharf.io/microtopology_antiaffinity 作为 key,其对应的 value 是一个 JSON 字段,JSON 中标明要求亲和或反亲和的 Pod Labels,并通过 zone 指明该亲和性要求的拓扑域,zone 可以选择 numa 和 socket,默认为 numa。

经过字节其他同学的实验,这个 API 的 JSON 解析产生的开销在可接受范围内,没有对该字段进行解析时,json.Unmarshal在程序火焰图中占比0.92%,进行解析后占0.95%,说明该 API 产生的 CPU 开销没有对程序运行产生较大影响。

picture.image

图3 API Json 解析开销

4.节点侧设计实现

接下来是节点侧的设计实现,前面讲整体设计时提到,节点侧主要做的是在原有的 HintProvider 基础上增加 Inter-Pod 亲和性&反亲和性筛选。

首先需要考虑 labels 的处理,Katalyst-agent 中所有请求都需要落盘存入 state file,为了尽可能减少存储的内容,agent 会对 Pod 的 labels 进行筛选,只存储规定有效的 labels。因此,为了避免亲和性相关的 labels 被筛除掉,我们在 agent 启动参数中添加了 qos-inter-pod-affinity-labels 字段,该字段是一个 string 数组,如果用户需要 NUMA 级别 Pod 间亲和性调度功能,则需要在启动 agent 之前,在该字段中声明后续会使用的亲和&反亲和 Labels 的 key 值,agent 会在处理请求时保留这些 labels,用于后续的亲和&反亲和分析。

接下来是 Pod 间亲和&反亲和的逻辑处理。开启亲和性分析之前我们获取节点中每个 NUMA 节点上运行的 Pod的 Labels 和亲和性信息,并且解析当前传入 Pod 的 Labels 和亲和性信息,然后开启亲和性分析。亲和性分析主要包括三部分:

  • Existing Pod 对 New Pod 的反亲和性要求;
  • New Pod 对 Existing Pod 的反亲和性要求;
  • New Pod 对 Existing Pod 的亲和性要求。

如果某个 Existing Pod 和 New Pod 之间形成了反亲和关系,那么 Existing Pod 所处的拓扑域都不允许 New Pod 部署;如果某个 Existing Pod 和 New Pod 之间形成了亲和关系,那么 Existing Pod 所处的拓扑域都允许 New Pod 部署。

此外,Katalyst 中存在 Exclusive 这一 NUMA 对齐策略,开启该策略的 Pod 要求必须独占 NUMA 节点,排斥所有其它 Pod,如果该策略开启,那么 Pod 的反亲和要求一定被满足,亲和要求一定不满足,所以亲和性分析开始之前需要查看 New Pod 是否要求独占。

完成亲和性分析后,我们获取了 New Pod 对于各个 NUMA 节点的亲和与反亲和信息,我们对 HintProvider 给出的 Hints 进行筛选,如果某个 Hint 中存在 NUMA 节点与 New Pod 形成了反亲和关系,或者没有形成亲和关系(在 New Pod 有亲和要求时),则该 Hint 不满足要求,我们直接将其筛除。

5.调度侧设计实现

调度侧的设计主要是为了避免 New Pod 被分配到所有 NUMA 均不满足亲和&反亲和要求的机器节点上,保证 New Pod 可以被成功部署,我们通过在调度侧设计和注册numainterpodaffinity插件来实现这一点。

为了讲清楚调度侧的设计,首先还是要祭出经典的 K8S 调度框架图:

picture.image

图4 Kubernetes调度框架

我们设计的调度器插件实现了 PreFilter,Filter 和 Reserve 三个接口,下面分别讲述这三部分的设计:

  1. Prefilter。当 New Pod 从调度队列中取出后,我们首先通过 PreFilter 来分析 New Pod 对集群中各个机器节点在 NUMA 上的亲和&反亲和信息。那么这里需要调度侧来读取机器节点的 NUMA 信息,原生的 Kubernetes 自然不支持,这里 Katalyst 设计了新的 CRD —— KCNR 来实现这一点,节点侧会获取本节点当前的 NUMA 信息,上报至 kube-apiserver,调度侧可以从 apiserver 获取节点侧当前的 NUMA 信息,主要包括每个机器节点中每个 NUMA 节点当前的资源,以及部署的 Pod 名称与 UID。这里 PreFilter 和节点侧一样进行亲和性分析,并将每个机器节点的 NUMA 亲和性信息写入 framework.CycleState;
  2. Filter。进入 Filter 阶段后,我们从 framework.CycleState 取出机器节点的 NUMA 信息,并与 New Pod 进行比较,判断该 Node 是否所有 NUMA 都不满足亲和性&反亲和性要求,如果不满足,则筛除该机器节点;
  3. Reserve。当最优机器节点被选出后,我们需要进行微拓扑级别的 Pod 亲和性信息预留。具体来说,当 New Pod 进入 Binding Cycle 后,此时存在一个空窗期,即 New Pod 还没有部署到机器节点上,节点暂时没有上报该 Pod 的 KCNR 信息到调度侧,但是同时 New Pod 很大概率会成功完成节点上的部署,如果此时有新的 Pod 进入调度周期,而调度插件没有将前一个 Pod 考虑进去,那么新的 Pod 部署后有可能与前一个 Pod 发生冲突。为此,在这个空窗期,我们必须通过 Reserve 来实现微拓扑级别的 Pod 亲和性信息预留,Reserve 会将当前 Pod 的亲和性信息记录下来,这部分信息在下一个 PreFilter 阶段会被考虑进去,如果 Pod 后面部署失败则通过 UnReserve 方法回滚删除这些信息,Pod 部署成功并上报最新 KCNR 到调度侧后,则 Reserve 留下的信息就用不上了,调度侧会更新并删除这些亲和性信息。

总结

具体的代码大家可以前往 Katalyst 看我的 PR :enhancement(plugin): Support inter-pod affinity and anti-affinity at NUMA level #347。如果想要使用这项优化策略,请在集群部署 Katalyst 后,在 kubelet 中开启 TopologyManager,并在 Katalyst-agent 启动参数的 qos-inter-pod-affinity-labels 字段中声明参与亲和&反亲和分析的 Labels Key 即可,另外不要忘记在 Pod 部署文件的 annotations 中添加我们的 API。

总体来说,我觉得参与 Katalyst 开源贡献是一段非常难忘的经历,在高校实验室中我们往往很难和真正的集群打交道,因此我印象中大部分同学对于 Kubernetes 不是很了解,我之前对于 Kubernetes 的感觉也是非常遥远。这次参与了 Katalyst 开源贡献后,我对 Kubernetes 的看法有了非常大的改观,这可以说是一次对 K8S “去魅”的经历,原来高踞云端的 K8S 也是非常“平易近人”的,原来阅读 K8S 源码似乎也没有想象中那样晦涩难解,我们甚至可以大胆地对它进行魔改。

最后,字节 Katalyst 团队给我的印象也非常好,通过参与社区内部的许多讨论,我感觉到这是一个很有技术氛围的年轻的团队,交流起来也非常轻松,特别感谢其中曹贺和汪喆两位大佬,使我有机会参与到社区建设并且顺利完成项目的实现,也希望更多同学能够了解 Katalyst,参与项目的开源贡献!

254
3
1
0
关于作者
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论