巧用Prometheus来扩展kubernetes调度器

2023-05-16

Overview

本文将深入讲解 如何扩展 Kubernetes scheduler 中各个扩展点如何使用,与扩展scheduler的原理,这些是作为扩展 scheduler 的所需的知识点。最后会完成一个实验,基于网络流量的调度器。

kubernetes调度配置

kubernetes集群中允许运行多个不同的 scheduler ,也可以为Pod指定不同的调度器进行调度。在一般的Kubernetes调度教程中并没有提到这点,这也就是说,对于亲和性,污点等策略实际上并没有完全的使用kubernetes调度功能,在之前的文章中提到的一些调度插件,如基于端口占用的调度 NodePorts 等策略一般情况下是没有使用到的,本章节就是对这部分内容进行讲解,这也是作为扩展调度器的一个基础。

Scheduler Configuration [1]

kube-scheduler 提供了配置文件的资源,作为给 kube-scheduler 的配置文件,启动时通过 --onfig= 来指定文件。目前各个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 即可。

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/spark-on-k8s-operator@v0.0.0-20210307184338-1947244ce5f4 requires
        k8s.io/apiextensions-apiserver@v0.0.0: 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
}

而在 framework 中,真正执行的操作的方法也是 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 ] [0,100] [0,100] ,所以这里实现的算法公式将为 最高分 − ( 当前带宽 / 最高最高带宽 ∗ 100 ) 最高分 - (当前带宽 / 最高最高带宽 * 100) 最高分(当前带宽/最高最高带宽100),这样就保证了,带宽占用越大的机器,分数越低。

例如,最高带宽为200000,而当前Node带宽为140000,那么这个Node分数为: m a x − 140000 200000 × 100 = 100 − ( 0.7 × 100 ) = 30 max - \frac{140000}{200000}\times 100 = 100 - (0.7\times100)=30 max200000140000×100=100(0.7×100)=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

网络插件的扩展中还存在一个 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: "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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

