巧用Prometheus來擴充套件kubernetes排程器

2022-08-08 09:02:13

Overview

本文將深入講解 如何擴充套件 Kubernetes scheduler 中各個擴充套件點如何使用,與擴充套件scheduler的原理,這些是作為擴充套件 scheduler 的所需的知識點。最後會完成一個實驗,記錄網路流量的排程器。

kubernetes排程設定

kubernetes叢集中允許執行多個不同的 scheduler ,也可以為Pod指定不同的排程器進行排程。在一般的Kubernetes排程教學中並沒有提到這點,這也就是說,對於親和性,汙點等策略實際上並沒有完全的使用kubernetes排程功能,在之前的文章中提到的一些排程外掛,如基於埠佔用的排程 NodePorts 等策略一般情況下是沒有使用到的,本章節就是對這部分內容進行講解,這也是作為擴充套件排程器的一個基礎。

Scheduler Configuration [1]

kube-scheduler 提供了組態檔的資源,作為給 kube-scheduler 的組態檔,啟動時通過 --config= 來指定檔案。目前各個kubernetes版本中使用的 KubeSchedulerConfiguration 為,

  • 1.21 之前版本使用 v1beta1
  • 1.22 版本使用 v1beta2 ,但保留了 v1beta1
  • 1.23, 1.24, 1.25 版本使用 v1beta3 ,但保留了 v1beta2,刪除了 v1beta1

下面是一個簡單的 kubeSchedulerConfiguration 範例,其中 kubeconfig 與啟動引數 --kubeconfig 是相同的功效。而 kubeSchedulerConfiguration 與其他元件的組態檔類似,如 kubeletConfiguration 都是作為服務啟動的組態檔。

apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: /etc/srv/kubernetes/kube-scheduler/kubeconfig

Notes: --kubeconfig--config 是不可以同時指定的,指定了 --config 則其他引數自然失效 [2]

kubeSchedulerConfiguration使用

通過組態檔,使用者可以自定義多個排程器,以及設定每個階段的擴充套件點。而外掛就是通過這些擴充套件點來提供在整個排程上下文中的排程行為。

下面設定是對於設定擴充套件點的部分的一個範例,關於擴充套件點的講解可以參考kubernetes官方檔案排程上下文部分

apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
profiles:
  - plugins:
      score:
        disabled:
        - name: PodTopologySpread
        enabled:
        - name: MyCustomPluginA
          weight: 2
        - name: MyCustomPluginB
          weight: 1

Notes: 如果name="*" 的話,這種情況下將禁用/啟用對應擴充套件點的所有外掛

既然kubernetes提供了多排程器,那麼對於組態檔來說自然支援多個組態檔,profile也是列表形式,只要指定多個設定列表即可,下面是多組態檔範例,其中,如果存在多個擴充套件點,也可以為每個排程器設定多個擴充套件點。

apiVersion: kubescheduler.config.k8s.io/v1beta2
kind: KubeSchedulerConfiguration
profiles:
  - schedulerName: default-scheduler
  	plugins:
      preScore:
        disabled:
        - name: '*'
      score:
        disabled:
        - name: '*'
  - schedulerName: no-scoring-scheduler
    plugins:
      preScore:
        disabled:
        - name: '*'
      score:
        disabled:
        - name: '*'

scheduler排程外掛 [3]

