Контроллер узлов для анализа исходного кода Kubernetes

Kubernetes

Весь код в этом разделе основан на версии 1.13.4.

Процесс загрузки

Как упоминалось ранее в анализе контроллер-менеджер, контроллер имеет в основном один и тот же формат управления для каждого контроллера, который имеет видstart***ControllerСпособ инкапсулировать его в независимый метод, NodeController не является исключением. В версии 1.13.4 контроллеры Node разделены на два типа (в более ранних версиях был только один), а именноNodeIpamControllerа такжеNodeLifecycleController. Среди них NodeIpamController в основном имеет дело с IPAM-адресом Node, а NodeLifecycleController — со всем жизненным циклом Node.В этой статье в основном анализируется NodeLifecycleController.

Как показано на рисунке, NodeLifecycleController начинается сstartNodeLifecycleControllerМетод начинает процесс управления его жизненным циклом. В основном сосредоточьтесь на двух методах:NewNodeLifecycleControllerа такжеRun. NewNodeLifecycleController отвечает за создание объектов ресурсов, а Run отвечает за запуск и завершение выполнения задач.

Анализ NewNodeLifecycleController

NewNodeLifecycleController в основном выполняет следующие задачи:
1. Построить большую структуру контроллера в соответствии с заданной конфигурацией и выполнить задачи по настройке некоторых параметров;
2. ДляpodInformer,nodeInformer,leaseInformerтак же какdaemonSetInformerНастройте соответствующие методы обратного вызова, включаяAddFunc,UpdateFuncтак же какDeleteFunc. Таким образом, при изменении соответствующего узла соответствующий контроллер может вовремя отслеживать его и вызывать соответствующий метод обработки;
3. Вернуть построенную структуру.
Есть несколько переменных, на которые необходимо обратить внимание при настройке, которые будут часто использоваться позже.

runTaintManager: указывает, что TaintManager запущен для удаления модулей из узла;
useTaintBasedEvictions: добавив в узелTaintNodeNotReadyа такжеTaintNodeUnreachableМетод taint заменяет предыдущий метод прямого исключения модулей, который удаляет модули посредством управления потоком. Главным образом для предотвращения внезапного выселения подов в большом количестве в определенный момент времени;
taintNodeByCondition: добавить соответствующую заразу в узел через состояние узла.
К тому же, в отличие от предыдущей версии, добавлен LeezeInformer, основная функция которого — судить о работоспособности Node.

Выполнить анализ метода

Метод Run в основном включает в себя следующие методы, каждый из которых запускается как отдельная горутина:
1,go nc.taintManager.Run(stopCh): TaintManager, который в основном выполняет задачу выселения пода;
2,doNoScheduleTaintingPassWorker: выполнить задачу обновления taint в NoSchedule;
3.doNoExecuteTaintingPass,doEvictionPass: выполнить задачу обновления испорченного кода NoExecute;
4.monitorNodeHealth: проверка состояния узла и выполнение таких задач, как добавление, удаление, изменение и проверка узла, а также обработка вытеснения пода.
код показывает, как показано ниже

// Run starts an asynchronous loop that monitors the status of cluster nodes.
func (nc *Controller) Run(stopCh <-chan struct{}) {
	defer utilruntime.HandleCrash()

	klog.Infof("Starting node controller")
	defer klog.Infof("Shutting down node controller")

	if !controller.WaitForCacheSync("taint", stopCh, nc.leaseInformerSynced, nc.nodeInformerSynced, nc.podInformerSynced, nc.daemonSetInformerSynced) {
		return
	}

	if nc.runTaintManager {
		go nc.taintManager.Run(stopCh)
	}

	if nc.taintNodeByCondition {
		// Close node update queue to cleanup go routine.
		defer nc.nodeUpdateQueue.ShutDown()

		// Start workers to update NoSchedule taint for nodes.
		for i := 0; i < scheduler.UpdateWorkerSize; i++ {
			// Thanks to "workqueue", each worker just need to get item from queue, because
			// the item is flagged when got from queue: if new event come, the new item will
			// be re-queued until "Done", so no more than one worker handle the same item and
			// no event missed.
			go wait.Until(nc.doNoScheduleTaintingPassWorker, time.Second, stopCh)
		}
	}

	if nc.useTaintBasedEvictions {
		// Handling taint based evictions. Because we don't want a dedicated logic in TaintManager for NC-originated
		// taints and we normally don't rate limit evictions caused by taints, we need to rate limit adding taints.
		go wait.Until(nc.doNoExecuteTaintingPass, scheduler.NodeEvictionPeriod, stopCh)
	} else {
		// Managing eviction of nodes:
		// When we delete pods off a node, if the node was not empty at the time we then
		// queue an eviction watcher. If we hit an error, retry deletion.
		go wait.Until(nc.doEvictionPass, scheduler.NodeEvictionPeriod, stopCh)
	}

	// Incorporate the results of node health signal pushed from kubelet to master.
	go wait.Until(func() {
		if err := nc.monitorNodeHealth(); err != nil {
			klog.Errorf("Error monitoring node health: %v", err)
		}
	}, nc.nodeMonitorPeriod, stopCh)

	<-stopCh
}

Процесс реализации

Процесс выполнения NodeLifecycleController — это в основном задачи, соответствующие каждой горутине, которые анализируются одна за другой.

TaintManager

TaintManager запускается методом Run. В методе Run в основном выполняются несколько задач:
1. ИнициализацияnodeUpdateChannelsа такжеpodUpdateChannels, размер 8 каналов, которые потом можно обрабатывать параллельно;
2. Запустите две горутины для мониторинга сообщений nodeUpdateQueue и podUpdateQueue соответственно;
3. Запустить 8 рабочих задач параллельно для обработки отслеживаемых сообщений nodeUpdate и podUpdate.

// Run starts NoExecuteTaintManager which will run in loop until `stopCh` is closed.
func (tc *NoExecuteTaintManager) Run(stopCh <-chan struct{}) {
	klog.V(0).Infof("Starting NoExecuteTaintManager")

	for i := 0; i < UpdateWorkerSize; i++ {
		tc.nodeUpdateChannels = append(tc.nodeUpdateChannels, make(chan nodeUpdateItem, NodeUpdateChannelSize))
		tc.podUpdateChannels = append(tc.podUpdateChannels, make(chan podUpdateItem, podUpdateChannelSize))
	}

	// Functions that are responsible for taking work items out of the workqueues and putting them
	// into channels.
	go func(stopCh <-chan struct{}) {
		for {
			item, shutdown := tc.nodeUpdateQueue.Get()
			if shutdown {
				break
			}
			nodeUpdate := item.(nodeUpdateItem)
			hash := hash(nodeUpdate.nodeName, UpdateWorkerSize)
			select {
			case <-stopCh:
				tc.nodeUpdateQueue.Done(item)
				return
			case tc.nodeUpdateChannels[hash] <- nodeUpdate:
				// tc.nodeUpdateQueue.Done is called by the nodeUpdateChannels worker
			}
		}
	}(stopCh)

	go func(stopCh <-chan struct{}) {
		for {
			item, shutdown := tc.podUpdateQueue.Get()
			if shutdown {
				break
			}
			podUpdate := item.(podUpdateItem)
			hash := hash(podUpdate.nodeName, UpdateWorkerSize)
			select {
			case <-stopCh:
				tc.podUpdateQueue.Done(item)
				return
			case tc.podUpdateChannels[hash] <- podUpdate:
				// tc.podUpdateQueue.Done is called by the podUpdateChannels worker
			}
		}
	}(stopCh)

	wg := sync.WaitGroup{}
	wg.Add(UpdateWorkerSize)
	for i := 0; i < UpdateWorkerSize; i++ {
		go tc.worker(i, wg.Done, stopCh)
	}
	wg.Wait()
}