巧用Prometheus来扩展kubernetes调度器 的相关文章

  • 删除所有 kubernetes 命名空间中所有 pod 的命令

    查看文档后 有一个 API 调用可以删除单个 pod 但是有没有办法删除all所有命名空间中的 Pod 没有命令可以完全按照您的要求进行操作 以下是一些势均力敌的比赛 运行任何这些命令之前请务必小心 如果您使用多个集群 请确保您连接到正确的
  • 由于 istio-sidecar 注入,Kubernetes 部署失败

    我们的 K8 集群工作了一年多 最近它出现了一些奇怪的行为 现在当我们使用kubectl apply f deployment manifest yaml 它没有显示在kubectl get pods 但显示在kubectl get dep
  • 使用 Puppet 配置远程规则集

    我正在尝试使普罗米修斯自动化node exporter和我的普罗米修斯服务器 为了node exporter我已经编写了一个模块来安装所有需要的软件包 设置 ipaddress基于facter还有更多 现在我想确保收集到的信息 hostna
  • 禁用对特定主机的警报,同时对所有其他主机发出警报

    我有数百台主机向普罗米修斯服务器报告 我的每个主机有很多出口商 我希望能够列出我不希望收到警报的主机列表 我仍然需要对这些主机进行普罗米修斯监控 我尝试过匹配没有接收器的路线 这不起作用 我究竟做错了什么 或者说 我应该怎么做 我的路线规则
  • 通过流式传输将分段文件上传到 Amazon S3 时内存使用率过高?

    我的 Java Spring 应用程序中的以下方法直接将文件流式传输并上传到 Amazon S3 存储桶 我研究过 使用流将使上传大文件 对于我的用例 gt 100MB 视频 更加内存有效 当使用 25MB 文件测试该方法时 Kuberne
  • Pod 副本如何相互同步 - Kubernetes?

    我有一个带有 3 个副本的 MySQL 数据库 Pod 现在我正在一个 Pod 中进行一些更改 Pod 数据 而不是 Pod 配置 假设我要添加一张表 该更改将如何影响 Pod 的其他副本 我正在使用带有 3 个工作节点的 kubernet
  • 跨命名空间共享秘密

    有没有办法在 Kubernetes 中跨命名空间共享秘密 我的用例是 我的所有命名空间都有相同的私有注册表 并且我想避免为每个命名空间创建相同的秘密 秘密 API 对象驻留在命名空间中 它们只能由同一命名空间中的 pod 引用 基本上 您必
  • Docker nuget连接超时

    尝试利用官方jetbrains teamcity agentKubernetes 上的图像 我已经设法在 Docker 中运行 Docker 但尝试使用以下命令构建 ASP NET Core 映像docker build命令失败于dotne
  • 如何重用现有的持久卷声明

    我已经删除了我的 Elasticsearch 集群 但现在在部署新集群后 我需要访问存储在 3 个持久卷 PV 上的旧数据 如下所述 NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
  • 如何从 Next.js 应用程序访问 Kubernetes 容器环境变量?

    在我的 next config js 中 我有一个如下所示的部分 module exports serverRuntimeConfig Will only be available on the server side mySecret s
  • 在普罗米修斯中找不到查询 label_values [关闭]

    Closed 这个问题是无法重现或由拼写错误引起 help closed questions 目前不接受答案 在 Grafana 文档中 我发现我应该能够查询我的 Prometheus 服务器以获取使用以下命令提供监控数据的所有实例 lab
  • 使用 zeppelin 在 kubernetes 上 Spark

    我按照本指南在使用 minikube 设置的本地 kubernetes 集群中运行 zeppelin 容器 https zeppelin apache org docs 0 9 0 SNAPSHOT quickstart kubernete
  • gofabric8> 无法解压缩 /Users/apple/.fabric8/bin/oc.zip zip:不是有效的 zip

    我正在尝试为微服务设置环境 我正在使用 Fabric8 来做到这一点 我在用着mvn fabric8 cluster start Dfabric8 cluster kind openshift命令 执行时出现以下错误 INFO gofabr
  • Kubernetes 中的暂停镜像有什么用?

    看来在 Windows 上 Kubernetes 启动了一个pause创建的每个 Pod 的图像 这个暂停图像的目的是什么 我在哪里可以找到更多有关它的文档 The pause容器是保存 Pod 网络命名空间的容器 Kubernetes 创
  • prometheus 节点实例列表

    是否可以使用 prometheus 获取节点实例列表 我有一个节点导出器 但我没有看到这样的指标 我们应该添加一个新的运算符吗 您可以使用kube 状态指标 https github com kubernetes kube state me
  • 运行 istio-proxy 后启动容器/pod

    我正在尝试使用 Istio 和 Envoy 通过 Kubernetes 实现服务网格 我能够设置服务和 istio proxy 但无法控制容器和 istio proxy 的启动顺序 我的容器是第一个启动的 并尝试通过 TCP 访问外部资源
  • Ubuntu 16.04 中 kubeadm join 命令每次都会超时

    我正在使用 Ubuntu 16 04 Xenial 但我遇到了一个问题kubeadm加入时nodes to my master 在加入一个node to my master 我不断遇到超时问题 discovery Failed to req
  • Kubernetes - 服务之间的通信

    我目前正在开发 kubernetes 集群 集群工作正常 我需要在不使用代理的情况下建立服务之间的通信 例如我有以下服务 worker app1 app2 app3 Worker 需要直接通过 SSH 登录应用程序容器并执行一些命令 在 d
  • kubernetes/openshift 中的请求与限制 cpu

    我在为 Openshift 中的 pod 选择正确的请求和限制设置时遇到一些困境 一些数据 在启动期间 应用程序需要至少 600 毫核才能在 150 秒内完成就绪检查 启动后 200 毫核应该足以让应用程序保持空闲状态 所以我从文档中的理解
  • Podman:如何解析使用 Kubernetes YAML 部署的 pod 的主机名

    我正在使用 podman 4 5 dev 我使用以下方式部署了两个 Pod podman kube 播放 foo yaml podman kube play bar yaml 我在文件中指定了 pod 的主机名 但它们不会在容器内得到解析