kube-scheduler 預設提供了很多外掛作為排程方法,預設不設定的情況下會啟用這些外掛,如:

  • ImageLocality:排程將更偏向於Node存在容器映象的節點。擴充套件點:score.
  • TaintToleration:實現汙點與容忍度功能。擴充套件點:filter, preScore, score.
  • NodeName:實現排程策略中最簡單的排程方法 NodeName 的實現。擴充套件點:filter.
  • NodePorts:排程將檢查Node埠是否已佔用。擴充套件點:preFilter, filter.
  • NodeAffinity:提供節點親和性相關功能。擴充套件點:filter, score.
  • PodTopologySpread:實現Pod拓撲域的功能。擴充套件點:preFilter, filter, preScore, score.
  • NodeResourcesFit:該外掛將檢查節點是否擁有 Pod 請求的所有資源。使用以下三種策略之一:LeastAllocated (預設)MostAllocatedRequestedToCapacityRatio。擴充套件點:preFilter, filter, score.
  • VolumeBinding:檢查節點是否有或是否可以繫結請求的 . 擴充套件點:preFilter, filter, reserve, preBind, score.
  • VolumeRestrictions:檢查安裝在節點中的卷是否滿足特定於卷提供程式的限制。擴充套件點:filter.
  • VolumeZone:檢查請求的卷是否滿足它們可能具有的任何區域要求。擴充套件點:filter.
  • InterPodAffinity: 實現Pod 間的親和性與反親和性的功能。擴充套件點:preFilter, filter, preScore, score.
  • PrioritySort:提供基於預設優先順序的排序。擴充套件點:queueSort.

對於更多組態檔使用案例可以參考官方給出的檔案

如何擴充套件kube-scheduler [4]

當在第一次考慮編寫排程程式時,通常會認為擴充套件 kube-scheduler 是一件非常困難的事情,其實這些事情 kubernetes 官方早就想到了,kubernetes為此在 1.15 版本引入了framework的概念,framework旨在使 scheduler 更具有擴充套件性。

framework 通過重新定義 各擴充套件點,將其作為 plugins 來使用,並且支援使用者註冊 out of tree 的擴充套件,使其可以被註冊到 kube-scheduler 中,下面將對這些步驟進行分析。

定義入口

scheduler 允許進行自定義,但是對於只需要參照對應的 NewSchedulerCommand,並且實現自己的 plugins 的邏輯即可。

import (
    scheduler "k8s.io/kubernetes/cmd/kube-scheduler/app"
)

func main() {
    command := scheduler.NewSchedulerCommand(
            scheduler.WithPlugin("example-plugin1", ExamplePlugin1),
            scheduler.WithPlugin("example-plugin2", ExamplePlugin2))
    if err := command.Execute(); err != nil {
        fmt.Fprintf(os.Stderr, "%v\n", err)
        os.Exit(1)
    }
}

NewSchedulerCommand 允許注入 out of tree plugins,也就是注入外部的自定義 plugins,這種情況下就無需通過修改原始碼方式去定義一個排程器,而僅僅通過自行實現即可完成一個自定義排程器。

// WithPlugin 用於注入out of tree plugins 因此scheduler程式碼中沒有其參照。
func WithPlugin(name string, factory runtime.PluginFactory) Option {
	return func(registry runtime.Registry) error {
		return registry.Register(name, factory)
	}
}

外掛實現

對於外掛的實現僅僅需要實現對應的擴充套件點介面。下面通過內建外掛進行分析

對於內建外掛 NodeAffinity ,我們通過觀察他的結構可以發現,實現外掛就是實現對應的擴充套件點抽象 interface 即可。

定義外掛結構體

其中 framework.FrameworkHandle 是提供了Kubernetes API與 scheduler 之間呼叫使用的,通過結構可以看出包含 lister,informer等等,這個引數也是必須要實現的。

type NodeAffinity struct {
	handle framework.FrameworkHandle
}

實現對應的擴充套件點

func (pl *NodeAffinity) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
	nodeInfo, err := pl.handle.SnapshotSharedLister().NodeInfos().Get(nodeName)
	if err != nil {
		return 0, framework.NewStatus(framework.Error, fmt.Sprintf("getting node %q from Snapshot: %v", nodeName, err))
	}

	node := nodeInfo.Node()
	if node == nil {
		return 0, framework.NewStatus(framework.Error, fmt.Sprintf("getting node %q from Snapshot: %v", nodeName, err))
	}

	affinity := pod.Spec.Affinity

	var count int64
	// A nil element of PreferredDuringSchedulingIgnoredDuringExecution matches no objects.
	// An element of PreferredDuringSchedulingIgnoredDuringExecution that refers to an
	// empty PreferredSchedulingTerm matches all objects.
	if affinity != nil && affinity.NodeAffinity != nil && affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution != nil {
		// Match PreferredDuringSchedulingIgnoredDuringExecution term by term.
		for i := range affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution {
			preferredSchedulingTerm := &affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution[i]
			if preferredSchedulingTerm.Weight == 0 {
				continue
			}

			// TODO: Avoid computing it for all nodes if this becomes a performance problem.
			nodeSelector, err := v1helper.NodeSelectorRequirementsAsSelector(preferredSchedulingTerm.Preference.MatchExpressions)
			if err != nil {
				return 0, framework.NewStatus(framework.Error, err.Error())
			}

			if nodeSelector.Matches(labels.Set(node.Labels)) {
				count += int64(preferredSchedulingTerm.Weight)
			}
		}
	}

	return count, nil
}

最後在通過實現一個 New 函數來提供註冊這個擴充套件的方法。通過這個 New 函數可以在 main.go 中將其作為 out of tree plugins 注入到 scheduler 中即可

// New initializes a new plugin and returns it.
func New(_ runtime.Object, h framework.FrameworkHandle) (framework.Plugin, error) {
	return &NodeAffinity{handle: h}, nil
}

實驗:基於網路流量的排程 [7]

通過上面閱讀了解到了如何擴充套件 scheduler 外掛,下面實驗將完成一個基於流量的排程,通常情況下,網路一個Node在一段時間內使用的網路流量也是作為生產環境中很常見的情況。例如在設定均衡的多個主機中,主機A作為業務拉單指令碼執行,主機B作為尋常服務執行。因為拉單需要下載大量資料,而硬體資源佔用的卻很少,此時,如果有Pod被排程到該節點上,那麼可能雙方業務都會收到影響(前端代理覺得這個節點連線數少會被大量排程,而拉單指令碼因為網路頻寬的佔用降低了效能)。

實驗環境

  • 一個kubernetes叢集,至少保證有兩個節點。
  • 提供的kubernetes叢集都需要安裝prometheus node_exporter,可以是叢集內部的,也可以是叢集外部的,這裡使用的是叢集外部的。
  • promQLclient_golang 有所瞭解

實驗大致分為以下幾個步驟

  • 定義外掛API
    • 外掛命名為 NetworkTraffic
  • 定義擴充套件點
    • 這裡使用了 Score 擴充套件點,並且定義評分的演演算法
  • 定義分數獲取途徑(從prometheus指標中拿到對應的資料)
  • 定義對自定義排程器的引數傳入
  • 將專案部署到叢集中(叢集內部署與叢集外部署)
  • 實驗的結果驗證

實驗將仿照內建外掛 nodeaffinity 完成程式碼編寫,為什麼選擇這個外掛,只是因為這個外掛相對比較簡單,並且與我們實驗目的基本相同,其實其他外掛也是同樣的效果。

整個實驗的程式碼上傳至 github.com/CylonChau/customScheduler

實驗開始

錯誤處理

在初始化專案時,go mod tidy 等操作時,會遇到大量下面的錯誤

go: github.com/GoogleCloudPlatform/[email protected] requires
        k8s.io/[email protected]: reading k8s.io/apiextensions-apiserver/go.mod at revision v0.0.0: unknown revision v0.0.0

kubernetes issue #79384 [5] 中有提到這個問題,粗略瀏覽下沒有說明為什麼會出現這個問題,在最下方有個大佬提供了一個指令碼,出現上述問題無法解決時直接執行該指令碼後正常。

#!/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/kubernetes@v${VERSION}"

定義外掛API

通過上面內容描述瞭解到了定義外掛只需要實現對應的擴充套件點抽象 interface ,那麼可以初始化專案目錄 pkg/networtraffic/networktraffice.go

定義外掛名稱與變數

const Name = "NetworkTraffic"
var _ = framework.ScorePlugin(&NetworkTraffic{})

定義外掛的結構體

