Анализ исходного кода планировщика Kubernetes и применение собственного алгоритма планирования ресурсов

задняя часть Kubernetes
Анализ исходного кода планировщика Kubernetes и применение собственного алгоритма планирования ресурсов

image.png

火轮.png

Анализ планировщика kubernetes

Что такое планировщик kubernetes?

Небольшой, как кластер kubernetes, выполняющий десятки рабочих нагрузок, или большой, как кластер kubernetes, выполняющий тысячи рабочих нагрузок, где именно должна выполняться каждая рабочая нагрузка, для этого требуется умный мозг, и планировщик kubernetes — это умный мозг. По результатам его работа очень проста, достаточно заполнить имя ноды для pod.spec.nodeName.Из процесса он крайне сложен, т.к какую ноду выбрать наиболее разумно, ответ часто и Сценарии тесно связаны, и почти невозможно найти набор алгоритмов планирования, которые адаптируются к различным сценариям. Таким образом, все виды подключаемых модулей алгоритмов появляются бесконечным потоком, и индивидуальная разработка компанией алгоритмов планирования стала обычным требованием.

Как работает планировщик?

Планировщик в основном состоит из таких двух больших циклов调度器

  • Первый цикл управления отвечает за чтение незапланированных модулей из etcd и добавление их в очередь планирования.
  • Основная логика второго цикла управления заключается в непрерывном исключении модуля из очереди планирования. Затем вызывается алгоритм Predicates для «фильтрации». Набор узлов, «отфильтрованных» на этом этапе, представляет собой список всех хостов, на которых может работать этот модуль. Затем планировщик вызовет алгоритм приоритетов, чтобы оценить узлы в приведенном выше списке по шкале от 0 до 100. Узел с наивысшим баллом будет результатом этого планирования. После выполнения алгоритма планирования планировщику необходимо изменить значение поля nodeName объекта Pod на имя указанного выше узла.

Как планировщик следит за тем, чтобы разные копии одного и того же контроллера не находились на одном узле в максимально возможной степени?

В сочетании со структурой планировщика, которую мы проанализировали выше, легко подумать, что планировщик должен находиться на стадии оценки, в соответствии с различными условиями узла (есть ли идентичные копии? Сколько?) для оценки, чтобы гарантировать чтобы стручки были максимально рассредоточены. Да, именно это и делает планировщик, но этот процесс подсчета очков разбит на три этапа, и мы можем понять принцип, проанализировав эту часть кода.портал исходного кода

Этап 1: предварительная оценка

func (pl *SelectorSpread) PreScore(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod, nodes []*v1.Node) *framework.Status {
	if skipSelectorSpread(pod) {
		return nil
	}
	var selector labels.Selector
	selector = helper.DefaultSelector(
		pod,
		pl.services,
		pl.replicationControllers,
		pl.replicaSets,
		pl.statefulSets,
	)
	state := &preScoreState{
		selector: selector,
	}
	cycleState.Write(preScoreStateKey, state)
	return nil
}
func DefaultSelector(pod *v1.Pod, sl corelisters.ServiceLister, cl corelisters.ReplicationControllerLister, rsl appslisters.ReplicaSetLister, ssl appslisters.StatefulSetLister) labels.Selector {
	labelSet := make(labels.Set)
	// Since services, RCs, RSs and SSs match the pod, they won't have conflicting
	// labels. Merging is safe.

	if services, err := GetPodServices(sl, pod); err == nil {
		for _, service := range services {
			labelSet = labels.Merge(labelSet, service.Spec.Selector)
		}
	}

	if rcs, err := cl.GetPodControllers(pod); err == nil {
		for _, rc := range rcs {
			labelSet = labels.Merge(labelSet, rc.Spec.Selector)
		}
	}

	selector := labels.NewSelector()
	if len(labelSet) != 0 {
		selector = labelSet.AsSelector()
	}

	if rss, err := rsl.GetPodReplicaSets(pod); err == nil {
		for _, rs := range rss {
			if other, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector); err == nil {
				if r, ok := other.Requirements(); ok {
					selector = selector.Add(r...)
				}
			}
		}
	}

	if sss, err := ssl.GetPodStatefulSets(pod); err == nil {
		for _, ss := range sss {
			if other, err := metav1.LabelSelectorAsSelector(ss.Spec.Selector); err == nil {
				if r, ok := other.Requirements(); ok {
					selector = selector.Add(r...)
				}
			}
		}
	}

	return selector
}

