k8s 中的 numa 亲和性
默认的情况下,k8s 对于 pod 在单个节点的资源分配并不会考虑到 NUMA 架构。比如 cpu 默认会采用 cgroup CFS 来做资源的分配,并未考虑到 NUMA 架构。为了提升 pod 的性能,需要 pod 在分配资源时感知 NUMA 架构。
为此,k8s 在 kubelet 中通过 CPU Manager、Memory Manager、Device Manager、Topology Manager 等特性对 NUMA 做了支持,支持的 pod QoS 类型要求为 Granteed pod。
各特性的支持版本情况如下:
特性 | alpha | beta | stable |
---|---|---|---|
CPU Manager | 1.12 | 1.26 | |
Memory Manager | 1.21 | 1.22 | - |
Topology Manager | 1.16 | 1.18 | - |
CPU Manager
在 k8s 中使用 cgroup 的 CFS 配额来执行 pod 的 CPU 约束,在 CFS 的模式下,pod 可能会运行在不同的核上,会导致 pod 的缓存失效的问题。对于性能要求非常高的 pod,为了提升性能,可以通过 cgroup 中的 cpuset 绑核的特性来提升 pod 的性能。
在 k8s 中仅针对如下的 pod 类型做了绑核操作:
- 必须为 guaranteed pod 类型。即 pod 需要满足如下两个条件:
- pod 中的每个容器都必须指定cpu 和 内存的 request 和 limit。
- pod 中的每个容器的 cpu 和内存的 request 和 limit 必须相等。
- pod 的 cpu request 和 limit 必须为整数。
kubelet 的参数配置
在 k8s 中仅通过 kubelet 来支持 pod 的绑核操作,跟其他组件无关。
kubelet 通过参数 --cpu-manager-policy
或者在 kubelet 的配置文件中参数cpuManagerPolicy
来配置,支持如下值:
- none:默认策略。即不执行绑核操作。
- static:允许为节点上的某些特征的 pod 赋予增强的 cpu 亲和性和独占性。
kubelet 通过参数 --cpu-manager-reconcile-period
来指定内存中的 cpu 分配跟 cgroupfs 一致。
kubelet 通过参数 --cpu-manager-policy-options
来微调。该特性通过特性门控 CPUManagerPolicyOptions 来控制。
模式之间切换
默认的 kubelet cpuManagerPolicy
配置为 none,策略配置位于文件 /var/lib/kubelet/cpu_manager_state
,文件内容如下:
1 | {"policyName":"none","defaultCpuSet":"","checksum":1353318690} |
修改 kubelet 的cpuManagerPolicy
为 static,将模式从 none 切换为 static,重启 kubelet。发现 kubelet 会启动失败,kubelet 并不能支持仅修改参数就切换模式,报如下错误:
1 | Mar 06 15:00:02 iZt4nd5yyw9vfuxn3q2g3tZ kubelet[102800]: E0306 15:00:02.463939 102800 cpu_manager.go:223] "Could not initialize checkpoint manager, please drain node and remove policy state file" err="could not restore state from checkpoint: configured policy \"static\" differs from state checkpoint policy \"none\", please drain this node and delete the CPU manager checkpoint file \"/var/lib/kubelet/cpu_manager_state\" before restarting Kubelet" |
将文件 /var/lib/kubelet/cpu_manager_state
删除后,kubelet 即可启动成功,新创建的 /var/lib/kubelet/cpu_manager_state
文件内容如下:
1 | {"policyName":"static","defaultCpuSet":"0-3","checksum":611748604} |
可以看到已经存在了绑核的 pod。
如果节点上已经存在符合绑核条件的 pod,在修改配置并重启 kubelet 后即可绑核生效。
绑核实践
配置 kubelet 的cpuManagerPolicy
值为 static,创建 guaranteed pod:
1 | apiVersion: apps/v1 |
在 pod 调度的节点上,进入到 /sys/fs/cgroup/cpuset/kubepods.slice 目录下,跟绑核相关的设置均在该目录下,该目录的结构如下:
1 | $ ll /sys/fs/cgroup/cpuset/kubepods.slice |
其中 kubepods-besteffort.slice 和 kubepods-burstable.slice 分别对应的 besteffort 和 burstable 类型的 pod 配置,因为这两种类型的 pod 并不执行绑核操作,所有的子目录下的 cpuset.cpus 文件均绑定的 cpu 核。
kubepods-pod4dc3ad18_5bad_4728_9f79_59f2378de46e.slice 目录为要绑核的 pod 目录,其中 4dc3ad18_5bad_4728_9f79_59f2378de46e 根据 pod 的 uid 转换而来,pod 的 metadata.uid
字段的值为 4dc3ad18-5bad-4728-9f79-59f2378de46e,即目录结构中将-
转换为_
。
目录结构如下:
1 | $ ll /sys/fs/cgroup/cpuset/kubepods.slice/kubepods-pod4dc3ad18_5bad_4728_9f79_59f2378de46e.slice |
其中 cri-containerd-75f8e4b4185f673869604d300629ec2de934cf253244bf91c577f9fc0ba0f14a.scope 和 cri-containerd-cce0a338829921419407fcdc726d1a4bd5d4489da712b49a4834a020131ce718.scope 为 pod 的两个容器,其中一个为 pause 容器,另外一个为 nginx 容器。
1 | $ crictl pods | grep nginx-deploy |
其中第一列的 cce0a33882992 为 pod id,cri-containerd-cce0a338829921419407fcdc726d1a4bd5d4489da712b49a4834a020131ce718.scope 对应的为 pause 容器,查看该目录下的 cpuset.cpus 文件,绑定了所有的核,并未做绑核操作。
1 | $ crictl ps | grep nginx-deploy |
其中第一列的 75f8e4b4185f6 为 container id,cri-containerd-75f8e4b4185f673869604d300629ec2de934cf253244bf91c577f9fc0ba0f14a.scope 对应的为 nginx 容器。查看 /sys/fs/cgroup/cpuset/kubepods.slice/kubepods-pod4dc3ad18_5bad_4728_9f79_59f2378de46e.slice/cri-containerd-75f8e4b4185f673869604d300629ec2de934cf253244bf91c577f9fc0ba0f14a.scope/cpuset.cpus 对应的值为2-3
,说明绑核成功。
/var/lib/kubelet/cpu_manager_state
文件内容如下,跟 cgroup cpuset 实际配置可以完全对应:
1 | {"policyName":"static","defaultCpuSet":"0-1","entries":{"4dc3ad18-5bad-4728-9f79-59f2378de46e":{"nginx":"2-3"}},"checksum":3689800814} |
kubelet 内部实现
在 kubelet 内部,采用 cpu manager 模式实现绑核功能。
总结
- 只能 by 节点配置,不能按照 pod 来灵活的配置。
- 无法适用于所有类型的 pod,pod 必须为 Guaranteed 时才允许开启。
- 修复配置较为麻烦,一旦配置变更后,需要删除文件
/var/lib/kubelet/cpu_manager_state
后才可生效,而且对现有的 pod 均有影响。
Memory Manager
kubelet 中参数配置
k8s 1.21 版本引入,需要使用 featuregate 开启,参数 --feature-gates=MemoryManager=true
。在k8s 1.22 版本为 beta 版本,默认开启。
kubelet 通过参数 --memory-manager-policy
来配置内存管理策略,支持如下值:
- none:默认策略,不执行任何内存分配的策略。
- static:仅针对 Guaranteed pod 生效,对于 Guaranteed pod,会返回跟 NUMA 相关的 topology hint 信息。在该模式下,会修改 cgroup cpuset.mems 的配置为对应的 cpu core。
kubelet 将已经分配的 pod 的内存绑定信息位于文件 /var/lib/kubelet/memory_manager_state
中,文件格式为 json。
Topology Manager
Topology Manager 特性理解起来比较抽象。举个例子说明:上述的 CPU Manager 和 Memory Manager 的特性,在 kubelet 的实现中是完全独立的,可能会导致 cpu 和内存被分配到了不同的 numa 节点上,CPU Manager 通过 cgroup 的 cpuset.cpus 来控制容器要绑定的 cpu,而 Memory Manager 则通过 cgroup 的 cpuset.mems 来控制容器要使用的 NUMA node 内存。如果两者的信息不匹配,则会导致跨 NUMA Node 的内存访问,从而会对于性能要求高的应用产生影响。
Topology Manager 是 kubelet 中的一部分功能,通过 Hint Providers 来发送和接收各个模块的 NUMA 拓扑信息,比如接收 CPU Manager 和 Memory Manager 的 NUMA Node以及 NUMA Node 分配的优先级。
kubelet 的参数配置
kubelet 通过参数--topology-manager-scope
来指定作用域:
- container:默认值,按照容器级别分配到共同的 NUMA node 集合上。
- pod:将 pod 内的所有 container 分配到共同的 NUMA node 集合上。
kubelet 通过参数 --topology-manager-policy
来设置 NUMA 的分配策略:
- none:默认值,不执行任何的拓扑对齐。
- best-effort:优先选择首选亲和性的 NUMA node,如果亲和性不满足,pod 仍然可以调度成功。
- restricted:选择首选亲和性的 NUMA node,如果亲和性不满足,pod 调度失败。
- single-numa-node:通过 Hint Provider 返回的结果,判断单 NUMA 节点的亲和性是否,如果不满足,则 pod 调度失败。
kubelet 中的实现
在 kubelet 中定义了接口:
1 | // TopologyHint is a struct containing the NUMANodeAffinity for a Container |
CPU Manager、Memory Manager 和 Device Manager 均实现了该接口。在 Topology Manager 中根据各个 Manager 返回的 TopologyHint 数据,从而决定最终的 NUMA Node 分配,并调用各个 Manager 的 Allocate 来做最终的 NUMA Node 分配。