type NetworkTraffic struct {
    // 這個作為後面獲取node網路流量使用
	prometheus *PrometheusHandle
	// FrameworkHandle 提供外掛可以使用的資料和一些工具。
	// 它在外掛初始化時傳遞給 plugin 工廠類。
	// plugin 必須儲存和使用這個handle來呼叫framework函數。
	handle framework.FrameworkHandle
}

定義擴充套件點

因為選用 Score 擴充套件點,需要定義對應的方法,來實現對應的抽象

func (n *NetworkTraffic) Score(ctx context.Context, state *framework.CycleState, p *corev1.Pod, nodeName string) (int64, *framework.Status) {
    // 通過promethes拿到一段時間的node的網路使用情況
	nodeBandwidth, err := n.prometheus.GetGauge(nodeName)
	if err != nil {
		return 0, framework.NewStatus(framework.Error, fmt.Sprintf("error getting node bandwidth measure: %s", err))
	}
	bandWidth := int64(nodeBandwidth.Value)
	klog.Infof("[NetworkTraffic] node '%s' bandwidth: %s", nodeName, bandWidth)
	return bandWidth, nil // 這裡直接返回就行
}

接下來需要對結果歸一化,這裡就回到了排程框架中擴充套件點的執行問題上了,通過原始碼可以看出,Score 擴充套件點需要實現的並不只是這單一的方法。

// Run NormalizeScore method for each ScorePlugin in parallel.
parallelize.Until(ctx, len(f.scorePlugins), func(index int) {
    pl := f.scorePlugins[index]
    nodeScoreList := pluginToNodeScores[pl.Name()]
    if pl.ScoreExtensions() == nil {
        return
    }
    status := f.runScoreExtension(ctx, pl, state, pod, nodeScoreList)
    if !status.IsSuccess() {
        err := fmt.Errorf("normalize score plugin %q failed with error %v", pl.Name(), status.Message())
        errCh.SendErrorWithCancel(err, cancel)
        return
    }
})

通過上面程式碼瞭解到,實現 Score 就必須實現 ScoreExtensions,如果沒有實現則直接返回。而根據 nodeaffinity 中範例發現這個方法僅僅返回的是這個擴充套件點物件本身,而具體的歸一化也就是真正進行打分的操作在 NormalizeScore 中。

// NormalizeScore invoked after scoring all nodes.
func (pl *NodeAffinity) NormalizeScore(ctx context.Context, state *framework.CycleState, pod *v1.Pod, scores framework.NodeScoreList) *framework.Status {
	return pluginhelper.DefaultNormalizeScore(framework.MaxNodeScore, false, scores)
}

// ScoreExtensions of the Score plugin.
func (pl *NodeAffinity) ScoreExtensions() framework.ScoreExtensions {
	return pl
}

而在排程框架中,真正執行的操作的方法也是 NormalizeScore()

func (f *frameworkImpl) runScoreExtension(ctx context.Context, pl framework.ScorePlugin, state *framework.CycleState, pod *v1.Pod, nodeScoreList framework.NodeScoreList) *framework.Status {
	if !state.ShouldRecordPluginMetrics() {
		return pl.ScoreExtensions().NormalizeScore(ctx, state, pod, nodeScoreList)
	}
	startTime := time.Now()
	status := pl.ScoreExtensions().NormalizeScore(ctx, state, pod, nodeScoreList)
	f.metricsRecorder.observePluginDurationAsync(scoreExtensionNormalize, pl.Name(), status, metrics.SinceInSeconds(startTime))
	return status
}

下面來實現對應的方法

NormalizeScore 中需要實現具體的選擇node的演演算法,因為對node打分結果的區間為 \([0,100]\) ,所以這裡實現的演演算法公式將為 \(最高分 - (當前頻寬 / 最高最高頻寬 * 100)\),這樣就保證了,頻寬佔用越大的機器,分數越低。

例如,最高頻寬為200000,而當前Node頻寬為140000,那麼這個Node分數為:\(max - \frac{140000}{200000}\times 100 = 100 - (0.7\times100)=30\)

