天天看點

使用Scheduling Framework擴充kube-scheduler

排程架構介紹

        排程架構是面向 Kubernetes 排程器的一種插件架構, 它為現有的排程器添加了一組新的“插件” API。插件會被編譯到排程器之中。 這些 API 允許大多數排程功能以插件的形式實作,同時使排程“核心”保持簡單且可維護。

架構工作流程

        排程架構定義了一些擴充點。排程器插件注冊後在一個或多個擴充點處被調用。 這些插件中的一些可以改變排程決策,而另一些僅用于提供資訊。每次排程一個 Pod 的嘗試都分為兩個階段,即 排程周期 和 綁定周期。

排程周期和綁定周期

        排程周期為 Pod 選擇一個節點,綁定周期将該決策應用于叢集。 排程周期和綁定周期一起被稱為“排程上下文”。排程周期是串行運作的,而綁定周期可能是同時運作的。如果确定 Pod 不可排程或者存在内部錯誤,則可以終止排程周期或綁定周期。 Pod 将傳回隊列并重試。

擴充點

        以下為可以可擴充的11個擴充點,需要在哪個階段自定義一些操作,就根據自己的需求實作哪個階段的擴充點即可

使用Scheduling Framework擴充kube-scheduler

源碼在[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的時候能成功排程

繼續閱讀