CycleState в сигнатуре метода используется как хранилище для диспетчеризации данных на разных этапах, на этом этапе делать нечего, то есть узнать все контроллеры, которые содержат метки подов, и сохранить их в cycleState для использования в следующий этап. Следует отметить, что контроллер модуля должен быть одним из Services, ReplicaSets и StatefulSets, replicationControllers.

Этап 2: Оценка

func (pl *SelectorSpread) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
	if skipSelectorSpread(pod) {
		return 0, nil
	}

	c, err := state.Read(preScoreStateKey)
	if err != nil {
		return 0, framework.NewStatus(framework.Error, fmt.Sprintf("Error reading %q from cycleState: %v", preScoreStateKey, err))
	}

	s, ok := c.(*preScoreState)
	if !ok {
		return 0, framework.NewStatus(framework.Error, fmt.Sprintf("%+v convert to tainttoleration.preScoreState error", c))
	}

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

	count := countMatchingPods(pod.Namespace, s.selector, nodeInfo)
	return int64(count), nil
}
func skipSelectorSpread(pod *v1.Pod) bool {
	return len(pod.Spec.TopologySpreadConstraints) != 0
}
func countMatchingPods(namespace string, selector labels.Selector, nodeInfo *framework.NodeInfo) int {
	if len(nodeInfo.Pods) == 0 || selector.Empty() {
		return 0
	}
	count := 0
	for _, p := range nodeInfo.Pods {
		// Ignore pods being deleted for spreading purposes
		// Similar to how it is done for SelectorSpreadPriority
		if namespace == p.Pod.Namespace && p.Pod.DeletionTimestamp == nil {
			if selector.Matches(labels.Set(p.Pod.Labels)) {
				count++
			}
		}
	}
	return count
}
  • Во-первых, оценивается, установлены ли для модуля топологические ограничения, и если они установлены, он сразу получает 0 баллов.
  • Если он не установлен, он передается функции countMatchingPods для подсчета очков. Метод расчета заключается в том, чтобы увидеть, сколько подов на узле, в том же пространстве имен и не в удаленном состоянии, имеют ту же метку, что и текущий запланированный под, 1 = 1 балл, а накопленные баллы — это баллы текущего узел.

Этап 3: нормализовать счет

Функция этого этапа состоит в том, чтобы внести поправку в оценку этапа Score, и пересмотренный результат будет использоваться в качестве окончательной оценки узла. Конкретный код выглядит следующим образом.

