Topology Manager是kubelet的一個元件,在kubernetes 1.16加入,而kubernetes 1.18中該feature變為beta版。本篇文檔将分析Topology Manager的具體工作原理。
1.為什麼需要Topology Manager
現代計算機的CPU架構多采用NUMA(Non-Uniform Memory Access,非統一記憶體)架構。NUMA就是将cpu資源分開,以node 為機關進行分組,每個node都有着獨有的cpu、memory等資源,當一個NUMA節點内的資源互相動時,性能将會有很大的提升;但是,如果是兩個NUMA節點之間的資源互動将會變得很慢。
下面這幅圖中有兩個NUMA節點存在:
- NUMA0:由cpu0、cpu1、cpu2、cpu3以及gpu0、nic0和一塊本地記憶體組成
- NUMA1:由cpu4、cpu5、cpu6、cpu7以及gpu1、nic1和一塊本地記憶體組成
假設某個pod需要的資源清單如下:
- 4個CPU
- 200MB記憶體
- 1個GPU
- 1個NIC
我們知道,在kubelet中cpu和其他外圍裝置(比如GPU)的配置設定由不同的元件完成,cpu的配置設定由CPU Manager完成,外圍裝置由Device Manager完成。它們在給pod配置設定裝置時,都是獨立工作的,不會有一個全局觀念,這會造成一個什麼問題呢?在這個例子中,對于該pod而言比較好的資源組合有兩個:
- 組合1:cpu0、cpu1、cpu2、cpu3、gpu0、nic0
- 組合2:cpu4、cpu5、cpu6、cpu7、gpu1、nic1
之是以稱為比較好的組合,因為這些資源都在一個NUMA節點内。但是CPU Manager和Device Manager是獨立工作的,它們不會感覺對方給出的配置設定方案與自己給出的配置設定方案是不是最優的組合,于是就有可能出現下面這種組合:
- 組合3:cpu0、cpu1、cpu2、cpu3、gpu1、nic1
這個配置設定方案就不是我們想要的。Topology Manager就是為了解決這個問題而設計的,它的目标就是要找到我們例子中的組合1群組合2。