// 如果返回framework.ScoreExtensions 就需要實現framework.ScoreExtensions
func (n *NetworkTraffic) ScoreExtensions() framework.ScoreExtensions {
	return n
}

// NormalizeScore與ScoreExtensions是固定格式
func (n *NetworkTraffic) NormalizeScore(ctx context.Context, state *framework.CycleState, pod *corev1.Pod, scores framework.NodeScoreList) *framework.Status {
	var higherScore int64
	for _, node := range scores {
		if higherScore < node.Score {
			higherScore = node.Score
		}
	}
	// 計算公式為,滿分 - (當前頻寬 / 最高最高頻寬 * 100)
	// 公式的計算結果為,頻寬佔用越大的機器,分數越低
	for i, node := range scores {
		scores[i].Score = framework.MaxNodeScore - (node.Score * 100 / higherScore)
		klog.Infof("[NetworkTraffic] Nodes final score: %v", scores)
	}

	klog.Infof("[NetworkTraffic] Nodes final score: %v", scores)
	return nil
}

Notes:在kubernetes中最大的node數支援5000個,豈不是在獲取最大分數時迴圈就佔用了大量的效能,其實不必擔心。scheduler 提供了一個引數 percentageOfNodesToScore。這個引數決定了這個部署迴圈的數量。更多的細節可以參考官方檔案對這部分的說明 [6]

設定外掛名稱

為了使外掛註冊時候使用,還需要為其設定一個名稱

// Name returns name of the plugin. It is used in logs, etc.
func (n *NetworkTraffic) Name() string {
	return Name
}

定義要傳入的引數

網路外掛的擴充套件中還存在一個 prometheusHandle,這個就是操作prometheus-server拿去指標的動作。

首先需要定義一個 PrometheusHandle 的結構體

type PrometheusHandle struct {
	deviceName string // 網路介面名稱
	timeRange  time.Duration // 抓取的時間段
	ip         string // prometheus server的連線地址
	client     v1.API // 操作prometheus的使用者端
}

有了結構就需要查詢的動作和指標,對於指標來說,這裡使用了 node_network_receive_bytes_total 作為獲取Node的網路流量的計算方式。由於環境是部署在叢集之外的,沒有node的主機名,通過 promQL 獲取,整個語句如下:

sum_over_time(node_network_receive_bytes_total{device="eth0"}[1s]) * on(instance) group_left(nodename) (node_uname_info{nodename="node01"})

整個 Prometheus 部分如下:

type PrometheusHandle struct {
	deviceName string
	timeRange  time.Duration
	ip         string
	client     v1.API
}

func NewProme(ip, deviceName string, timeRace time.Duration) *PrometheusHandle {
	client, err := api.NewClient(api.Config{Address: ip})
	if err != nil {
		klog.Fatalf("[NetworkTraffic] FatalError creating prometheus client: %s", err.Error())
	}
	return &PrometheusHandle{
		deviceName: deviceName,
		ip:         ip,
		timeRange:  timeRace,
		client:     v1.NewAPI(client),
	}
}

func (p *PrometheusHandle) GetGauge(node string) (*model.Sample, error) {
	value, err := p.query(fmt.Sprintf(nodeMeasureQueryTemplate, node, p.deviceName, p.timeRange))
	fmt.Println(fmt.Sprintf(nodeMeasureQueryTemplate, p.deviceName, p.timeRange, node))
	if err != nil {
		return nil, fmt.Errorf("[NetworkTraffic] Error querying prometheus: %w", err)
	}

	nodeMeasure := value.(model.Vector)
	if len(nodeMeasure) != 1 {
		return nil, fmt.Errorf("[NetworkTraffic] Invalid response, expected 1 value, got %d", len(nodeMeasure))
	}
	return nodeMeasure[0], nil
}

func (p *PrometheusHandle) query(promQL string) (model.Value, error) {
    // 通過promQL查詢並返回結果
	results, warnings, err := p.client.Query(context.Background(), promQL, time.Now())
	if len(warnings) > 0 {
		klog.Warningf("[NetworkTraffic Plugin] Warnings: %v\n", warnings)
	}

	return results, err
}