В рабочих задачах, запущенных параллельно, событие nodeUpdate обрабатывается первым, а событие podUpdate обрабатывается после завершения обработки nodeUpdate. Соответствие методу обработки nodeUpdatehandleNodeUpdate, соответствующий podUpdatehandlePodUpdate.
handleNodeUpdateОсновная функция состоит в том, чтобы получить информацию об узле через отслеживаемое имя узла и получить соответствующие сведения об узле с помощью информации об узле. Затем последовательно выполняются все поды на узле.processPodOnNodeметод. Методы, как показано ниже:

func (tc *NoExecuteTaintManager) handleNodeUpdate(nodeUpdate nodeUpdateItem) {
	node, err := tc.getNode(nodeUpdate.nodeName)
	if err != nil {
		if apierrors.IsNotFound(err) {
			// Delete
			klog.V(4).Infof("Noticed node deletion: %#v", nodeUpdate.nodeName)
			tc.taintedNodesLock.Lock()
			defer tc.taintedNodesLock.Unlock()
			delete(tc.taintedNodes, nodeUpdate.nodeName)
			return
		}
		utilruntime.HandleError(fmt.Errorf("cannot get node %s: %v", nodeUpdate.nodeName, err))
		return
	}

	// Create or Update
	klog.V(4).Infof("Noticed node update: %#v", nodeUpdate)
	taints := getNoExecuteTaints(node.Spec.Taints)
	func() {
		tc.taintedNodesLock.Lock()
		defer tc.taintedNodesLock.Unlock()
		klog.V(4).Infof("Updating known taints on node %v: %v", node.Name, taints)
		if len(taints) == 0 {
			delete(tc.taintedNodes, node.Name)
		} else {
			tc.taintedNodes[node.Name] = taints
		}
	}()
	pods, err := getPodsAssignedToNode(tc.client, node.Name)
	if err != nil {
		klog.Errorf(err.Error())
		return
	}
	if len(pods) == 0 {
		return
	}
	// Short circuit, to make this controller a bit faster.
	if len(taints) == 0 {
		klog.V(4).Infof("All taints were removed from the Node %v. Cancelling all evictions...", node.Name)
		for i := range pods {
			tc.cancelWorkWithEvent(types.NamespacedName{Namespace: pods[i].Namespace, Name: pods[i].Name})
		}
		return
	}

	now := time.Now()
	for i := range pods {
		pod := &pods[i]
		podNamespacedName := types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name}
		tc.processPodOnNode(podNamespacedName, node.Name, pod.Spec.Tolerations, taints, now)
	}
}

handlePodUpdateПолучение информации об одном модуле и информации об узле также является окончательным выполнением.processPodOnNodeметод. Методы, как показано ниже:

func (tc *NoExecuteTaintManager) handlePodUpdate(podUpdate podUpdateItem) {
	pod, err := tc.getPod(podUpdate.podName, podUpdate.podNamespace)
	if err != nil {
		if apierrors.IsNotFound(err) {
			// Delete
			podNamespacedName := types.NamespacedName{Namespace: podUpdate.podNamespace, Name: podUpdate.podName}
			klog.V(4).Infof("Noticed pod deletion: %#v", podNamespacedName)
			tc.cancelWorkWithEvent(podNamespacedName)
			return
		}
		utilruntime.HandleError(fmt.Errorf("could not get pod %s/%s: %v", podUpdate.podName, podUpdate.podNamespace, err))
		return
	}

	// We key the workqueue and shard workers by nodeName. If we don't match the current state we should not be the one processing the current object.
	if pod.Spec.NodeName != podUpdate.nodeName {
		return
	}

	// Create or Update
	podNamespacedName := types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name}
	klog.V(4).Infof("Noticed pod update: %#v", podNamespacedName)
	nodeName := pod.Spec.NodeName
	if nodeName == "" {
		return
	}
	taints, ok := func() ([]v1.Taint, bool) {
		tc.taintedNodesLock.Lock()
		defer tc.taintedNodesLock.Unlock()
		taints, ok := tc.taintedNodes[nodeName]
		return taints, ok
	}()
	// It's possible that Node was deleted, or Taints were removed before, which triggered
	// eviction cancelling if it was needed.
	if !ok {
		return
	}
	tc.processPodOnNode(podNamespacedName, nodeName, pod.Spec.Tolerations, taints, time.Now())
}

