<Kubelet从入门到放弃>系列将对Kubelet组件由基础知识到源码进行深刻梳理。上一篇zouyee带各位看了CPU 治理的相干内容,其中提及拓扑治理,本文将对此进行具体分析,拓扑治理在Kubernetes 1.18时晋升为Beta。 TopologyManager性能可实现CPU、内存和外围设备(例如SR-IOV VF和GPU)的NUMA对齐,从而使集群满足低提早需要。
三、源码剖析
对于拓扑管理器代码剖析,咱们从两个方面进行:
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、设计文档