設定排程器的引數

因為需要指定 prometheus 的地址,網路卡名稱,和獲取資料的大小,故整個結構體如下,另外,引數結構必須遵循<Plugin Name>Args 格式的名稱。

type NetworkTrafficArgs struct {
	IP         string `json:"ip"`
	DeviceName string `json:"deviceName"`
	TimeRange  int    `json:"timeRange"`
}

為了使這個型別的資料作為 KubeSchedulerConfiguration 可以解析的結構,還需要做一步操作,就是在擴充套件APIServer時擴充套件對應的資源型別。在這裡kubernetes中提供兩種方法來擴充套件 KubeSchedulerConfiguration 的資源型別。

一種是舊版中提供了 framework.DecodeInto 函數可以做這個操作

func New(plArgs *runtime.Unknown, handle framework.FrameworkHandle) (framework.Plugin, error) {
	args := Args{}
	if err := framework.DecodeInto(plArgs, &args); err != nil {
		return nil, err
	}
	...
}

另外一種方式是必須實現對應的深拷貝方法,例如 NodeLabel 中的

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// NodeLabelArgs holds arguments used to configure the NodeLabel plugin.
type NodeLabelArgs struct {
	metav1.TypeMeta

	// PresentLabels should be present for the node to be considered a fit for hosting the pod
	PresentLabels []string
	// AbsentLabels should be absent for the node to be considered a fit for hosting the pod
	AbsentLabels []string
	// Nodes that have labels in the list will get a higher score.
	PresentLabelsPreference []string
	// Nodes that don't have labels in the list will get a higher score.
	AbsentLabelsPreference []string
}

最後將其註冊到register中,整個行為與擴充套件APIServer是類似的

// addKnownTypes registers known types to the given scheme
func addKnownTypes(scheme *runtime.Scheme) error {
	scheme.AddKnownTypes(SchemeGroupVersion,
		&KubeSchedulerConfiguration{},
		&Policy{},
		&InterPodAffinityArgs{},
		&NodeLabelArgs{},
		&NodeResourcesFitArgs{},
		&PodTopologySpreadArgs{},
		&RequestedToCapacityRatioArgs{},
		&ServiceAffinityArgs{},
		&VolumeBindingArgs{},
		&NodeResourcesLeastAllocatedArgs{},
		&NodeResourcesMostAllocatedArgs{},
	)
	scheme.AddKnownTypes(schema.GroupVersion{Group: "", Version: runtime.APIVersionInternal}, &Policy{})
	return nil
}

Notes:對於生成深拷貝函數及其他檔案,可以使用 kubernetes 程式碼庫中的指令碼 https://www.cnblogs.com/Cylon/p/kubernetes/hack/update-codegen.sh

這裡為了方便使用了 framework.DecodeInto 的方式。

專案部署

準備 scheduler 的 profile,可以看到,我們自定義的引數,就可以被識別為 KubeSchedulerConfiguration 的資源型別了。

apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: /mnt/d/src/go_work/customScheduler/scheduler.conf
profiles:
- schedulerName: custom-scheduler
  plugins:
    score:
      enabled:
      - name: "NetworkTraffic"
      disabled:
      - name: "*"
  pluginConfig:
    - name: "NetworkTraffic"
      args:
        ip: "http://10.0.0.4:9090"
        deviceName: "eth0"
        timeRange: 60

如果需要部署到叢集內部,可以打包成映象

FROM golang:alpine AS builder
MAINTAINER cylon
WORKDIR /scheduler
COPY ./ /scheduler
ENV GOPROXY https://goproxy.cn,direct
RUN \
    sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories && \
    apk add upx  && \
    GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -o scheduler main.go && \
    upx -1 scheduler && \
    chmod +x scheduler

FROM alpine AS runner
WORKDIR /go/scheduler
COPY --from=builder /scheduler/scheduler .
COPY --from=builder /scheduler/scheduler.yaml /etc/
VOLUME ["./scheduler"]

部署在叢集內部所需的資源清單