processPodOnNodeМетод в основном добавляет поды, которые необходимо удалить в заранее определенном формате, вtaintEvictionQueue, задачи в очереди устанавливаются с рассчитанным временем задачи, и они вызываются в течение соответствующего времени.deletePodHandlerметод удаления модуля, этот метод находится вpkg/controller/nodelifecycle/scheduler/taint_manager.goВниз. Методы, как показано ниже:

func deletePodHandler(c clientset.Interface, emitEventFunc func(types.NamespacedName)) func(args *WorkArgs) error {
	return func(args *WorkArgs) error {
		ns := args.NamespacedName.Namespace
		name := args.NamespacedName.Name
		klog.V(0).Infof("NoExecuteTaintManager is deleting Pod: %v", args.NamespacedName.String())
		if emitEventFunc != nil {
			emitEventFunc(args.NamespacedName)
		}
		var err error
		for i := 0; i < retries; i++ {
			err = c.CoreV1().Pods(ns).Delete(name, &metav1.DeleteOptions{})
			if err == nil {
				break
			}
			time.Sleep(10 * time.Millisecond)
		}
		return err
	}
}

Поэтому основная функция TaintManager — настроить задачи удаления по расписанию для подов, которые необходимо исключить, а затем удалить их с соответствующего узла по одному.

doNoScheduleTaintingPassWorker

Когда функция taintNodeByCondition включена, она будет вызыватьсяdoNoScheduleTaintingPassWorkerперейти к узлуNoSchedule.Обновление пятен. называетсяdoNoScheduleTaintingPassметод. Методы, как показано ниже:

func (nc *Controller) doNoScheduleTaintingPass(nodeName string) error {
	node, err := nc.nodeLister.Get(nodeName)
	if err != nil {
		// If node not found, just ignore it.
		if apierrors.IsNotFound(err) {
			return nil
		}
		return err
	}

	// Map node's condition to Taints.
	var taints []v1.Taint
	for _, condition := range node.Status.Conditions {
		if taintMap, found := nodeConditionToTaintKeyStatusMap[condition.Type]; found {
			if taintKey, found := taintMap[condition.Status]; found {
				taints = append(taints, v1.Taint{
					Key:    taintKey,
					Effect: v1.TaintEffectNoSchedule,
				})
			}
		}
	}
	if node.Spec.Unschedulable {
		// If unschedulable, append related taint.
		taints = append(taints, v1.Taint{
			Key:    schedulerapi.TaintNodeUnschedulable,
			Effect: v1.TaintEffectNoSchedule,
		})
	}

	// Get exist taints of node.
	nodeTaints := taintutils.TaintSetFilter(node.Spec.Taints, func(t *v1.Taint) bool {
		// only NoSchedule taints are candidates to be compared with "taints" later
		if t.Effect != v1.TaintEffectNoSchedule {
			return false
		}
		// Find unschedulable taint of node.
		if t.Key == schedulerapi.TaintNodeUnschedulable {
			return true
		}
		// Find node condition taints of node.
		_, found := taintKeyToNodeConditionMap[t.Key]
		return found
	})
	taintsToAdd, taintsToDel := taintutils.TaintSetDiff(taints, nodeTaints)
	// If nothing to add not delete, return true directly.
	if len(taintsToAdd) == 0 && len(taintsToDel) == 0 {
		return nil
	}
	if !nodeutil.SwapNodeControllerTaint(nc.kubeClient, taintsToAdd, taintsToDel, node) {
		return fmt.Errorf("failed to swap taints of node %+v", node)
	}
	return nil
}

