作者 青鸟
在看了katalyst和koordinator的调度后,发现都离不开对NUMA亲和调度的实现,于是对k8s的NUMA实现有点兴趣,这篇博客也就诞生了.
为什么需要NUMA亲和
现代 CPU 多采用 NUMA 架构, NUMA 全称 “Non-Uniform Memory Access” 即非一致性内存访问。为啥搞个非一致性,一致性不好吗?答案肯定是不好,因为如果使用 UMA 即一致性内存访问,随着北桥上的物理核心越来越多,CPU的频率也越来越高,总线带宽扛不住,访问同一块内存的冲突问题也会越来越严重。我们回到 NUMA架构,每个 NUMA node 上会有自己的物理CPU内核,以及每个 NUMA node 上核心之间之间也共享 L3 Cache。同时,内存也分布在每个NUMA node上的。某些开启了超线程的CPU,一个物理CPU内核在操作系统上会呈现两个逻辑的核。
在NUMA架构中,系统的内存被分成多个节点,每个节点与特定的CPU核心相连。每个CPU核心可以更快地访问其本地节点的内存,而访问其他节点的内存则会导致更高的延迟。对于业务侧,如果程序都跑在同一个NUMA node上,可以更好地去共享一些L3 Cache,L3 Cache的访问速度会很快。如果L3 Cache没有命中,可以到内存中读取数据,访存速度会大大降低。
在容器大行其道的今天,由于 CPU 错误分配的问题尤为严重。因为现在节点出现了超卖,节点上有大量的容器在同时运行,如果同一个进程分配了不同的 NUMA 会发生什么问题:
- CPU 争抢带来频繁的上下文切换时间;
- 频繁的进程切换导致 CPU 高速缓存失败;
- 跨 NUMA 访存会带来更严重的性能瓶颈。
k8s中的NUMA亲和的实现
目前,Kubernetes 在节点级别支持 Pod 间亲和性和反亲和性,然而将这种支持扩展到 NUMA 级别的需求逐渐增加。举例来说,当前我有四个高内存带宽消耗的业务,这些业务之间自然是反亲和的,为了避免互相干扰,在原生的反亲和性调度策略下,我们需要找四个机器节点来逐个部署这些业务,但是使用了 NUMA 粒度的反亲和性策略后,我们只需要将这四个业务部署在同一台机器的四个 NUMA 节点上即可,这样既节省了算力资源,同时能够保证业务之间互不干扰,这就是 NUMA 粒度亲和与反亲和策略的优势所在。
目前,Kubernetes 对于 NUMA 拓扑的亲和力度是节点级别的, 也就是 pod 被调度到某个节点上后 kubelet 会执行设置好的NUMA感知和NUMA亲和调度策略. 具体执行的模块就是Topology Manager. Topology Manager 其实是解决一个历史问题,CPU Manager 和 Device Manager 是独立工作的,互相不感知.这里不再赘述Topology Manager实现的原理,可以看这篇阿里同学写的文章,很清晰的描述了具体实现Kubelet之Topology Manager分析.
这里做一个总结: 找到不同资源的 topology hints 即拓扑信息.寻找的原则如下,不同 topology 类型的 hints 做 merge 操作,也就是 union,选出最优策略. 这里的最优是指cpu 的选择标准是在满足资源申请的情况下,涉及的 NUMA 节点个数最少前提下涉及到 socket 个数最小的优先选择。 device manager 则在满足资源申请情况下,涉及 NUMA 节点最小优先选择。
如果选到还好,如果没选出来怎么办?kubernetes 提供了 kubelet 配置策略:
- best-effort: kubernetes 节点也会接纳这个 Pod,就是效果不达预期。
- restricted:节点会拒绝接纳这个 Pod,如果 Pod 遭到节点拒绝,其状态将变为 Terminated。
- single-NUMA-node:节点会拒绝接纳这个Pod,如果 Pod 遭到节点拒绝,其状态将变为Terminated。这里比 restricted 还多一个条件是选出来的 NUMA 节点个数需要是1个。
所以我们看到 Kubernetes Topology Manager 还是在以 NUMA 为中心来进行不同资源(NIC, GPU, CPU)来进行 complete fair 最短路径的选择。而且是在 pod 被调度到某个节点上后 kubelet 执行上述的过程,这样会带来几个问题:
- pod 有很大概率会 Terminated, 生产上不可用.
- 缺乏集群视角,选择发生在调度节点之后,在调度时没有办法以NUMA亲和调度最佳节点.
- 节点的 topology-manager-policy 配置不方便, kubelet 每次配置参数都需要重启,如果遇到特殊的版本可能会重启所有节点 pod
所以我们会想到几个优化的方案:
- 让一个类似 kubelet 的 daemon 进程,可以发现 topo 关系,并向外暴露;
- 可以让拓扑感知放在 kube-scheduler 给 pod 分配 node 的时候就感知,而且可以指导调度.
这里大致描述一下社区里的开源项目实现的流程:
- agent 从节点采集资源拓扑,包括NUMA、Socket、设备等信息,汇总到一个自定义的节点资源拓扑资源对象中。
- scheduler在调度时会参考节点的节点资源拓扑资源对象获取到节点详细的资源拓扑结构,在调度到节点的同时还会为Pod分配拓扑资源,并将结果写到Pod的annotations中。
- agent在节点上Watch到Pod被调度后,从Pod的annotations中获取到拓扑分配结果,并按照用户给定的CPU绑定策略进行CPUSet的细粒度分配。
这里其实就已经解决上述 Kubernetes Topology Manager 的缺陷。
具体实现建议完整阅读阿里巴巴的Koordinator的实现直播回顾 | 云原生混部系统 Koordinator 架构详解. 相信你会有不小的收获
参考链接: