排程架構介紹
排程架構是面向 Kubernetes 排程器的一種插件架構, 它為現有的排程器添加了一組新的“插件” API。插件會被編譯到排程器之中。 這些 API 允許大多數排程功能以插件的形式實作,同時使排程“核心”保持簡單且可維護。
架構工作流程
排程架構定義了一些擴充點。排程器插件注冊後在一個或多個擴充點處被調用。 這些插件中的一些可以改變排程決策,而另一些僅用于提供資訊。每次排程一個 Pod 的嘗試都分為兩個階段,即 排程周期 和 綁定周期。
排程周期和綁定周期
排程周期為 Pod 選擇一個節點,綁定周期将該決策應用于叢集。 排程周期和綁定周期一起被稱為“排程上下文”。排程周期是串行運作的,而綁定周期可能是同時運作的。如果确定 Pod 不可排程或者存在内部錯誤,則可以終止排程周期或綁定周期。 Pod 将傳回隊列并重試。
擴充點
以下為可以可擴充的11個擴充點,需要在哪個階段自定義一些操作,就根據自己的需求實作哪個階段的擴充點即可
源碼在[email protected]/pkg/scheduler/framework/interface.go
//插件的名字,必須實作此接口
type Plugin interface {
Name() string
}
1. 隊列排序 QueueSort
//隊列排序插件用于對排程隊列中的 Pod 進行排序。 隊列排序插件本質上提供 less(Pod1, Pod2) 函數。 一次隻能啟動一個隊列插件。
type QueueSortPlugin interface {
Plugin
Less(*QueuedPodInfo, *QueuedPodInfo) bool
}
//比較pod的優先級
type LessFunc func(podInfo1, podInfo2 *QueuedPodInfo) bool
2. 前置過濾 PreFilter
//前置過濾插件用于預處理 Pod 的相關資訊,或者檢查叢集或 Pod 必須滿足的某些條件。 如果 PreFilter 插件傳回錯誤,則排程周期将終止。
type PreFilterPlugin interface {
Plugin
PreFilter(ctx context.Context, state *CycleState, p *v1.Pod) *Status
PreFilterExtensions() PreFilterExtensions
}
type EnqueueExtensions interface {
EventsToRegister() []ClusterEvent
}
type PreFilterExtensions interface {
AddPod(ctx context.Context, state *CycleState, podToSchedule *v1.Pod, podInfoToAdd *PodInfo, nodeInfo *NodeInfo) *Status
RemovePod(ctx context.Context, state *CycleState, podToSchedule *v1.Pod, podInfoToRemove *PodInfo, nodeInfo *NodeInfo) *Status
}
3. 過濾 Filter
//過濾插件用于過濾出不能運作該 Pod 的節點。對于每個節點, 排程器将按照其配置順序調用這些過濾插件。如果任何過濾插件将節點标記為不可行, 則不會為該節點調用剩下的過濾插件。節點可以被同時進行評估。
type FilterPlugin interface {
Plugin
Filter(ctx context.Context, state *CycleState, pod *v1.Pod, nodeInfo *NodeInfo) *Status
}
4. 後置過濾
//這些插件在篩選階段後調用,但僅在該 Pod 沒有可行的節點時調用。 插件按其配置的順序調用。
//如果任何後過濾器插件标記節點為“可排程”, 則其餘的插件不會調用。典型的後篩選實作是搶占,
//試圖通過搶占其他 Pod 的 資源使該 Pod 可以排程。
type PostFilterPlugin interface {
Plugin
PostFilter(ctx context.Context, state *CycleState, pod *v1.Pod, filteredNodeStatusMap NodeToStatusMap) (*PostFilterResult, *Status)
}
5. 前置評分
//前置評分插件用于執行 “前置評分” 工作,即生成一個可共享狀态供評分插件使用。 如果 PreScore 插件傳回錯誤,則排程周期将終止。
type PreScorePlugin interface {
Plugin
PreScore(ctx context.Context, state *CycleState, pod *v1.Pod, nodes []*v1.Node) *Status
}
6. 評分
//評分插件用于對通過過濾階段的節點進行排名。排程器将為每個節點調用每個評分插件。
//将有一個定義明确的整數範圍,代表最小和最大分數。
//在标準化評分階段之後,排程器将根據配置的插件權重 合并所有插件的節點分數。
type ScorePlugin interface {
Plugin
Score(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) (int64, *Status)
ScoreExtensions() ScoreExtensions
}
type ScoreExtensions interface {
NormalizeScore(ctx context.Context, state *CycleState, p *v1.Pod, scores NodeScoreList) *Status
}
7. Reserve
//這是一個資訊擴充點,當資源已經預留給 Pod 時,會通知插件。 管理運作時狀态的插件(也成為“有狀态插件”)應該使用此擴充點,以便 排程器在節點給指定 Pod 預留了資源時能夠通知該插件。 這是在排程器真正将 Pod 綁定到節點之前發生的,并且它存在是為了防止 在排程器等待綁定成功時發生競争情況。
//這個是排程周期的最後一步。 一旦 Pod 處于保留狀态,它将在綁定周期結束時觸發不保留 插件 (失敗時)或 綁定後 插件(成功時)。
type ReservePlugin interface {
Plugin
Reserve(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) *Status
Unreserve(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string)
}
8. PreBind
//這些插件在 Pod 綁定節點之前執行
type PreBindPlugin interface {
Plugin
PreBind(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) *Status
}
9. PostBind
//這是一個資訊擴充點,在 Pod 綁定了節點之後調用。
type PostBindPlugin interface {
Plugin
PostBind(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string)
}
10. Permit
// 這些插件可以阻止或延遲 Pod 綁定, 一個允許插件可以做以下三件事之一:
//準許:一旦所有 Permit 插件準許 Pod 後,該 Pod 将被發送以進行綁定。
//拒絕:如果任何 Permit 插件拒絕 Pod,則該 Pod 将被傳回到排程隊列。 這将觸發Unreserve 插件。
//等待(帶有逾時):如果一個 Permit 插件傳回 “等待” 結果,則 Pod 将保持在一個内部的 “等待中” 的 //Pod 清單,同時該 Pod 的綁定周期啟動時即直接阻塞直到得到 準許。如果逾時發生,等待 變成 拒絕,并且 Pod 将傳回排程隊列,進而觸發 Unreserve 插件。
type PermitPlugin interface {
Plugin
Permit(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) (*Status, time.Duration)
}
11. Bind
// 這個插件将 Pod 與節點綁定。綁定插件是按順序調用的,隻要有一個插件完成了綁定,其餘插件都會跳過。綁定插件至少需要一個。
type BindPlugin interface {
Plugin
Bind(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) *Status
}
官方有給出demo
https://github.com/kubernetes-sigs/scheduler-plugins
仿照官方的demo實作一個小功能,比如在排程的時候需要判斷将pod排程到label為gpu=true的節點
Demo
注冊
package main
import (
"k8s.io/kubernetes/cmd/kube-scheduler/app"
"math/rand"
"os"
my_scheduler "sche/my-scheduler"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
// 注冊,可以注冊多個
command := app.NewSchedulerCommand(
app.WithPlugin(my_scheduler.Name, my_scheduler.New),
// app.WithPlugin(my_scheduler2.Name, my_scheduler2.New),
)
if err := command.Execute(); err != nil {
os.Exit(1)
}
}
實作
package my_scheduler
import (
"context"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubernetes/pkg/scheduler/framework"
"log"
)
const Name = "sample"
type sample struct{}
// ide(goland)可以自動識别要實作的接口長啥樣,implement missing method會一鍵建立對應方法
var _ framework.FilterPlugin = &sample{}
var _ framework.PreScorePlugin = &sample{}
/**
根據k8s版本,此處可能有些許不同
經測試在1.19版本需要如此導入framework包
framework "k8s.io/kubernetes/pkg/scheduler/framework/v1alpha1"
New函數為
func New(_ *runtime.Object, _ framework.FrameworkHandle) (framework.Plugin, error) {
return &Sample{}, nil
}
*/
func New(_ runtime.Object, _ framework.Handle) (framework.Plugin, error) {
return &sample{}, nil
}
// 必須實作
func (pl *sample) Name() string {
return Name
}
func (pl *sample) Filter(ctx context.Context, state *framework.CycleState, pod *v1.Pod, node *framework.NodeInfo) *framework.Status {
log.Printf("filter pod: %v, node: %v", pod.Name, node)
// 如果節點的label不包含gpu=true,傳回不可排程
if node.Node().Labels["gpu"] != "true" {
return framework.NewStatus(framework.Unschedulable, "Node: "+node.Node().Name)
}
return framework.NewStatus(framework.Success, "Node: "+node.Node().Name)
}
func (pl *sample)PreScore(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodes []*v1.Node) *framework.Status {
// 沒做具體實作,此處隻為示範,隻做列印
log.Println(nodes)
log.Println(pod)
return framework.NewStatus(framework.Success, "Pod: "+pod.Name)
}
打包鏡像
FROM ubuntu:18.04
ADD sche /usr/bin/
RUN chmod a+x /usr/bin/sche
CMD ["sche"]
問題
k8s下載下傳依賴包的一些問題
直接go mod init xx
然後go mod tidy
會出現unknown revision v0.0.0問題,可用以下腳本解決
#!/bin/sh
set -euo pipefail
VERSION=${1#"v"}
if [ -z "$VERSION" ]; then
echo "Must specify version!"
exit 1
fi
MODS=($(
curl -sS https://raw.githubusercontent.com/kubernetes/kubernetes/v${VERSION}/go.mod |
sed -n 's|.*k8s.io/\(.*\) => ./staging/src/k8s.io/.*|k8s.io/\1|p'
))
for MOD in "${MODS[@]}"; do
V=$(
go mod download -json "${MOD}@kubernetes-${VERSION}" |
sed -n 's|.*"Version": "\(.*\)".*|\1|p'
)
go mod edit "-replace=${MOD}=${MOD}@${V}"
done
go get "k8s.io/[email protected]${VERSION}"
使用方法為:
go mod init xxx
bash mod.sh v1.19.2 // 假設上面的腳本儲存為mod.sh 需要的k8s版本為1.19.2
插件配置
配置多個排程器 | Kubernetes
排程器配置 | Kubernetes
叢集開啟了RBAC,是以需要給權限,然後通過configmap的方式将配置檔案傳給自定義的scheduler,使用一個deployment将自定義的scheduler運作起來
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: sample-scheduler-clusterrole
rules:
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- get
- update
- create
- apiGroups:
- ""
resources:
- endpoints
verbs:
- delete
- get
- patch
- update
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: sample-scheduler-sa
namespace: kube-system
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: sample-scheduler-clusterrolebinding
namespace: kube-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: sample-scheduler-clusterrole
subjects:
- kind: ServiceAccount
name: sample-scheduler-sa
namespace: kube-system
---
apiVersion: v1
kind: ConfigMap
metadata:
name: scheduler-config
namespace: kube-system
data:
scheduler-config.yaml: |
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
leaderElection:
leaderElect: false
clientConnection:
kubeconfig: "/etc/kubernetes/ssl/kubecfg-kube-scheduler.yaml"
profiles:
- schedulerName: sample-scheduler # 新的排程器的名字,随便取
plugins:
filter: # 表示在filter這個擴充點
enabled: # 開啟
- name: sample # sample這個插件定義的filter,同時還會執行預設的插件的filter
preScore:
enabled:
- name: sample
disabled: #禁用
- name: "*" # 表示禁用預設的preScore,隻使用sample的preScore,除了預設的排程器,其他擴充的排程器必須顯示enable才會執行,預設的排程器必須顯示disabled才會不執行
# 注意:所有配置檔案必須在 QueueSort 擴充點使用相同的插件,并具有相同的配置參數(如果适用)。 這是因為排程器隻有一個儲存 pending 狀态 Pod 的隊列。意思要麼用預設的queueSort不用擴充的queueSort,要麼用擴充的queueSort,把預設的queueSort禁用掉
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-scheduler
namespace: kube-system
labels:
component: sample-scheduler
spec:
replicas: 1
selector:
matchLabels:
component: sample-scheduler
template:
metadata:
labels:
component: sample-scheduler
spec:
hostNetwork: true
serviceAccount: sample-scheduler-sa
priorityClassName: system-cluster-critical
volumes:
- name: scheduler-config
configMap:
name: scheduler-config
- name: kubeconfigdir
hostPath:
path: /etc/kubernetes/ssl # 本地的scheduler-config檔案所在位置,在我的環境中該檔案名為kubecfg-kube-scheduler.yaml,挂載到deployment内供deployment用到的configmap使用
type: DirectoryOrCreate
containers:
- name: scheduler-ctrl
image: sche:v1
imagePullPolicy: IfNotPresent
args:
- /usr/bin/sche
- --config=/etc/kubernetes/scheduler-config.yaml # configmap挂載的檔案
- --v=3
resources:
requests:
cpu: "50m"
volumeMounts:
- name: scheduler-config
mountPath: /etc/kubernetes
- name: kubeconfigdir
mountPath: /etc/kubernetes/ssl
readOnly: true
測試
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-example
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
schedulerName: sample-scheduler # 使用自定義的scheduler,如果不指定則會使用預設的scheduler
containers:
- name: nginx
image: nginx
排程時可使用kubectl logs檢視sample-scheduler的日志
在未給節點打上gpu=true時,pod一直處于pending狀态,當打上标簽之後pod成功排程
未完成事項
官方文檔中有以下一段:
要在啟用了 leader 選舉的情況下運作多排程器,你必須執行以下操作:
首先,更新上述 Deployment YAML(my-scheduler.yaml)檔案中的以下字段:
- --leader-elect=true
- --lock-object-namespace=<lock-object-namespace>
- --lock-object-name=<lock-object-name>
配置之後pod無法排程,日志中立即列印Add event for unscheduled pod,隻有在scheduler-config.yaml中
leaderElection:
leaderElect: false
設定為false的時候能成功排程