doNoScheduleTaintingPassВ основном выполняют следующие работы:
1. Получить информацию об узле в соответствии с nodeName;
2. Судите по полю node.Status.Conditions, нужно ли узлу добавлять пометку NoSchedule. Критерии оценки следующие:

Пока он находится в любом состоянии, необходимо добавить пометку NoSchedule;
3. Если узел находится в состоянии Unschedulable, также добавьте пометку NoSchedule;
4. По имеющимся пятнам на Node определить, какие пятна, добавленные ранее, нужно добавить, а какие удалить;
5. звонокSwapNodeControllerTaintОбновление состояния заражения на Node.

doNoExecuteTaintingPass и doEvictionPass

doNoExecuteTaintingPassа такжеdoEvictionPassОба будут выполнять только одно или другое.

Когда функция useTaintBasedEvictions включена, вызовитеdoNoExecuteTaintingPassдобавлен метод для узлаNoExecuteпятно; иdoEvictionPassОн заключается в том, чтобы напрямую определить, какие поды необходимо удалить, и перейти непосредственно к работе по удалению.
doNoExecuteTaintingPassметод, получивzoneNoExecuteTainterДанные внутри оценивают состояние узла, при необходимости добавляют пометку NoExecute и вызываютSwapNodeControllerTaintметод для обновления taint на этом узле. Информация zoneNoExecuteTainter передается черезmonitorNodeHealthМетод получен, и он будет проанализирован позже.doNoExecuteTaintingPassМетод заключается в следующем:
doEvictionPassполучается непосредственноzonePodEvictorЧтобы определить, какие поды необходимо исключить, вызовите интерфейс DELETE пода, чтобы выполнить задачу вытеснения пода. Информация zonePodEvictor также передается черезmonitorNodeHealthметод получен.doEvictionPassМетоды, как показано ниже:
Разница между двумя методами заключается в том, чтоdoNoExecuteTaintingPassПросто размазать Node, иdoEvictionPassЗатем завершается окончательное удаление.doEvictionPassТаким образом, большое количество подов необходимо удалить в течение определенного периода времени, что будет генерировать много трафика; иdoNoExecuteTaintingPassРазмазав узел, пусть TaintManager выполнит окончательное удаление Pod.Задача удаления TaintManager выполняется регулярно по периодам времени, поэтому такой большой проблемы с трафиком не будет. Поэтому рекомендуется включить эту функцию и добавить ее в параметры запуска kube-controller-manager.--feature-gates=TaintBasedEvictions=trueВот и все.

monitorNodeHealth

Задачи первых нескольких горутин в основном вращаются вокруг Taint, аmonitorNodeHealthЭто регулярно обновлять информацию о узле и генерировать источник данных.
monitorNodeHealthОсновные задачи можно разделить на следующие этапы:
1. Получить всю информацию об узлах, в соответствии с тем, какие из них были добавлены вновь, какие из них необходимо удалить, а какие являются соответствующей информацией о возвращенных узлах, которые необходимо перепланировать;

2. Выполнить соответствующие операции обработки по добавлению Узлов, удалению Узлов и Планированию Узлов;
3. Пройдитесь по всем узлам, обновите статус узла и вызовитеtryUpdateNodeHealthметод;
4. В соответствии с полученным состоянием узла сравните его с исходным состоянием узла и соответствующим образом отметьте узел. Этот код длинный, основная структура следующая
5. Для проблемы прерывания сети узла настройте соответствующую скорость выселения в соответствии с различными состояниями узла и вызовитеhandleDisruptionметод.
Затем проанализируйте каждый шаг один за другим.

шаг 1

Сначала получите всю информацию об узле через интерфейс List, черезclassifyNodesЗавершите разделение Node.classifyNodesПравило деления очень простое, сравнениеknownNodeSetа такжеallNodes, что можно понимать какknownNodeSetдля последних данных,allNodesновые данные, то:
1. Если вallNodesсуществуют, вknownNodeSetне существует, это недавно добавленный узел;
2. Если вknownNodeSetсуществуют, вallNodesНе существует, это удаленный узел;
3. Если вknownNodeSetа такжеallNodesВсе существуют, но нет состояний зоны, которые являются узлами newZoneRepresentatives. Каждый узел должен принадлежать Зоне.

