<Kubelet從入門到放棄>系列將對Kubelet組件由基礎知識到源碼進行深刻梳理。上一篇zouyee帶各位看了CPU 管理的相關內容,其中說起拓撲管理,本文將對此進行詳細剖析,拓撲管理在Kubernetes 1.18時提高爲Beta。 TopologyManager功能可實現CPU、內存和外圍設備(例如SR-IOV VF和GPU)的NUMA對齊,從而使集羣知足低延遲需求。node
3、源碼分析算法
對於拓撲管理器代碼分析,咱們從兩個方面進行:api
1)Kubelet初始化時,涉及拓撲管理的相關操做markdown
2)Kubelet運行時,涉及拓撲管理的相關操做,深刻分析拓撲管理結構邏輯架構
3.1 Kubelet初始化app
關於Kubelet初始化,咱們在以CPU manager結合拓撲管理器的啓動圖(當前爲CPU manager、memory manager、device manager構成資源分配管理器,其屬於Container Manager模塊的子系統)進行說明。框架
對於上圖的內容,zouyee總結流程以下:less
一、在命令行啓動部分,Kubelet中調用NewContainerManager構建ContainerManager
二、NewContainerManager函數調用topologymanager.NewManager構建拓撲管理器,不然未啓用拓撲管理器,則構建fake
三、NewContainerManager函數分別調用cpu、memory及device提供的NewManager構建相關管理器
四、若拓撲管理特性開啓,則拓撲管理器使用AddHintPriovider方法將CPU、memory及device管理器加入管理,上述三種資源分配器,須要實現HintPriovider接口。
五、回到命令行啓動部分,調用NewMainKubelet(),構建Kubelet結構體
六、構建Kubelet結構體時,將CPU、memory管理器(沒有device)跟拓撲管理器封裝爲InternalContainerLifecycle接口,其實現Pod相關的生命週期資源管理操做,涉及資源分配回收相關的是PreStartContainer、PostStopContainer方法,可參看具體實現。
七、構建Kubelet結構體時,調用AddPodmitHandler將GetAllocateResourcesPodAdmitHandler方法加入到Pod准入插件中,在Pod建立時,資源預分配檢查,其中GetAllocateResourcesPodAdmitHandler根據是否開啓拓撲管理,決定是返回拓撲管理Admit接口,仍是使用cpu、memory及device構成資源分配器,實現Admit接口。
八、構建Kubelet結構體後,調用ContainerManager的Start方法,ContainerManager在Start方法中調用CPU、memeory及device管理器的Start方法,其作一些處理工做並孵化一個goroutine,執行reconcileState()
注:關於上述啓動流程的代碼解釋,能夠返回識透CPU一文。
複製代碼
3.2 Kubelet運行時ide
Kubelet運行時,涉及到拓撲管理、資源分配的就是對於Pod處理流程,zouyee總結以下:函數
一、PodConfig從apiserver、file及http三處接受Pod,調用Updates()返回channel,內容爲Pod列表及類型。
二、Kubelet調用Run方法,處理PodConfig的Updates()返回的channel
三、在Run方法內部,Kubelet調用syncLoop,而在syncLoop內部,調用syncLoopIteration
四、在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方法,後續統一介紹。
五、在介紹Kubelet啓動時,調用AddPodmitHandler將GetAllocateResourcesPodAdmitHandler方法加入到admitHandlers中,所以在調用Admit方法的操做,涉及到拓撲管理的也就是GetAllocateResourcesPodAdmitHandler,那麼接下來就接受一下該方法。
六、在Kublet的GetAllocateResourcesPodAdmitHandler方法的處理邏輯爲:當啓用拓撲特性時,資源分配由拓撲管理器統一接管,若是未啓用,則爲cpu管理器、內存管理器及設備管理器分別管理,本文只介紹啓用拓撲管理器的狀況。
七、啓用拓撲管理器後,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總結流程以下:
下面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列表。考慮如下場景:
在上述狀況下,生成的惟一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函數處理流程以下所示:
接上文的分配說明,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{01:True}以准入該Pod。
4、後續發展
4.1 已知問題
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
5、參考資料
一、kubernetes-1-18-feature-topoloy-manager-beta
二、topology manager
三、cpu manager policy
四、設計文檔