apiVersion: v1
kind: ServiceAccount
metadata:
  name: scheduler-sa
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: scheduler
subjects:
  - kind: ServiceAccount
    name: scheduler-sa
    namespace: kube-system
roleRef:
  kind: ClusterRole
  name: system:kube-scheduler
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: custom-scheduler
  namespace: kube-system
  labels:
    component: custom-scheduler
spec:
  selector:
    matchLabels:
      component: custom-scheduler
  template:
    metadata:
      labels:
        component: custom-scheduler
    spec:
      serviceAccountName: scheduler-sa
      priorityClassName: system-cluster-critical
      containers:
        - name: scheduler
          image: cylonchau/custom-scheduler:v0.0.1
          imagePullPolicy: IfNotPresent
          command:
            - ./scheduler
            - --config=/etc/scheduler.yaml
            - --v=3
          livenessProbe:
            httpGet:
              path: /healthz
              port: 10251
            initialDelaySeconds: 15
          readinessProbe:
            httpGet:
              path: /healthz
              port: 10251

啟動自定義 scheduler,這裡通過簡單的二進位制方式啟動,所以需要一個kubeconfig做認證檔案

./main --logtostderr=true \
	--address=127.0.0.1 \
	--v=3 \
	--config=`pwd`/scheduler.yaml \
	--kubeconfig=`pwd`/scheduler.conf

啟動後為了驗證方便性,關閉了原來的 kube-scheduler 服務,因為原來的 kube-scheduler 已經作為HA中的master,所以不會使用自定義的 scheduler 導致pod pending。

驗證結果

準備一個需要部署的Pod,指定使用的排程器名稱

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2 
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
      schedulerName: custom-scheduler

這裡實驗環境為2個節點的kubernetes叢集,master與node01,因為master的服務比node01要多,這種情況下不管怎樣,排程結果永遠會被排程到node01上。

$ kubectl get pods -o wide
NAME                                READY   STATUS    RESTARTS   AGE   IP             NODE     NOMINATED NODE   READINESS GATES
nginx-deployment-69f76b454c-lpwbl   1/1     Running   0          43s   192.168.0.17   node01   <none>           <none>
nginx-deployment-69f76b454c-vsb7k   1/1     Running   0          43s   192.168.0.16   node01   <none>           <none>

而排程器的紀錄檔如下

I0808 01:56:31.098189   27131 networktraffic.go:83] [NetworkTraffic] node 'node01' bandwidth: %!s(int64=12541068340)
I0808 01:56:31.098461   27131 networktraffic.go:70] [NetworkTraffic] Nodes final score: [{master-machine 0} {node01 12541068340}]
I0808 01:56:31.098651   27131 networktraffic.go:70] [NetworkTraffic] Nodes final score: [{master-machine 0} {node01 71}]
I0808 01:56:31.098911   27131 networktraffic.go:73] [NetworkTraffic] Nodes final score: [{master-machine 0} {node01 71}]
I0808 01:56:31.099275   27131 default_binder.go:51] Attempting to bind default/nginx-deployment-69f76b454c-vsb7k to node01
I0808 01:56:31.101414   27131 eventhandlers.go:225] add event for scheduled pod default/nginx-deployment-69f76b454c-lpwbl
I0808 01:56:31.101414   27131 eventhandlers.go:205] delete event for unscheduled pod default/nginx-deployment-69f76b454c-lpwbl
I0808 01:56:31.103604   27131 scheduler.go:609] "Successfully bound pod to node" pod="default/nginx-deployment-69f76b454c-lpwbl" node="no
de01" evaluatedNodes=2 feasibleNodes=2
I0808 01:56:31.104540   27131 scheduler.go:609] "Successfully bound pod to node" pod="default/nginx-deployment-69f76b454c-vsb7k" node="no
de01" evaluatedNodes=2 feasibleNodes=2

Reference

[1] scheduling config

[2] kube-scheduler

[3] scheduling-plugins

[4] custom scheduler plugins

[5] ssues #79384

[6] scheduler perf tuning

[7] creating a kube-scheduler plugin