Шаг 2

После завершения разделения узлов на шаге 1 на шаге 2 выполняются соответствующие операции обработки для каждого типа узла.
1. Добавьте Node для добавления в кэш в knownNodeSet и передайтеaddPodEvictorForNewZoneЧтобы он принадлежал Зоне, с помощью переключателя useTaintBasedEvictions решается, следует ли пометить узел как достижимый или отменить исключение модуля. Короче говоря, это означает, что узел можно использовать в обычном режиме;
2. Для удаляемого узла удалите его из набора известных узлов;
3. Узлы, не разделенные на Зоны, добавляются в Кэш Зон.

Шаг 3

Для всех полученных узлов звонитеPollImmediateметод каждые 20 мс повторяет 5 попыток обновления состояния узла, в основном вызываяtryUpdateNodeHealthметод.tryUpdateNodeHealthСреди значений метода основное внимание уделяетсяobservedReadyConditionа такжеcurrentReadyCondition. можно понимать какobservedReadyConditionУказывает последнее состояние узла,currentReadyConditionПредставляет текущее состояние узла. Следующие множественные операторы if-else работают с этими двумя значениями.

Шаг 4

Все большое утверждение начинается сcurrentReadyConditionЕсли он не пуст, его можно разделить на следующие ситуации:
1,observedReadyConditionЗначение равно False, то есть узел не готов, пометьте узел с помощью node.kubernetes.io/not-ready:NoExecute taint или напрямую вытесните под на узле;
2,observedReadyConditionЕсли значение равно Unknown, пометьте Node.kubernetes.io/unreachable:NoExecute как taint или напрямую изгоните Pod на Node;
3.observedReadyConditionЗначение равно True, что указывает на то, что Node находится в нормальном рабочем состоянии, помечая Node как Reachable или останавливая операцию исключения Pod;
4.currentReadyConditionне для правдыobservedReadyConditionЕсли это правда, это означает, что узел находится в состоянии «Не готов», помечая узел как «Не готов» и обновляя статус пода на узле;
5.currentReadyConditionЕсли это не True и настроен облачный провайдер, он удалит Node.
Основная задача всего большого цикла — судить о статусе Node, помечать пометку Node или исключать связанные операции.zoneNoExecuteTainterа такжеzonePodEvictorИнформация обоих наборов данных соответственно обновляется здесь.

Шаг 5

Последний вызовhandleDisruptionВыполните некоторые связанные операции обработки для сетевых прерываний.
Прерывания в основном имеют следующие состояния:

существуетhandleDisruptionв, черезallAreFullyDisruptedа такжеallWasFullyDisruptedОтметьте текущее состояние зоны и ранее кэшированное состояние зоны, указав последний результат и информацию о последнем результате соответственно. Затем выполните три операции обработки:
1. Если новое состояниеfullDisruption, то есть полное прерывание, указывающее на то, что все Узлы находятся в состоянии Not Ready.В это время нормальная скорость вытеснения восстанавливается, и операция вытеснения останавливается;
2. Если новое состояниеpartialDisruption, то есть частичное прерывание, указывающее на то, что некоторые Узлы находятся в состоянии Not Ready, и в это время установлена ​​определяемая пользователем скорость выселения;
3, если новое состояниеnormal, возвращается к коэффициенту выселения по умолчанию.
Основная цель состоит в том, чтобы контролировать скорость вытеснения в соответствии с различными состояниями прерывания для поддержания стабильности системы.
На этом задача NodeLifecycleController завершена. Основной процесс заключается в обновлении информации об узле в соответствии с различными состояниями узла, отметке/удалении соответствующего дефекта на узле, чтобы убедиться, что узел можно запланировать, и регулярном выполнении операции выселения пода.