随机推荐

  • 阿里Arthas(阿尔赛斯)-java诊断工具使用入门

    1 下载官方测试程序启动 doda 64 host166 game curl O https arthas aliyun com math game jar doda 64 host166 game java jar math game j
  • LHL 华为开发者大会有感 之 什么是HMS

    华为移动服务 同义词 HMS xff08 Huawei Mobile Services的英文缩写 xff09 一般指华为移动服务 华为移动服务 xff08 HMS xff09 全称为Huawei Mobile Services xff0c
  • EF Core之自动历史记录

    EF Core之自动历史记录 有的场景下需要记录特定表的增删改操作 xff0c 以便追溯 传统的做法是在增删改的方法里同步做记录 xff0c 很繁琐 在这里我们可以配合EF Core的DBContext做一个全局管控 全局管控自然就要有固定
  • SQL Server 2012 导出数据及分离MDF、LDF

    最近在设计数据库时看到自己设计的数据库和师哥师姐给我们的不一样 xff0c 于是就查了一下把数据库导出写了下来 分离数据库步骤 这是师哥师姐给我们的数据库格式 xff0c 如下 xff1a 那么如何生成MDF和LDF格式的数据库呢 xff1
  • kindeditor异步加载时无法显示编辑框

    事情是这样的 xff0c 我在A页面的div通过 jQuery load 方法载入一个页面B时 xff0c B页面带有编辑框 textarea xff0c 现在想让这个 textarea 显示为一个 kindeditor的富文本编辑区域 x
  • VR游戏设计之三大特性

    2016 年 9 月 23 24 日 xff0c 由 CSDN 和创新工场联合主办的 MDCC 2016 移动开发者大会 中国 xff08 Mobile Developer Conference China xff09 将在北京 国家会议中
  • 组合导航系列文章(十二):滤波器基本原理

    我觉得不错的地方 3 滤波器估计原理 组合导航中 xff0c 先验是imu解算的值 xff0c 观测是gps等传感器给出的值 xff0c 融合的目的是找到概率最大的那个值 上面介绍的三种方法都是对先验和观测的融合 xff0c 由于极大似然要
  • Android 未进行依赖管理导致的问题:IDE 报错 “Unresolved reference: registerForActivityResult“但是能正常编译运行

    前言 谷歌前段时间废弃了 startActicityForResult 取而代之的是 ActicityResultApi xff0c 作为一个紧跟谷歌步伐走的菜鸟开发者 xff0c 当然想着的是第一时间学习并替换项目中的相关代码 但是 xf
  • 基于DataBinding的基类(Activity、Fragment、Adapter)

    基于DataBinding的基类 xff08 Activity Fragment Adapter xff09 1 BaseBindActivity public abstract class BaseBindActivity lt DB e
  • STM32 LwIP单网卡绑定多个IP地址

    STM32 LwIP单网卡绑定多个IP地址 芯片 xff1a STM32F107VC 编译器 xff1a KEIL4 作者 xff1a SY 日期 xff1a 2018 6 28 11 02 53 概述 在只有一个物理网卡的情况下 xff0
  • nginx 配置https 并兼容http 或强制http 转为https

    当我们升级http 为https时 xff0c 为了兼顾一些访问者还是通过http访问的方式 xff0c 我们通常采用两种方式 第一种 xff1a http 可以访问 xff0c https也可以访问 第二种 xff1a http 访问时
  • 滤波学习理解----EKF(一)

    最近回到slam方向了 xff0c 所以有时间整理一下最近的收获 最复杂也是最简单的模块 滤波 引入 那么滤波是什么呢 xff1f 滤波就是由于观测observation xff08 OB xff09 天生具备的误差和噪声 当有多个信号源观
  • systemd开机启动和关机回调脚本

    一 开机执行一次的脚本 我们通过可以创建一个 etc rc local文件 xff1a etc rc local文件内容如下 xff1a span class token operator span span class token ope
  • (五)Selenium自动化测试实战—PO模式

    nbsp 上一篇 四 selenium自动化测试之上传本地文件 要开朗的spookypop的博客 CSDN博客 selenium上传本地文件 先看下测试代码运行效果 在做自动化测试时 测试框架设计很重要 测试代码写法也很多种 最简单的莫过于
  • 网络隧道技术

    隧道技术摘要 隧道技术 xff08 Tunneling xff09 是网络基础设置在网络之间传递数据的方式 xff0c 使用隧道技术传递可以是不同协议的数据包 xff0c 隧道协议将这些其他协议的数据包重新封装在新的包头中发送 被封装的数据
  • 数据库的备份和还原

    数据库的备份和还原是一个很重要的问题 xff0c 有时候我们一个误删可能数据库里的数据就都没有了 xff0c 所以一定要做好备份的工作 备份 1 右击 任务 备份 2 选择要备份的数据库 xff0c 和备份的类型 完整 xff0c 添加 3
  • 海信A6/A6L A7Pro/CC A5PRO/A5PRO CC 安装gms google service指南

    用过海信双面屏或者eink手机的朋友都知道 xff0c 海信手机就是死活安装不了谷歌全家桶 xff0c 因为海信的领导说跟谷歌有协议不能安装谷歌框架 xff08 还说后期google审核坚决不给安装 xff0c 人家其他ov mui都可以安
  • Windows Terminal无法加载WSL [process exited with code 4294967295 (0xffffffff)]

    在Windows Terminal中WSL无法打开错误代码是 process exited with code 4294967295 0xffffffff xff0c 但在命令行中 通过 34 C Windows System32 wsl
  • 【经验分享】PC端免费高效的同声翻译

    2022 09 26 谷歌翻译退出中国市场 translate googleapis com 与 谷歌翻译web translate google cn现在无法用了 最新解决方法见 https blog csdn net sinat 240
  • 巧用Prometheus来扩展kubernetes调度器

    Overview 本文将深入讲解 如何扩展 Kubernetes scheduler 中各个扩展点如何使用 xff0c 与扩展scheduler的原理 xff0c 这些是作为扩展 scheduler 的所需的知识点 最后会完成一个实验 xf