巧用Prometheus來擴展kubernetes調度器

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 (默認)MostAllocated 和 RequestedToCapacityRatio。擴展點: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 即可。

image

定義插件結構體

其中 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,可以是集群內部的,也可以是集群外部的,這裡使用的是集群外部的。
  • 對 promQL 與 client_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 //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 程式碼庫中的腳本 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: "//10.0.0.4:9090"
        deviceName: "eth0"
        timeRange: 60

如果需要部署到集群內部,可以打包成鏡像

FROM golang:alpine AS builder
MAINTAINER cylon
WORKDIR /scheduler
COPY ./ /scheduler
ENV GOPROXY //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

Tags: