前言
排程器配置中解析了kube-scheduler的各種配置,但是自始至終都沒有看到如何根據配置構造排程器。雖然排程器文章中提到了構造函數,但是其核心實作是Configurator,本文将解析Configurator構造排程器的過程。雖然意義遠沒有排程隊列、排程架構、排程插件等重大,但是對于了解排程器從生到死整個過程來說是必要一環,況且确實能學到點東西。
Configurator
Configurator定義
Configurator類似于Scheduler的工廠,但是一般的工廠類很少會有這麼多的配置,是以Configurator這個名字比Factroy更合适。源碼連結:https://github.com/kubernetes/kubernetes/blob/release-1.21/pkg/scheduler/factory.go#L59
// Configurator根據配置構造排程器
type Configurator struct {
// 因為kube-scheduler需要通路apiserver,是以clientset.Interface是必須的。
client clientset.Interface
recorderFactory profile.RecorderFactory
// informerFactory和client都是用來通路apiserver,informerFactory用來讀API對象,client用于寫API對象(比如綁定)
informerFactory informers.SharedInformerFactory
// 這個關閉信号,用chan實作是非常普遍的做法
StopEverything <-chan struct{}
// 排程緩存,排程器必須的子產品,這可以證明排程緩存不是Scheduler構造的,而是通過Configurator傳給Scheduler
schedulerCache internalcache.Cache
// 是否運作所有的FilterPlugin,即便中間某個插件傳回失敗。
// 了解排程插件的讀者都知道,有任何過濾插件傳回失敗,表示Pod不可排程,所有的過濾插件是與的關系。
// 是以排序靠前的過濾插件傳回失敗理論上是無需再用後面的插件過濾了,這個配置就是是否運作所有的過濾插件。
alwaysCheckAllPredicates bool
// 這幾個配置參數已經在排程器配置、排程架構、排程隊列等文章中詳細解釋過了,Configurator隻需要透明傳遞就好了
percentageOfNodesToScore int32
podInitialBackoffSeconds int64
podMaxBackoffSeconds int64
// 每個KubeSchedulerProfile對應一個排程架構(Framework)的配置,是以Configurator需要根據配置構造Framework
profiles []schedulerapi.KubeSchedulerProfile
// 排程插件工廠系統資料庫,即根據排程插件名字與排程插件工廠的映射。
// 因為Configurator根據配置為Framework建立插件,是以需要根據插件名字擷取排程插件工廠來構造插件
registry frameworkruntime.Registry
// 排程緩存快照,熟悉排程緩存的讀者應該知道,Scheduler每次排程之前都會更新排程緩存快照,排程緩存隻需要将更新與快照diff的部分即可。
// 是以排程緩存快照是和Scheduler一起建立,生命周期與排程器是相同的,由Configurator建立再傳遞給Scheduler。
nodeInfoSnapshot *internalcache.Snapshot
// 排程擴充程式配置,Configurator需要根據配置構造排程擴充程式并傳遞給Scheduler
extenders []schedulerapi.Extender
// frameworkCapturer是回調函數,用來捕獲每最終的KubeSchedulerProfile,目的是什麼?
// 因為輸入的KubeSchedulerProfile裡包含有使能和禁止的插件配置,而Configurator構造Scheduler時會合并出最終的KubeSchedulerProfile。
// 如果需要擷取最終的KubeSchedulerProfile怎麼辦?就可以通過FrameworkCapturer捕獲。
frameworkCapturer FrameworkCapturer
// 用來配置最大并行度,這個參數再排程器配置中有詳細說明,說的簡單點就是配置最大協程的數量。
parallellism int32
}
create
直接進入Configurator最核心的函數,就是更具配置構造Scheduler,源碼連結:https://github.com/kubernetes/kubernetes/blob/release-1.21/pkg/scheduler/factory.go#L90
// create()建立Scheduler對象
func (c *Configurator) create() (*Scheduler, error) {
var extenders []framework.Extender
var ignoredExtendedResources []string
// 是否配置了Extender?
if len(c.extenders) != 0 {
var ignorableExtenders []framework.Extender
// 周遊所有的Extender配置
for ii := range c.extenders {
// 根據配置構造Extender(目前隻有HTTPExtender實作)
klog.V(2).InfoS("Creating extender", "extender", c.extenders[ii])
extender, err := core.NewHTTPExtender(&c.extenders[ii])
if err != nil {
return nil, err
}
// 根據Extender'是否可以被忽略'将Extender分為兩個集合:extenders和ignorableExtenders
if !extender.IsIgnorable() {
extenders = append(extenders, extender)
} else {
ignorableExtenders = append(ignorableExtenders, extender)
}
// 周遊Extender管理的資源,統計Scheduler可以忽略的資源
for _, r := range c.extenders[ii].ManagedResources {
if r.IgnoredByScheduler {
ignoredExtendedResources = append(ignoredExtendedResources, r.Name)
}
}
}
// 最後将ignorableExtenders追加到extenders尾部,這樣做的目的筆者在解析Extender的文章中有說明。
// 此處簡單描述一下:在周遊Extender排程Pod時,因為二者存在明确的分界線,當調用錯誤時可以根據目前Extender是否可忽略選擇傳回錯誤(否)或者成功(是)
extenders = append(extenders, ignorableExtenders...)
}
// 如果Extender中有任何資源需要Scheduler忽略掉,需要通過追加到每個Profile的PluginConfig一個忽略種資源的參數。
// 這僅對v1beta1有效,可以在配置Extender和插件參數,對于較早的版本,不允許同時使用政策(Policy)和自定義插件配置。
// 這非常有意思,沒想到還可以這麼玩兒,這樣在接下來構造排程插件的時候就可以把忽略的資源通過參數告知排程插件。
// 筆者在解析排程插件的文章中提到了noderesources.Fit這個插件,專門負責資源比對的插件,是以忽略哪些資源應該告知這個插件。
if len(ignoredExtendedResources) > 0 {
for i := range c.profiles {
prof := &c.profiles[i]
pc := schedulerapi.PluginConfig{
Name: noderesources.FitName,
Args: &schedulerapi.NodeResourcesFitArgs{
IgnoredResources: ignoredExtendedResources,
},
}
prof.PluginConfig = append(prof.PluginConfig, pc)
}
}
// 構造PodNominator,所有的排程架構(Framework)共享使用。
// 不可能每個Framework獨享一個PodNominator,這會導緻Pod1提名了Node1而在排程Pod2時全然不知,因為Pod1和Pod2可能使用不同的排程架構。
nominator := internalqueue.NewPodNominator()
// ClusterEvent抽象了系統資源狀态是如何更改的,比如Pod的Added,感興趣的讀者可以看看ClusterEvent的定義。
// clusterEventMap是系統資源狀态的更改到插件名字集合的映射,說白了就是有哪些插件更改了資源,至于有什麼用讀者自己研究吧~
clusterEventMap := make(map[framework.ClusterEvent]sets.String)
// 根據[]KubeSchedulerProfile構造map[string]Framework(map查找更快),傳入了排程架構和排程插件需要的參數。
// 如何根據KubeSchedulerProfile構造Framework,讀者可以自己看一下,相對比較簡單,筆者此處不再注釋了。
profiles, err := profile.NewMap(c.profiles, c.registry, c.recorderFactory,
frameworkruntime.WithClientSet(c.client),
frameworkruntime.WithInformerFactory(c.informerFactory),
frameworkruntime.WithSnapshotSharedLister(c.nodeInfoSnapshot),
frameworkruntime.WithRunAllFilters(c.alwaysCheckAllPredicates),
frameworkruntime.WithPodNominator(nominator),
frameworkruntime.WithCaptureProfile(frameworkruntime.CaptureProfile(c.frameworkCapturer)),
frameworkruntime.WithClusterEventMap(clusterEventMap),
frameworkruntime.WithParallelism(int(c.parallellism)),
)
if err != nil {
return nil, fmt.Errorf("initializing profiles: %v", err)
}
if len(profiles) == 0 {
return nil, errors.New("at least one profile is required")
}
// 此處需要注意了,使用第一個Profile的Q�ueueSortPlugin的排序函數構造排程隊列。
// 我們知道排程隊列隻有一個,Shecuduler不會為每個排程架構建立一個排程隊列,這就強行要求所有Profile必須配置同一個QueueSortPlugin。
// 這一點筆者在解析排程器配置的文章中已經提到了,但是并沒有從代碼層面給出證明,此處算是證明了這一點。
lessFn := profiles[c.profiles[0].SchedulerName].QueueSortFunc()
podQueue := internalqueue.NewSchedulingQueue(
lessFn,
c.informerFactory,
internalqueue.WithPodInitialBackoffDuration(time.Duration(c.podInitialBackoffSeconds)*time.Second),
internalqueue.WithPodMaxBackoffDuration(time.Duration(c.podMaxBackoffSeconds)*time.Second),
internalqueue.WithPodNominator(nominator),
internalqueue.WithClusterEventMap(clusterEventMap),
)
// 與核心内容關系不大,本文忽略。
debugger := cachedebugger.New(
c.informerFactory.Core().V1().Nodes().Lister(),
c.informerFactory.Core().V1().Pods().Lister(),
c.schedulerCache,
podQueue,
)
debugger.ListenForSignal(c.StopEverything)
// 構造排程算法,筆者在解析排程算法的文章中提到了,排程算法的唯一實作就是genericScheduler
algo := core.NewGenericScheduler(
c.schedulerCache,
c.nodeInfoSnapshot,
extenders,
c.percentageOfNodesToScore,
)
// 最終傳回Scheduler對象
return &Scheduler{
SchedulerCache: c.schedulerCache,
Algorithm: algo,
Profiles: profiles,
// 注入擷取下一個待排程Pod的函數
NextPod: internalqueue.MakeNextPodFunc(podQueue),
// 注入排程Pod錯誤的處理函數
Error: MakeDefaultErrorFunc(c.client, c.informerFactory.Core().V1().Pods().Lister(), podQueue, c.schedulerCache),
StopEverything: c.StopEverything,
SchedulingQueue: podQueue,
}, nil
}
雖然create()函數可以構造Scheduler,但是有沒有發現缺少預設的插件配置?也就是說,create()直接利用Profile構造Framework,那麼使用者配置的Profile是怎麼與預設的合并的呢?接下來的章節解析Configurator是如何生成預設配置并合并自定義配置。
createFromProvider
kube-scheduler通過算法源擷取插件的預設配置,算法源分為Provider和Policy兩種,本章節介紹Provider,源碼連結:https://github.com/kubernetes/kubernetes/blob/release-1.21/pkg/scheduler/factory.go#L193
// createFromProvider()根據名字找到已注冊的算法提供者以此構造Scheduler。
func (c *Configurator) createFromProvider(providerName string) (*Scheduler, error) {
klog.V(2).InfoS("Creating scheduler from algorithm provider", "algorithmProvider", providerName)
// NewRegistry()傳回了預設的插件配置(筆者在在解析排程插件的文章中介紹了預設插件配置)。
// 感興趣的讀者可以了解一下這個函數,代碼量不多,如果不了解的讀者也沒關系,就簡單認為隻有一種預設插件配置就可以了。
r := algorithmprovider.NewRegistry()
defaultPlugins, exist := r[providerName]
if !exist {
return nil, fmt.Errorf("algorithm provider %q is not registered", providerName)
}
// 周遊所有的Profile.
for i := range c.profiles {
prof := &c.profiles[i]
plugins := &schedulerapi.Plugins{}
// 在預設插件配置基礎上應用使用者的自定義配置,即使能新的插件或者禁止某些預設插件,相關函數讀者自己檢視即可。
plugins.Append(defaultPlugins)
plugins.Apply(prof.Plugins)
// 将最終的插件配置更新到Profile中用于建立Framework,這個在前面已經提到了。
prof.Plugins = plugins
}
// 建立Scheduler
return c.create()
}
createFromProvider()就是利用預設的插件配置再合并使用者自定義插件配置形成最終的插件配置,以此來建立Scheduler。有沒有發現該方法是靜态配置的,也就是已注冊的Provider都是寫在代碼裡的。在沒有KubeSchedulerProfile這一功能之前,如果需要多種不同的Provider隻能在代碼裡注冊多個Provider,這明顯是非常不友好的設計,并且無法為插件配置自定義參數,是以就有了基于政策(Policy)的插件配置。
createFromConfig
基于政策(Policy)的插件配置可以通過配置檔案、ConfigMap配置插件,這在KubeSchedulerProfile出來之間很好用,無奈KubeSchedulerProfile後來一同江湖了。源碼連結:https://github.com/kubernetes/kubernetes/blob/release-1.21/pkg/scheduler/factory.go#L213
// 從配置檔案建立Scheduler,僅在v1alpha1可使用。新版本已經不推薦使用算法院(包括Provider和Policy),改用KubeSchedulerProfile實作。
// 是以筆者不會對基于Policy建構Scheduler做詳細解析,因為相對比較複雜且意義不大,隻是簡單介紹一下,感興趣的讀者可以自己詳細看看。
func (c *Configurator) createFromConfig(policy schedulerapi.Policy) (*Scheduler, error) {
// NewLegacyRegistry()建立基于政策的預設插件系統資料庫,這一點與algorithmprovider.NewRegistry()功能類似。
lr := frameworkplugins.NewLegacyRegistry()
args := &frameworkplugins.ConfigProducerArgs{}
klog.V(2).InfoS("Creating scheduler from configuration", "policy", policy)
// 驗證政策配置的有效性
if err := validation.ValidatePolicy(policy); err != nil {
return nil, err
}
// 根據政策配置找到所有已注冊的Predicate的插件名字,Predicate在對應于目前的Filter
predicateKeys := sets.NewString()
if policy.Predicates == nil {
klog.V(2).InfoS("Using predicates from algorithm provider", "algorithmProvider", schedulerapi.SchedulerDefaultProviderName)
predicateKeys = lr.DefaultPredicates
} else {
for _, predicate := range policy.Predicates {
klog.V(2).InfoS("Registering predicate", "predicate", predicate.Name)
predicateName, err := lr.ProcessPredicatePolicy(predicate, args)
if err != nil {
return nil, err
}
predicateKeys.Insert(predicateName)
}
}
// 根據政策配置找到所有已注冊的Priority的插件名字,Priority在對應于目前的Score
priorityKeys := make(map[string]int64)
if policy.Priorities == nil {
klog.V(2).InfoS("Using default priorities")
priorityKeys = lr.DefaultPriorities
} else {
for _, priority := range policy.Priorities {
if priority.Name == frameworkplugins.EqualPriority {
klog.V(2).InfoS("Skip registering priority", "priority", priority.Name)
continue
}
klog.V(2).InfoS("Registering priority", "priority", priority.Name)
priorityName, err := lr.ProcessPriorityPolicy(priority, args)
if err != nil {
return nil, err
}
priorityKeys[priorityName] = priority.Weight
}
}
// 設定Pod硬親和權重參數
if policy.HardPodAffinitySymmetricWeight != 0 {
args.InterPodAffinityArgs = &schedulerapi.InterPodAffinityArgs{
HardPodAffinityWeight: policy.HardPodAffinitySymmetricWeight,
}
}
if policy.AlwaysCheckAllPredicates {
c.alwaysCheckAllPredicates = policy.AlwaysCheckAllPredicates
}
klog.V(2).InfoS("Creating scheduler", "predicates", predicateKeys, "priorities", priorityKeys)
// 因為在Policy中沒有隊列排序、搶占、綁定相關的配置,這些都用預設的插件
plugins := schedulerapi.Plugins{
QueueSort: schedulerapi.PluginSet{
Enabled: []schedulerapi.Plugin{{Name: queuesort.Name}},
},
PostFilter: schedulerapi.PluginSet{
Enabled: []schedulerapi.Plugin{{Name: defaultpreemption.Name}},
},
Bind: schedulerapi.PluginSet{
Enabled: []schedulerapi.Plugin{{Name: defaultbinder.Name}},
},
}
// 基于政策配置生産插件配置
var pluginConfig []schedulerapi.PluginConfig
var err error
if plugins, pluginConfig, err = lr.AppendPredicateConfigs(predicateKeys, args, plugins, pluginConfig); err != nil {
return nil, err
}
if plugins, pluginConfig, err = lr.AppendPriorityConfigs(priorityKeys, args, plugins, pluginConfig); err != nil {
return nil, err
}
if pluginConfig, err = dedupPluginConfigs(pluginConfig); err != nil {
return nil, err
}
// 以下就和Provider一樣了
for i := range c.profiles {
prof := &c.profiles[i]
prof.Plugins = &schedulerapi.Plugins{}
prof.Plugins.Append(&plugins)
prof.PluginConfig = pluginConfig
}
return c.create()
}
基于政策的配置無需修改代碼可以動态的調整插件配置,也可以配置插件參數。并且基于政策配置方法多樣化,支援ConfigMap和檔案。這些都是比Provider友好的地方,但是由于排程架構的更新,原有Predicates/Priority的設計已經無法适應,使得相關的配置也被淘汰。不得不說,基于政策的配置有它的先進性,而且從KubeSchedulerProfile的設計來看也有借鑒它的地方,比如插件參數。
總結
- 無論是基于Provider還是Policy,都是配置排程算法的方法,前者是靜态的,後者是動态,都稱之為算法源;
- 在KubeSchedulerProfile出來之前,算法源是配置插件的唯一方法,Provider通過代碼靜态編譯不是很友好,Policy雖然不用修改代碼,可以用ConfigMap和檔案配置,但是依然無法适應多Scheduler的需求;
- 算法源配置已經不推薦使用了,改用KubeSchedulerProfile方法,Configurator将算法源作為預設的插件配置,在此基礎上合并使用者自定的配置生成最終的[]KubeSchedulerProfile;
- 既然不推薦使用算法源,但是從Configurator隻有createFromProvider()和createFromConfig()兩種建立Scheduler的接口,Scheduler構造函數選擇的是createFromProvider(),預設配置了"DefaultProvider",這就是KubeSchedulerProfile所基于的預設配置。