func (pl *SelectorSpread) NormalizeScore(ctx context.Context, state *framework.CycleState, pod *v1.Pod, scores framework.NodeScoreList) *framework.Status {
	if skipSelectorSpread(pod) {
		return nil
	}

	countsByZone := make(map[string]int64, 10)
	maxCountByZone := int64(0)
	maxCountByNodeName := int64(0)

	for i := range scores {
		if scores[i].Score > maxCountByNodeName {
			maxCountByNodeName = scores[i].Score
		}
		nodeInfo, err := pl.sharedLister.NodeInfos().Get(scores[i].Name)
		if err != nil {
			return framework.NewStatus(framework.Error, fmt.Sprintf("getting node %q from Snapshot: %v", scores[i].Name, err))
		}
		zoneID := utilnode.GetZoneKey(nodeInfo.Node())
		if zoneID == "" {
			continue
		}
		countsByZone[zoneID] += scores[i].Score
	}

	for zoneID := range countsByZone {
		if countsByZone[zoneID] > maxCountByZone {
			maxCountByZone = countsByZone[zoneID]
		}
	}

	haveZones := len(countsByZone) != 0

	maxCountByNodeNameFloat64 := float64(maxCountByNodeName)
	maxCountByZoneFloat64 := float64(maxCountByZone)
	MaxNodeScoreFloat64 := float64(framework.MaxNodeScore)

	for i := range scores {
		// initializing to the default/max node score of maxPriority
		fScore := MaxNodeScoreFloat64
		if maxCountByNodeName > 0 {
			fScore = MaxNodeScoreFloat64 * (float64(maxCountByNodeName-scores[i].Score) / maxCountByNodeNameFloat64)
		}
		// If there is zone information present, incorporate it
		if haveZones {
			nodeInfo, err := pl.sharedLister.NodeInfos().Get(scores[i].Name)
			if err != nil {
				return framework.NewStatus(framework.Error, fmt.Sprintf("getting node %q from Snapshot: %v", scores[i].Name, err))
			}

			zoneID := utilnode.GetZoneKey(nodeInfo.Node())
			if zoneID != "" {
				zoneScore := MaxNodeScoreFloat64
				if maxCountByZone > 0 {
					zoneScore = MaxNodeScoreFloat64 * (float64(maxCountByZone-countsByZone[zoneID]) / maxCountByZoneFloat64)
				}
				fScore = (fScore * (1.0 - zoneWeighting)) + (zoneWeighting * zoneScore)
			}
		}
		scores[i].Score = int64(fScore)
	}
	return nil
}
  • Во-первых, оценивается, установлены ли для модуля ограничения топологии.Если они установлены, оценка равна 0, что точно такое же, как и на предыдущем этапе.
  • Затем найдите узел и зону с наибольшим количеством очков.
  • Окончательная оценка узла равна 100 * (максимальная оценка узла - текущая оценка узла) / максимальная оценка узла, поэтому чем выше текущая оценка узла, тем ниже итоговая оценка, что соответствует максимально возможной децентрализации. .
  • В случае зоны
    • Оценка текущей зоны = 100*(оценка самой большой зоны - оценка текущей зоны) / оценка самой большой зоны
    • Окончательная оценка измерения узла = оценка узла предыдущего шага * (1 - вес измерения зоны) + оценка измерения зоны * вес измерения зоны

Что такое зона?

Эта концепция используется в процессе планирования для адаптации к ситуации, когда некоторые узлы рассматриваются как единое целое.Например, если узел А и узел В принадлежат одной и той же зоне, то в алгоритме планирования в определенной степени существование этих два узла будут считаться одновременно одной и той же копией. Так к какой зоне относится узел, я нашел такой кусок кода в исходниках.

func GetZoneKey(node *v1.Node) string {
	labels := node.Labels
	if labels == nil {
		return ""
	}

	// TODO: prefer stable labels for zone in v1.18
	zone, ok := labels[v1.LabelZoneFailureDomain]
	if !ok {
		zone, _ = labels[v1.LabelZoneFailureDomainStable]
	}

	// TODO: prefer stable labels for region in v1.18
	region, ok := labels[v1.LabelZoneRegion]
	if !ok {
		region, _ = labels[v1.LabelZoneRegionStable]
	}

	if region == "" && zone == "" {
		return ""
	}

	// We include the null character just in case region or failureDomain has a colon
	// (We do assume there's no null characters in a region or failureDomain)
	// As a nice side-benefit, the null character is not printed by fmt.Print or glog
	return region + ":\x00:" + zone
}

Это указывает, находятся ли два узла в одной зоне, в зависимости от ее метки.

Как решить проблему фрагментации ресурсов?

Давайте рассмотрим пример, чтобы проиллюстрировать эту проблему.Как показано на рисунке ниже, у меня есть два узла с 1 оставшимся графическим процессором, но когда я хочу создать модуль с 2 графическими процессорами, я не могу найти узел, который можно запланировать.调度碎片的尴尬

Решения

Эту проблему можно решить в определенной степени, если мы сначала заполним один узел и запланируем его на другом поде. Конкретный алгоритм выглядит следующим образом.调度算法

Метод реализации

Точно так же, как сам планировщик решает задачу максимально возможного рассредоточения подов под одним контроллером, наш алгоритм также вмешивается в скоринг нод. Итак, нетрудно подумать о подключении нашего алгоритма к планировщику. Так как же должна быть выполнена эта работа? Далее будут представлены основные решения в отрасли.

