三、源码分析
对于拓扑管理器代码分析,我们从两个方面进行:
1)Kubelet初始化时,涉及拓扑管理的相关操作
2)Kubelet运行时,涉及拓扑管理的相关操作,深入分析拓扑管理结构逻辑
3.1 Kubelet初始化
关于Kubelet初始化,我们在以CPU manager结合拓扑管理器的启动图(当前为CPU manager、memory manager、device manager构成资源分配管理器,其属于Container Manager模块的子系统)进行说明。
对于上图的内容,zouyee总结流程如下:
1、在命令行启动部分,Kubelet中调用NewContainerManager构建ContainerManager
2、NewContainerManager函数调用topologymanager.NewManager构建拓扑管理器,否则未启用拓扑管理器,则构建fake
3、NewContainerManager函数分别调用cpu、memory及device提供的NewManager构建相关管理器
4、若拓扑管理特性开启,则拓扑管理器使用AddHintPriovider方法将CPU、memory及device管理器加入管理,上述三种资源分配器,需要实现HintPriovider接口。
5、回到命令行启动部分,调用NewMainKubelet(),构建Kubelet结构体
6、构建Kubelet结构体时,将CPU、memory管理器(没有device)跟拓扑管理器封装为InternalContainerLifecycle接口,其实现Pod相关的生命周期资源管理操作,涉及资源分配回收相关的是PreStartContainer、PostStopContainer方法,可参看具体实现。
7、构建Kubelet结构体时,调用AddPodmitHandler将GetAllocateResourcesPodAdmitHandler方法加入到Pod准入插件中,在Pod创建时,资源预分配检查,其中GetAllocateResourcesPodAdmitHandler根据是否开启拓扑管理,决定是返回拓扑管理Admit接口,还是使用cpu、memory及device构成资源分配器,实现Admit接口。
8、构建Kubelet结构体后,调用ContainerManager的Start方法,ContainerManager在Start方法中调用CPU、memeory及device管理器的Start方法,其做一些处理工作并孵化一个goroutine,执行reconcileState()
注:关于上述启动流程的代码解释,可以返回识透CPU一文。
3.2 Kubelet运行时
Kubelet运行时,涉及到拓扑管理、资源分配的就是对于Pod处理流程,zouyee总结如下:
1、PodConfig从apiserver、file及http三处接受Pod,调用Updates()返回channel,内容为Pod列表及类型。
2、Kubelet调用Run方法,处理PodConfig的Updates()返回的channel
3、在Run方法内部,Kubelet调用syncLoop,而在syncLoop内部,调用syncLoopIteration
4、在syncLoopIteration中,当configCh(即PodConfig调用的Updates())返回的pod类型为ADD时,执行handler.HandlePodAdditions,在HandlePodAdditions中,处理流程如下:当pod状态为非Termination时,Kubelet遍历admitHandlers,调用Admit方法。
注:syncLoopIteration中除了configCh,还有其他channel(plegCh、syncCh、housekeepingCh及livenessManager)其中plegCh、syncCh及livenessManager三类channel中调用的HandlePodAddtion、HandlePodReconcile、HandlePodSyncs及HandlePodUpdates都涉及dispatch方法调用,还记得Kubelet流程中,将CPU管理器、内存管理器跟拓扑管理器封装为InternalContainerLifecycle接口,其实现Pod相关的生命周期资源管理操作,涉及CPU、内存相关的是PreStartContainer方法,其调用AddContainer方法,后续统一介绍。
5、在介绍Kubelet启动时,调用AddPodmitHandler将GetAllocateResourcesPodAdmitHandler方法加入到admitHandlers中,因此在调用Admit方法的操作,涉及到拓扑管理的也就是GetAllocateResourcesPodAdmitHandler,那么接下来就接受一下该方法。
6、在Kublet的GetAllocateResourcesPodAdmitHandler方法的处理逻辑为:当启用拓扑特性时,资源分配由拓扑管理器统一接管,如果未启用,则为cpu管理器、内存管理器及设备管理器分别管理,本文只介绍启用拓扑管理器的情况。
7、启用拓扑管理器后,Kublet的GetAllocateResourcesPodAdmitHandler返回的Admit接口类型,由拓扑管理器实现,后续统一介绍。
上述流程即为Pod大致的处理流程,下面介绍拓扑结构初始化、AddContainer及Admit方法。
1)拓扑结构初始化
拓扑结构初始化函数为pkg/kubelet/cm/topologymanager/topology_manager.go:119
// NewManager creates a new TopologyManager based on provided policy and scope
func NewManager(topology []cadvisorapi.Node, topologyPolicyName string, topologyScopeName string) (Manager, error) {
// a. 根据cadvisor数据初始化numa信息
var numaNodes []int
for _, node := range topology {
numaNodes = append(numaNodes, node.Id)
}
// b. 判断策略为非none时,numa节点数量是否超过8,若超过,则返回错误
if topologyPolicyName != PolicyNone && len(numaNodes) > maxAllowableNUMANodes {
return nil, fmt.Errorf("unsupported on machines with more than %v NUMA Nodes", maxAllowableNUMANodes)
}
// c. 根据传入policy名称,进行初始化policy
var policy Policy
switch topologyPolicyName {
case PolicyNone:
policy = NewNonePolicy()
case PolicyBestEffort:
policy = NewBestEffortPolicy(numaNodes)
case PolicyRestricted:
policy = NewRestrictedPolicy(numaNodes)
case PolicySingleNumaNode:
policy = NewSingleNumaNodePolicy(numaNodes)
default:
return nil, fmt.Errorf("unknown policy: \"%s\"", topologyPolicyName)
}
// d. 根据传入scope名称,以初始化policy结构体初始化scope
var scope Scope
switch topologyScopeName {
case containerTopologyScope:
scope = NewContainerScope(policy)
case podTopologyScope:
scope = NewPodScope(policy)
default:
return nil, fmt.Errorf("unknown scope: \"%s\"", topologyScopeName)
}
// e. 封装scope,返回manager结构体
manager := &manager{
scope: scope,
}
a. 根据cadvisor数据初始化numa信息
b. 判断策略为非none时,numa节点数量是否超过8,若超过,则返回错误
c. 根据传入policy名称,进行初始化policy
d. 根据传入scope名称,以初始化policy结构体初始化scope
e. 封装scope,返回manager结构体
2) AddContainer
AddContainer实际调用scope的方法:pkg/kubelet/cm/topologymanager/scope.go:97
func (s *scope) AddContainer(pod *v1.Pod, containerID string) error {
s.mutex.Lock()
defer s.mutex.Unlock()
s.podMap[containerID] = string(pod.UID)
return nil
}
该处只做简单字典加入操作。
3)Admit
Admit函数调用:pkg/kubelet/cm/topologymanager/topology_manager.go:186,根据scope类型分别调用不同的实现:
a、container
pkg/kubelet/cm/topologymanager/scope_container.go:45
func (s *containerScope) Admit(pod *v1.Pod) lifecycle.PodAdmitResult {
// Exception - Policy : none
// 1. 策略为none,则跳过
if s.policy.Name() == PolicyNone {
return s.admitPolicyNone(pod)
}
// 2. 遍历init及常规容器
for _, container := range append(pod.Spec.InitContainers, pod.Spec.Containers...) {
// 2.1 计算亲和性,判断是否准入
bestHint, admit := s.calculateAffinity(pod, &container)
if !admit {
return topologyAffinityError()
}
// 2.2 记录分配结果
s.setTopologyHints(string(pod.UID), container.Name, bestHint)
// 2.3 调用hint provider分配资源
err := s.allocateAlignedResources(pod, &container)
if err != nil {
return unexpectedAdmissionError(err)
}
}
return admitPod()
}
b、pod
pkg/kubelet/cm/topologymanager/scope_pod.go:45
func (s *podScope) Admit(pod *v1.Pod) lifecycle.PodAdmitResult {
// Exception - Policy : none
// 1. 策略为none,则跳过
if s.policy.Name() == PolicyNone {
return s.admitPolicyNone(pod)
}
// 2 计算亲和性,判断是否准入
bestHint, admit := s.calculateAffinity(pod)
if !admit {
return topologyAffinityError()
}
// 3. 遍历init及常规容器
for _, container := range append(pod.Spec.InitContainers, pod.Spec.Containers...) {
// 3.1 记录分配结果
s.setTopologyHints(string(pod.UID), container.Name, bestHint)
// 3.2 调用hint provider分配资源
err := s.allocateAlignedResources(pod, &container)
if err != nil {
return unexpectedAdmissionError(err)
}
}
return admitPod()
}
具体说明见代码注释,需要说明的是scope为container与pod的区别主要在计算亲和性,判断是否准入的阶段,同样也反应了scope与container的粒度,后续重点介绍calculateAffinity方法。
下面zouyee带各位总结一下拓扑管理器的Admit逻辑。
拓扑管理器为组件定义Hint Providers的接口,以发送和接收拓扑信息,CPU、memory及device都实现该接口,拓扑管理器调用AddHintPriovider加入到管理器,其中拓扑信息表示可用的 NUMA 节点和首选分配指示的位掩码。 拓扑管理器策略对所提供的hint执行一组操作,并根据策略获取最优解;如果存储了与预期不符的hint,则该建议的优选字段设置为 false。所选建议可用来决定节点接受或拒绝 Pod 。 之后,hint结果存储在拓扑管理器中,供Hint Providers进行资源分配决策时使用。
对于上述两种作用域(container及pod)的calculateAffinity通用流程,汇总如下(忽略计算亲和性的差异):
对于上图的内容,zouyee总结流程如下:
- 遍历容器中的所有容器(scope为pod跟container的差别,上面已经说明)
- 对于每个容器,针对容器请求的每种拓扑感知资源类型(例如gpu-vendor.com/gpu、nic-vendor.com/nic、cpu等),从一组HintProviders中获取TopologyHints。
- 使用选定的策略,合并收集到的TopologyHints以找到最佳hint,该hint可以在所有资源类型之间对齐资源分配。
- 循环返回hintHintProviders集合,指示他们使用合并的hint来分配他们管理的资源。
- 如果上述步骤中的任一个失败或根据所选策略无法满足对齐要求,Kubelet将不会准入该pod。
下面zouyee根据下图依次介绍拓扑管理器涉及的结构体。
a. TopologyHints
拓扑hint对一组约束进行编码,记录可以满足给定的资源请求。 目前,我们唯一考虑的约束是NUMA对齐。 定义如下:
type TopologyHint struct {
NUMANodeAffinity bitmask.BitMask
Preferred bool
}
NUMANodeAffinity字段表示可以满足资源请求的NUMA节点个数的位掩码,是bitmask类型。 例如,在2个NUMA节点的系统上,可能的掩码包括:
{00}, {01}, {10}, {11}
Preferred是用来管理NUMANodeAffinity是否生效的布尔类型,如果Preferred为true那么当前的亲和度有效,如果为false那么当前的亲和度无效。 使用best-effort策略时,在生成最佳hint时,优先hint将优先于非优先hint。 使用restricted和single-numa-node策略时,将拒绝非优先hint。
HintProvider为每个可以满足该资源请求的NUMA节点的掩码生成一个TopologyHint。 如果掩码不能满足要求,则将其省略。 例如,当被要求分配2个资源时,HintProvider可能在具有2个NUMA节点的系统上提供以下hint。 这些hint编码代表的两种资源可以都来自单个NUMA节点(0或1),也可以各自来自不同的NUMA节点。
{01: True}, {10: True}, {11: False}
当且仅当NUMANodeAffinity代表的信息可以满足资源请求的最小NUMA节点集时,所有HintProvider才会将Preferred字段设置为True。
{0011: True}, {0111: False}, {1011: False}, {1111: False}
如果在其他容器释放资源之前无法满足实际的首选分配,则HintProvider返回所有Preferred字段设置为False的hint列表。考虑以下场景:
- 当前,除2个CPU外的所有CPU均已分配给容器
- 剩余的2个CPU在不同的NUMA节点上
- 一个新的容器请求2个CPU
在上述情况下,生成的唯一hint是{11:False}而不是{11:True}。因为可以从该系统上的同一NUMA节点分配2个CPU(虽然当前的分配状态,还不能立即分配),在可以满足最小对齐方式时,使pod进入失败并重试部署总比选择以次优对齐方式调度pod更好。
b. HintProviders
目前,Kubernetes中仅有的HintProviders是CPUManager、MemoryManager及DeviceManager。 拓扑管理器既从HintProviders收集TopologyHint,又使用合并的最佳hint调用资源分配。 HintProviders实现以下接口:
type HintProvider interface {
GetTopologyHints(*v1.Pod, *v1.Container) map[string][]TopologyHint
Allocate(*v1.Pod, *v1.Container) error
}
注意:GetTopologyHints返回一个map [string] [] TopologyHint。 这使单个HintProvider可以提供多种资源类型的hint。 例如,DeviceManager可以返回插件注册的多种资源类型。
当HintProvider生成hint时,仅考虑如何满足系统上当前可用资源的对齐方式。 不考虑已经分配给其他容器的任何资源。
例如,考虑图1中的系统,以下两个容器请求资源:
# Container0
spec:
containers:
- name: numa-aligned-container0
image: alpine
resources:
limits:
cpu: 2
memory: 200Mi
gpu-vendor.com/gpu: 1
nic-vendor.com/nic: 1
# Container1
spec:
containers:
- name: numa-aligned-container1
image: alpine
resources:
limits:
cpu: 2
memory: 200Mi
gpu-vendor.com/gpu: 1
nic-vendor.com/nic: 1
如果Container0是要在系统上分配的第一个容器,则当前三种拓扑感知资源类型生成以下hint集:
cpu: {{01: True}, {10: True}, {11: False}}
gpu-vendor.com/gpu: {{01: True}, {10: True}}
nic-vendor.com/nic: {{01: True}, {10: True}}
已经对齐的资源分配:
{cpu: {0, 1}, gpu: 0, nic: 0}
在考虑Container1时,上述资源假定为不可用,因此将生成以下hint集:
cpu: {{01: True}, {10: True}, {11: False}}
gpu-vendor.com/gpu: {{10: True}}
nic-vendor.com/nic: {{10: True}}
分配的对齐资源:
{cpu: {4, 5}, gpu: 1, nic: 1}
注意:HintProviders调用Allocate的时,并未采用合并的最佳hint, 而是通过TopologyManager实现的Store接口,HintProviders通过该接口,获取生成的hint:
type Store interface {
GetAffinity(podUID string, containerName string) TopologyHint
}
c. Policy.Merge
每个策略都实现了合并方法,各自实现如何将所有HintProviders生成的TopologyHint集合合并到单个TopologyHint中,该TopologyHint用于提供已对齐的资源分配信息。
// 1. bestEffort
func (p *bestEffortPolicy) Merge(providersHints []map[string][]TopologyHint) (TopologyHint, bool) {
filteredProvidersHints := filterProvidersHints(providersHints)
bestHint := mergeFilteredHints(p.numaNodes, filteredProvidersHints)
admit := p.canAdmitPodResult(&bestHint)
return bestHint, admit
}
// 2. restrict
func (p *restrictedPolicy) Merge(providersHints []map[string][]TopologyHint) (TopologyHint, bool) {
filteredHints := filterProvidersHints(providersHints)
hint := mergeFilteredHints(p.numaNodes, filteredHints)
admit := p.canAdmitPodResult(&hint)
return hint, admit
}
// 3. sigle-numa-node
func (p *singleNumaNodePolicy) Merge(providersHints []map[string][]TopologyHint) (TopologyHint, bool) {
filteredHints := filterProvidersHints(providersHints)
// Filter to only include don't cares and hints with a single NUMA node.
singleNumaHints := filterSingleNumaHints(filteredHints)
bestHint := mergeFilteredHints(p.numaNodes, singleNumaHints)
defaultAffinity, _ := bitmask.NewBitMask(p.numaNodes...)
if bestHint.NUMANodeAffinity.IsEqual(defaultAffinity) {
bestHint = TopologyHint{nil, bestHint.Preferred}
}
admit := p.canAdmitPodResult(&bestHint)
return bestHint, admit
}
从上述三种分配策略,可以发现Merge方法的一些类似流程:
1. filterProvidersHints
2. mergeFilteredHints
3. canAdmitPodResult
其中filterProvidersHints位于pkg/kubelet/cm/topologymanager/policy.go:62
func filterProvidersHints(providersHints []map[string][]TopologyHint) [][]TopologyHint {
// Loop through all hint providers and save an accumulated list of the
// hints returned by each hint provider. If no hints are provided, assume
// that provider has no preference for topology-aware allocation.
var allProviderHints [][]TopologyHint
for _, hints := range providersHints {
// If hints is nil, insert a single, preferred any-numa hint into allProviderHints.
if len(hints) == 0 {
klog.Infof("[topologymanager] Hint Provider has no preference for NUMA affinity with any resource")
allProviderHints = append(allProviderHints, []TopologyHint{{nil, true}})
continue
}
// Otherwise, accumulate the hints for each resource type into allProviderHints.
for resource := range hints {
if hints[resource] == nil {
klog.Infof("[topologymanager] Hint Provider has no preference for NUMA affinity with resource '%s'", resource)
allProviderHints = append(allProviderHints, []TopologyHint{{nil, true}})
continue
}
if len(hints[resource]) == 0 {
klog.Infof("[topologymanager] Hint Provider has no possible NUMA affinities for resource '%s'", resource)
allProviderHints = append(allProviderHints, []TopologyHint{{nil, false}})
continue
}
allProviderHints = append(allProviderHints, hints[resource])
}
}
return allProviderHints
}
遍历所有的HintProviders,收集并存储hint。如果HintProviders没有提供任何hint,那么就默认为该provider没有任何资源分配。最终返回allProviderHints.
其中mergeFilteredHints位于pkg/kubelet/cm/topologymanager/policy.go:95
// Merge a TopologyHints permutation to a single hint by performing a bitwise-AND
// of their affinity masks. The hint shall be preferred if all hits in the permutation
// are preferred.
func mergePermutation(numaNodes []int, permutation []TopologyHint) TopologyHint {
// Get the NUMANodeAffinity from each hint in the permutation and see if any
// of them encode unpreferred allocations.
preferred := true
defaultAffinity, _ := bitmask.NewBitMask(numaNodes...)
var numaAffinities []bitmask.BitMask
for _, hint := range permutation {
// Only consider hints that have an actual NUMANodeAffinity set.
if hint.NUMANodeAffinity == nil {
numaAffinities = append(numaAffinities, defaultAffinity)
} else {
numaAffinities = append(numaAffinities, hint.NUMANodeAffinity)
}
if !hint.Preferred {
preferred = false
}
}
// Merge the affinities using a bitwise-and operation.
mergedAffinity := bitmask.And(defaultAffinity, numaAffinities...)
// Build a mergedHint from the merged affinity mask, indicating if an
// preferred allocation was used to generate the affinity mask or not.
return TopologyHint{mergedAffinity, preferred}
}
func mergeFilteredHints(numaNodes []int, filteredHints [][]TopologyHint) TopologyHint {
// Set the default affinity as an any-numa affinity containing the list
// of NUMA Nodes available on this machine.
defaultAffinity, _ := bitmask.NewBitMask(numaNodes...)
// Set the bestHint to return from this function as {nil false}.
// This will only be returned if no better hint can be found when
// merging hints from each hint provider.
bestHint := TopologyHint{defaultAffinity, false}
iterateAllProviderTopologyHints(filteredHints, func(permutation []TopologyHint) {
// Get the NUMANodeAffinity from each hint in the permutation and see if any
// of them encode unpreferred allocations.
mergedHint := mergePermutation(numaNodes, permutation)
// Only consider mergedHints that result in a NUMANodeAffinity > 0 to
// replace the current bestHint.
if mergedHint.NUMANodeAffinity.Count() == 0 {
return
}
// If the current bestHint is non-preferred and the new mergedHint is
// preferred, always choose the preferred hint over the non-preferred one.
if mergedHint.Preferred && !bestHint.Preferred {
bestHint = mergedHint
return
}
// If the current bestHint is preferred and the new mergedHint is
// non-preferred, never update bestHint, regardless of mergedHint's
// narowness.
if !mergedHint.Preferred && bestHint.Preferred {
return
}
// If mergedHint and bestHint has the same preference, only consider
// mergedHints that have a narrower NUMANodeAffinity than the
// NUMANodeAffinity in the current bestHint.
if !mergedHint.NUMANodeAffinity.IsNarrowerThan(bestHint.NUMANodeAffinity) {
return
}
// In all other cases, update bestHint to the current mergedHint
bestHint = mergedHint
})
return bestHint
}
mergeFilteredHints函数处理流程如下所示:
- 通过cadvisor传递的NUMA节点数生成bitmask
- 设置 bestHint := TopologyHint{defaultAffinity, false}如果没有符合条件的hint,返回该hint
- 取每种资源类型生成的TopologyHints的交叉积
- 对于交叉中的每个条目,每个TopologyHint的NUMA亲和力执行位计算。 在合并hint中将此设置为NUMA亲和性。
- 如果条目中的所有hint都将Preferred设置为True,则在合并hint中的Preferred设置为True。
- 如果条目中存在Preferred设置为False的hint,则在合并hint中的Preferred设置为False。 如果其NUMA亲和性节点数量全为0,则在合并hint中的Preferred设置为False。
接上文的分配说明,Container0的hint为:
cpu: {{01: True}, {10: True}, {11: False}}
gpu-vendor.com/gpu: {{01: True}, {10: True}}
nic-vendor.com/nic: {{01: True}, {10: True}}
上面的算法将产生的交叉积及合并后的hint:
cross-product entry{cpu, gpu-vendor.com/gpu, nic-vendor.com/nic} "merged" hint
{{01: True}, {01: True}, {01: True}} {01: True}
{{01: True}, {01: True}, {10: True}} {00: False}
{{01: True}, {10: True}, {01: True}} {00: False}
{{01: True}, {10: True}, {10: True}} {00: False}
{{10: True}, {01: True}, {01: True}} {00: False}
{{10: True}, {01: True}, {10: True}} {00: False}
{{10: True}, {10: True}, {01: True}} {00: False}
{{10: True}, {10: True}, {10: True}} {01: True}
{{11: False}, {01: True}, {01: True}} {01: False}
{{11: False}, {01: True}, {10: True}} {00: False}
{{11: False}, {10: True}, {01: True}} {00: False}
{{11: False}, {10: True}, {10: True}} {10: False}
生成合并的hint列表之后,将根据Kubelet配置的拓扑管理器分配策略来确定哪个为最佳hint。
一般流程如下所示:
- 根据合并hint的“狭窄度”进行排序。狭窄度定义为hint的NUMA相似性掩码中设置的位数。设置的位数越少,hint越窄。对于在NUMA关联掩码中设置了相同位数的hint,设置为最低位的hint被认为是较窄的。
- 根据合并hint的Preferred字段排序。Preferred为true的hint优于Preferred为true的hint。
- 为Preferred选择具有最佳设置的最窄hint。
在上面的示例中,当前支持的所有策略都将使用hint{01:True}以准入该Pod。
四、后续发展
4.1 已知问题
- 拓扑管理器所能处理的最大 NUMA 节点个数是 8。若 NUMA 节点数超过 8, 枚举可能的 NUMA 亲和性而生成hint时会导致数据爆炸式增长。
- 调度器不支持资源拓扑功能,当调度至该节点,但因为拓扑管理器的原因导致在该节点上调度失败。
4.2 功能特性
a. hugepage的numa应用
如前所述,当前仅可用于TopologyManager的三个HintProvider是CPUManager、MemoryManager及DeviceManager。 但是,目前也正在努力增加对hugepage的支持,TopologyManager最终将能够在同一NUMA节点上分配内存,大页,CPU和PCI设备。
b. 调度
当前,TopologyManager不参与Pod调度决策,仅充当Pod Admission控制器,当调度器将Pod调度到某节点后,TopologyManager才判定应该接受还是拒绝该pod。但是可能会因为节点可用的NUMA对齐资源而拒绝pod,这跟调度系统的决定相悖。
那么我们如何解决这个问题呢?当前Kubernetes调度框架提供实现framework架构,调度算法插件化,可以实现诸如NUMA对齐之类的调度插件。
d. Pod对齐策略
如前所述,单个策略通过Kubelet命令行应用于节点上的所有Pod,而不是根据Pod进行自定义配置。
当前实现该特性最大的问题是,此功能需要更改API才能在Pod结构或其关联的RuntimeClass中表达所需的对齐策略。
后续相关内容,请查看公众号:DCOS
https://mp.weixin.qq.com/s/mA...
五、参考资料
1、kubernetes-1-18-feature-topoloy-manager-beta
2、topology manager
3、cpu manager policy
4、设计文档