2.什麼是TopologyHint
TopologyHint用中文描述為“拓撲提示”,在Topology Manager中,TopologyHint的定義如下:
type TopologyHint struct {
NUMANodeAffinity bitmask.BitMask
Preferred bool
}
其中NUMANodeAffinity是用bitmask表示的NUMA節點的組合。舉個例子,假設有兩個NUMA節點(編号分别為0和1),那麼可能出現的組合為:[0]、[1]、[0,1],用bitmask表示為:01,10,11(從右往左開始,組合中有哪一個NUMA節點,那一位就是1)。
Preferred代表這個NUMA節點組合對于某個pod而言是不是“優先考慮的”,某個TopologyHint對于pod而言是不是“優先考慮的”需要遵循如下的規則:在滿足申請資源個數的前提下,選擇的資源所涉及的NUMA節點個數最少,就是“優先考慮的”。怎麼了解這句話?我們舉個例子——假設現在有兩個NUMA節點(編号為0和1),每個NUMA節點上都有兩個cpu,如果某個pod需要請求兩個cpu,那麼TopologyHint有如下幾個:
- {01: True}代表從NUMA0上配置設定兩個cpu給pod,這兩個cpu都在一個NUMA節點上,涉及的NUMA節點個數最少(為1),是以是“優先考慮的”。
- {10: True}代表從NUMA1上配置設定兩個cpu給pod,這兩個cpu也在一個NUMA節點上,涉及的NUMA節點個數也最少(為1),是以是“優先考慮的”。
- {11: False}代表從NUMA0和NUMA1上各取一個cpu,涉及的NUMA節點個數為2,是以不是“優先考慮的”。
那麼,是不是所配置設定的資源必須在一個NUMA節點内,這個方案對于pod而言才是“優先考慮的”呢?——當然不是,比如現在有兩個NUMA節點,每個NUMA節點都隻有1塊GPU,而某個pod申請了2個GPU,此時{11: True}這個TopologyHint就是“優先考慮的”,因為在滿足申請資源個數的前提下,最少要涉及到2個NUMA節點。
3.Topology Manager的四種政策
Topology Manager提供了四種政策供使用者組合各個資源的TopologyHint。這四種政策是:
- none:什麼也不做,與沒有開啟Topology Manager的效果一樣。
- best-effort: 允許Topology Manager通過組合各個資源提供的TopologyHint,而找到一個最優的TopologyHint,如果沒有找到也沒關系,節點也會接納這個Pod。
- restricted:允許Topology Manager通過組合各個資源提供的TopologyHint,而找到一個最優的TopologyHint,如果沒有找到,那麼節點會拒絕接納這個Pod,如果Pod遭到節點拒絕,其狀态将變為Terminated。
- single-numa-node:允許Topology Manager通過組合各個資源提供的TopologyHint,而找到一個最優的TopologyHint,并且這個最優的TopologyHint所涉及的NUMA節點個數是1。如果沒有找到,那麼節點會拒絕接納這個Pod,如果Pod遭到節點拒絕,其狀态将變為Terminated。
至于Topology Manager是怎樣組合各個資源提供的TopologyHint,并且找到一個最優的TopologyHint這個問題,我們會在後面詳細闡述。
4.怎樣開啟Topology Manager
如果kubernetes版本為1.18及其以上的版本,直接在kubelet的啟動項中添加:
--topology-manager-policy=
[none | best-effort | restricted | single-numa-node]
如果kubernetes版本為1.16到1.18之間,還需要在kubelet啟動項中添加:
--feature-gates="...,TopologyManager=<true|false>"
5.什麼是HintProvider
在kubelet源碼中,HintProvider的定義如下:
type HintProvider interface {
// 根據container請求的資源數産生一組TopologyHint
GetTopologyHints(*v1.Pod, *v1.Container) map[string][]TopologyHint
// 根據container請求的資源數為container配置設定具體的資源
Allocate(*v1.Pod, *v1.Container) error
}
其中GetTopologyHints這個函數用于為某個container産生某種或多種資源的TopologyHint數組。舉個例子,假設有兩個NUMA節點(編号為0和1),NUMA0上有cpu1和cpu2,NUMA1上有cpu3和cpu4,某個pod請求兩個cpu。那麼CPU Manager這個HintProvider會調用GetTopologyHints産生如下的TopologyHint:
- {01: True}代表從NUMA0取2個cpu,并且是“優先考慮的”。
- {10: True}代表從NUMA1取2個cpu,并且是“優先考慮的”。
- {11: False}代表從NUM0和NUMA1各取一個cpu,不是“優先考慮的”。
目前在kubelet中充當HintProvider的總共有兩個元件:一個是CPU Manager,另外一個是Device Manager,這兩個元件都實作了HintProvider這個接口的兩個方法,後續會把HugePages元件加入進來。
另外需要注意的是:GetTopologyHints(*v1.Pod, *v1.Container) map[string][]TopologyHint函數的傳回類型是map[string][]TopologyHint,為什麼會是這種類型呢?這是為Device Manager設計的,因為Device Manager需要組合多種資源(比如GPU、NIC),每種資源都傳回一組TopologyHint。
6.Topology Manager工作原理
下面這段僞代碼說明了Topology Manager的主要工作原理:
for container := range append(InitContainers, Containers...) {
// 周遊每一個HintProvider
for provider := range HintProviders {
// 對每一個HintProvider,調用GetTopologyHints擷取一組或多組TopologyHint
hints += provider.GetTopologyHints(container)
}
// 将所有的TopologyHint進行合并操作
bestHint := policy.Merge(hints)
// 通過合并找到最優的TopologyHint,然後代入每一個HintProvider的Allocate函數中
// 為container配置設定資源
for provider := range HintProviders {
provider.Allocate(container, bestHint)
}
}
用一幅圖說明一下其原理:
- 周遊pod中的每一個容器
- 對于每一個容器,使用所有的HintProvider的GetTopologyHints方法産生TopologyHint
- 對這些TopologyHint做合并操作,尋求一個最優的TopologyHint
- 每個HintProvider通過最優的TopologyHint給容器配置設定相應的資源
- 根據設定的不同的政策,是否允許節點接納這個pod
接下來對每個階段進行詳細說明。
6.1 CPU Manager的GetTopologyHints實作
前面說過,目前可作為HintProvider的元件有兩個:CPU Manager和Device Manager。那麼這兩個元件是如何為給定的pod産生一組(或多組)TopologyHint的呢?本節首先分析CPU Manager。
CPU Manager的GetTopologyHints方法主要是調用了其policy的GetTopologyHints方法。而CPU Manager的static policy對該方法的實作如下:
func (p *staticPolicy) GetTopologyHints(s state.State, pod *v1.Pod, container *v1.Container) map[string][]topologymanager.TopologyHint {
// 省略其他非關鍵性代碼
......
// 産生TopologyHint的主要邏輯由這個函數完成
cpuHints := p.generateCPUTopologyHints(available, reusable, requested)
// 可以看到,隻傳回了一種資源的TopologyHint,那就是cpu
return map[string][]topologymanager.TopologyHint{
string(v1.ResourceCPU): cpuHints,
}
}
主要的邏輯都是由generateCPUTopologyHints這個函數完成,generateCPUTopologyHints内容如下:
func (p *staticPolicy) generateCPUTopologyHints(availableCPUs cpuset.CPUSet, reusableCPUs cpuset.CPUSet, request int) []topologymanager.TopologyHint {
// 在滿足容器申請資源數的前提下,TopologyHint涉及到的最少的NUMA節點個數
// 初始值為k8s節點上所有NUMA節點的個數。
minAffinitySize := p.topology.CPUDetails.NUMANodes().Size()
// 在滿足容器申請資源數的前提下,TopologyHint涉及到的最少的Socket個數
// 初始值為k8s節點上所有Socket的個數。
minSocketsOnMinAffinity := p.topology.CPUDetails.Sockets().Size()
// 用于儲存所有TopologyHint
hints := []topologymanager.TopologyHint{}
// bitmask.IterateBitMasks這個函數用于将k8s節點上所有的NUMA節點求組合,然後通過回調函數處理這個組合。
// 例如某個k8s節點上有3個NUMA節點(編号為0,1,2),那麼所有組合有
// [[0],[1],[2],[0,1],[0,2],[1,2],[0,1,2]]
bitmask.IterateBitMasks(p.topology.CPUDetails.NUMANodes().ToSlice(), func(mask bitmask.BitMask) {
// 取出NUMA節點組合(以bitmask形式表示)中所涉及到的cpu
cpusInMask := p.topology.CPUDetails.CPUsInNUMANodes(mask.GetBits()...).Size()
// 取出NUMA節點組合(以bitmask形式表示)中所涉及到的Socket
socketsInMask := p.topology.CPUDetails.SocketsInNUMANodes(mask.GetBits()...).Size()
// 如果NUMA節點組合中所涉及到的cpu個數比請求的cpu數大,并且這個組合所涉及的NUMA節點個數
// 是目前為止所有組合中最小的,那麼就更新它。
if cpusInMask >= request && mask.Count() < minAffinitySize {
minAffinitySize = mask.Count()
if socketsInMask < minSocketsOnMinAffinity {
minSocketsOnMinAffinity = socketsInMask
}
}
// 下面這兩個for循環使用者統計目前k8s節點可用的cpu中,有哪些是屬于目前正在處理的NUMA節點組合
numMatching := 0
for _, c := range reusableCPUs.ToSlice() {
// Disregard this mask if its NUMANode isn't part of it.
if !mask.IsSet(p.topology.CPUDetails[c].NUMANodeID) {
return
}
numMatching++
}
for _, c := range availableCPUs.ToSlice() {
if mask.IsSet(p.topology.CPUDetails[c].NUMANodeID) {
numMatching++
}
}
// 如果目前組合中可用的cpu數比請求的cpu小,那麼就直接傳回
if numMatching < request {
return
}
// 否則就建立一個TopologyHint,并把它加入到hints這個slice中
hints = append(hints, topologymanager.TopologyHint{
NUMANodeAffinity: mask,
Preferred: false,
})
})
// 這一步表示拿到所有的TopologyHint後,開始對哪些TopologyHint标注“Preferred = true”
// 這些TopologyHint會被标注為“Preferred = true”:
// (1)涉及到的NUMA節點個數最少
// (2)涉及到的socket個數最少
for i := range hints {
if hints[i].NUMANodeAffinity.Count() == minAffinitySize {
nodes := hints[i].NUMANodeAffinity.GetBits()
numSockets := p.topology.CPUDetails.SocketsInNUMANodes(nodes...).Size()
if numSockets == minSocketsOnMinAffinity {
hints[i].Preferred = true
}
}
}
return hints
}
總結一下這個函數:
- 建立一個存放TopologyHint的數組,名稱為hints。
- 根據k8s節點上所有的NUMA節點ID求所有的NUMA節點組合。
- 找出這些組合中涉及NUMA節點個數的最小值,将這個值設定為minAffinitySize。
- 找出這些組合中涉及到Socket個數的最小值,将這個值設定為minSocketsOnMinAffinity。
- 對每個組合,檢查目前k8s節點上可用的cpu與該組合所涉及的cpu的交集的個數是否大于容器申請的cpu數,如果比容器申請的cpu數小,那麼就不建立TopologyHint,否則就建立一個TopologyHint,并放入hints中。
- 檢查hints中所有的TopologyHint,如果該TopologyHint涉及到的NUMA節點數與minAffinitySize值相同,并且該TopologyHint所涉及到的Socket數與minSocketsOnMinAffinity相同,那麼将該TopologyHint的Preferred設定為true。
以一張圖來說明一下整個流程,圖中有3個NUMA節點,每個節點有2個cpu,假設某個pod請求2個cpu以及已知目前k8s節點上空閑的cpu,尋找TopologyHint過程如圖:
6.2 Device Manager的GetTopologyHints實作
DeviceManager的GetTopologyHint函數實作與CPU Manager的GetTopologyHint函數實作基本一緻,該函數主要調用generateDeviceTopologyHints這個函數,generateDeviceTopologyHints函數内容如下:
func (m *ManagerImpl) generateDeviceTopologyHints(resource string, available sets.String, reusable sets.String, request int) []topologymanager.TopologyHint {
// 初始化minAffinitySize為k8s節點中NUMA節點個數
minAffinitySize := len(m.numaNodes)
// 擷取所有NUMA節點組合
hints := []topologymanager.TopologyHint{}
bitmask.IterateBitMasks(m.numaNodes, func(mask bitmask.BitMask) {
// 對每一個NUMA組合做如下處理
// First, update minAffinitySize for the current request size.
// devicesInMask用于統計該NUMA組合涉及到device個數
devicesInMask := 0
// 擷取某種資源下的所有裝置(比如擷取gpu資源的所有GPU卡),并檢查該device是否在目前NUMA組合中
// 如果在,devicesInMask值加1
for _, device := range m.allDevices[resource] {
if mask.AnySet(m.getNUMANodeIds(device.Topology)) {
devicesInMask++
}
}
// 如果目前NUMA組合涉及到的device數量比request當,并且目前NUMA組合中包含的NUMA個數
// 比minAffinitySize還小,那麼更新minAffinitySize的值。
if devicesInMask >= request && mask.Count() < minAffinitySize {
minAffinitySize = mask.Count()
}
// numMatching用于擷取目前NUMA組合中空閑的device數
numMatching := 0
for d := range reusable {
// Skip the device if it doesn't specify any topology info.
if m.allDevices[resource][d].Topology == nil {
continue
}
// Otherwise disregard this mask if its NUMANode isn't part of it.
// 如果reusable中的device的NUMA節點ID不在目前這個NUMA組合中,那麼直接傳回
// 不對這個NUMA組合建立TopologyHint,這樣做的原因是保證reusable中的device
// 優先被使用完
if !mask.AnySet(m.getNUMANodeIds(m.allDevices[resource][d].Topology)) {
return
}
numMatching++
}
// Finally, check to see if enough available devices remain on the
// current NUMA node combination to satisfy the device request.
for d := range available {
if mask.AnySet(m.getNUMANodeIds(m.allDevices[resource][d].Topology)) {
numMatching++
}
}
// 如果目前NUMA組合中可用的device比請求的device數還少,那麼直接傳回
if numMatching < request {
return
}
// 建立TopologyHint
hints = append(hints, topologymanager.TopologyHint{
NUMANodeAffinity: mask,
Preferred: false,
})
})
// 如果某個TopologyHint所涉及的NUMA數最少,那麼将該TopologyHint的Preferred設定為true
for i := range hints {
if hints[i].NUMANodeAffinity.Count() == minAffinitySize {
hints[i].Preferred = true
}
}
return hints
}
稍微總結一下:
- 對每個組合,檢查目前k8s節點上某種資源(比如GPU)可用的裝置數與該組合所涉及的該資源的裝置數的交集的個數是否大于容器申請的裝置數,如果比容器申請的裝置數小,那麼就不建立TopologyHint,否則就建立一個TopologyHint,并放入hints中。
- 檢查hints中所有的TopologyHint,如果該TopologyHint涉及到的NUMA節點數與minAffinitySize值相同,那麼将該TopologyHint的Preferred設定為true。
6.3 TopologyHint的merge操作
前面已經說到了CPU Manager和Device Manager會産生多組TopologyHint。那麼如何合并這些TopologyHint,找到最優的那個TopologyHint呢?來看看是怎樣實作的。
以下面這幅圖做說明,在這幅圖中總共有3個NUMA節點,對于某個容器而言,CPU Manager找出了CPU資源的一組TopologyHint,Device Manager找出了GPU和NIC的TopologyHint。整個merge流程如下:
- 從每一組資源類型中拿出一個TopologyHint組合成一個新的TopologyHint組合。
- 在這個新的TopologyHint組合内,尋找它們公共的NUMA節點。并且隻有當這個組合内所有的TopologyHint的Preferred域都為true時,合并後的TopologyHint的Preferred域才為True。
- 從合并後的TopologyHint中尋找最優的TopologyHint(即TopologyHint的Preferred域為True)。
前面提到過Topology Manager的四種政策,現在重點說一下四種政策中的後面三種:
- best-effort: 結合上圖來說,如果沒有找到最優的TopologyHint(即圖中的TH6),k8s節點也會接納這個Pod。
- restricted:結合上圖來說,如果沒有找到最優的TopologyHint(即圖中的TH6),那麼節點會拒絕接納這個Pod,如果Pod遭到節點拒絕,其狀态将變為Terminated。
- single-numa-node:結合上圖來說,如果沒有找到最優的TopologyHint(即圖中的TH6,并且NUMA節點個數為1),那麼節點會拒絕接納這個Pod,如果Pod遭到節點拒絕,其狀态将變為Terminated。
另外需要說明的是,在為容器配置設定相應的資源時,CPU Manager和Device Manager會優先考慮在最優的TopologyHint所涉及的NUMA節點上為容器配置設定資源,如果這些NUMA節點上的資源不夠,還會從其他NUMA節點上為容器配置設定。
6.4 何時會進行配置設定操作
也就是說這些HintProvider何時會執行其Allocate函數為容器配置設定資源?在Topology Manager中有一個Admit函數,會周遊所有的HintProvider,執行HintProvider的Allocate函數。而Topology Manager的Admit函數會在kubelet判斷一個pod是否被節點接納的時候執行(kubelet調用所有的PodAdmitHandler,隻要有一個PodAdmitHandler給出拒絕意見,那麼節點将不會接納該pod),因為Topology Manager也是一個PodAdmitHandler。