Стандартный процесс расширения планировщика: Scheduling Framework

Способ расширения планировщика можно резюмировать в виде следующих четырех

  • Измените исходный код kube-scheduler напрямую
  • Дополнительное развертывание полностью независимого планировщика
  • расширение планировщика
  • Структура планирования Первые три устарели, поэтому здесь представлена ​​только последняя. Помните две контрольные петли, которые мы сделали в начале статьи, прорыв расширения находится внутри второй петли. Его можно увеличить до изображения ниже.○ можно условно разделить на две части ■ Процесс планирования: выберите узлы для модулей ● Зеленая область является синхронной, и одновременно можно запланировать только один модуль. ■ Процесс привязки: поместите модуль на выбранный узел. ● Желтая область является асинхронной, и несколько модулей могут быть привязаны одновременно. ○ Процесс планирования и процесс привязки будут завершены на полпути, когда они столкнутся со следующими ситуациями.В это время модуль будет помещен обратно в очередь для планирования и будет ждать следующей повторной попытки. ■ Планировщик считает, что в настоящее время нет необязательных узлов для модуля. ■ Внутренняя ошибка Планировщик предоставляет нам в общей сложности интерфейсов 12. Реализация интерфейса соответствует пользовательской логике планирования, которая завершает определенный этап. Функции этих 12 интерфейсов соответственно.
  • QueueSort: определите, какой модуль запланирован первым
  • Предварительный фильтр: часть обязательной фильтрации, предварительной обработки информации.
  • Фильтр: принудительно отфильтровывает узлы, которые не могут быть запланированы.
  • Пост-фильтр: его можно использовать для создания некоторых уведомлений, поскольку узлы, которые нельзя запланировать, в настоящее время отфильтрованы. Все, что осталось, — это узлы, которые могут запускать модуль, но неясно, кто лучший узел на данный момент.
  • PreScore: Предварительная обработка перед оценкой.При анализе исходного кода планировщика выше разработчик сохраняет здесь данные контроллера модуля для использования на более поздних этапах.
  • Оценка: оценка каждого узла.
  • NormalizeScoring: измените результат предыдущего шага как окончательный. Следует отметить, что если ваша оценка на предыдущем шаге не находится в диапазоне [0-100], то вы должны реализовать этот интерфейс.
  • Резерв: зарезервируйте ресурсы для модулей перед привязкой, так как процесс привязки модулей является асинхронным, как показано в желтой части рисунка выше, чтобы избежать избытка ресурсов. Если привязка не удалась, будет активирована функция UnReserve для освобождения ресурсов.
  • Разрешить: заблокировать или отложить привязку модулей.
  • Предварительная привязка: предварительная обработка перед привязкой, например обработка сетевых ресурсов, от которых зависят модули.
  • bind: расширение используется для привязки модулей к узлам.
  • Unreserve: поскольку ресурсы зарезервированы для модулей на этапе резервирования, в случае сбоя привязки модуля необходимо освободить зарезервированные ресурсы на этом узле.

Как это отражено в коде

Ниже я сделаю простое демо на основе kube-scheduler 1.19

  • Первым шагом является реализация плагина планировщика
package pkg

import (
	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"context"
	framework "k8s.io/kubernetes/pkg/scheduler/framework/v1alpha1"
)

type demoPlugin struct {

}

func NewDemoPlugin(_ runtime.Unknown, _ framework.FrameworkHandle) (framework.Plugin, error) {
	return &demoPlugin{}, nil
}
// Name returns name of the plugin. It is used in logs, etc.
func (d *demoPlugin) Name() string {
	return "demo-plugin"
}
func (d *demoPlugin) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
	return 100,nil
}
// ScoreExtensions of the Score plugin.
func (d *demoPlugin) ScoreExtensions() framework.ScoreExtensions {
	return nil
}

Реализация трех интерфейсов Name Score ScoreExtensions завершает простейший плагин планировщика.Здесь я по умолчанию возвращаю 100 баллов для каждого узла.

  • Второй шаг — регистрация плагина в планировщике.
package main

import (
	"git.cai-inc.com/devops/zcy-scheduler/pkg/demo"
	"math/rand"
	"os"
	"time"

	"k8s.io/component-base/logs"
	"k8s.io/kubernetes/cmd/kube-scheduler/app"

)

func main() {
	rand.Seed(time.Now().UnixNano())

	command := app.NewSchedulerCommand(
		app.WithPlugin(noderesources.AllocatableName, demo.NewDemoPlugin),
	)
	logs.InitLogs()
	defer logs.FlushLogs()

	if err := command.Execute(); err != nil {
		os.Exit(1)
	}
}

Здесь мы можем напрямую обратиться к коду в kubernetres/cmd/kube-scheduler/app, который содержит NewSchedulerCommand, где мы можем зарегистрировать наш пользовательский плагин.

  • Третий шаг — написать файл конфигурации.
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
leaderElection:
  leaderElect: false
clientConnection:
  kubeconfig: "/Users/zcy/newgo/zcy-scheduler/scheduler.conf"
profiles:
- schedulerName: zcy-scheduler
  plugins:
    score:
      enabled:
      - name: demo-plugin
      disabled:
      - name: "*"

Здесь вы даете своему планировщику имя и устанавливаете демо-плагин, который будет использоваться на этапе оценки. Выше приведен базовый способ реализации планировщика.

Суммировать

Сегодняшняя статья знакомит со структурой планировщика, в основном с двумя циклами. Как сделать так, чтобы реплики подов были распределены по разным нодам, и как решить проблему фрагментации ресурсов. Наконец, в нем также рассказывается, как разработать собственный планировщик, в надежде дать вам некоторые идеи при разработке собственного планировщика.

использованная литература

Рекомендуемое чтение

Бой с Guava Cache — от использования сцены до принципиального анализа

Подробное объяснение протоколов HTTP2.0 и HTTPS.

Познакомьтесь с JVM в первый раз (познакомьтесь с JVM с другой точки зрения)

Карьера

Техническая команда Zhengcaiyun (Zero), команда, полная страсти, творчества и исполнения, расположена в живописном Ханчжоу. В настоящее время у команды более 300 партнеров по исследованиям и разработкам, включая солдат-ветеранов из Ali, Huawei и NetEase, а также новичков из Чжэцзянского университета, Университета науки и технологий Китая, Университета Хандянь и других учебных заведений. В дополнение к ежедневному развитию бизнеса, команда также проводит технические исследования и практику в области облачных технологий, блокчейна, искусственного интеллекта, платформы с низким кодом, промежуточного программного обеспечения, больших данных, системы материалов, инженерной платформы, производительности, визуализации и т. д. И приземлился ряд внутренних технологических продуктов, и продолжал исследовать новые границы технологий. Кроме того, команда также посвятила себя созданию сообщества.В настоящее время они участвуют во многих отличных сообществах с открытым исходным кодом, таких как google flutter, scikit-learn, Apache Dubbo, Apache Rocketmq, Apache Pulsar, CNCF Dapr, Apache DolphinScheduler, alibaba Seata. , и т.д. Если вы хотите измениться, вас забросали вещами, и вы хотите начать их бросать; если вы хотите измениться, вам сказали, что вам нужно больше идей, но вы не можете сломать игру; если вы хотите измениться, у вас есть возможность сделать это, но вы не нуждаетесь в этом; если вы хотите изменить то, что хотите сделать, вам нужна команда, которая это поддержит, но вам некуда вести людей; если вы хотите измениться, у вас есть хорошее понимание, но всегда есть этот слой размытой бумаги... Если вы верите в силу веры, я верю, что обычные люди могут достичь необыкновенных вещей, и я верю, что они могут встретить лучшего себя. Если вы хотите участвовать в процессе развития бизнеса и лично способствовать росту технической команды с глубоким пониманием бизнеса, надежной технической системой, технологиями, создающими ценность, и побочным влиянием, я думаю, нам следует поговорить. В любое время, ожидая, пока вы что-нибудь напишете, отправьте это наzcy-tc@cai-inc.com

Публичный аккаунт WeChat

Статья выпущена одновременно, общедоступный аккаунт технической команды Zhengcaiyun, пожалуйста, обратите внимание

image.png