Análisis de caché del programador de kube

Prefacio

Antes de leer este artículo, se recomienda comenzar con "Análisis de la SchedulingQueue de kube-planificador" , porque el autor comenzó a analizar la implementación del código fuente de kube-planificador de forma gradual y en profundidad con la cola de programación como un gran avance. Como se resume en "Análisis de SchedulingQueue del programador de kube" , la cola de programación (SchedulingQueue) son todos los pods en estado Pendiente, es decir, pods no programados. La caché analizada en este artículo son todos pods programados (incluidos los pods programados asumidos). Y Cache no es solo para almacenar Pods programados para facilitar la búsqueda, sino para proporcionar información de estado muy importante para la programación, incluso más allá del alcance de la definición de Cache en sí.

Dado que se define como caché, se deben responder las siguientes preguntas:

  1. ¿Quién es el caché? La información de kubernetes se almacena en etcd, y la única forma de acceder a etcd de kubernetes es a través de apiserver, por lo que es preciso almacenar en caché la información de etcd.
  2. ¿Qué información se almacena en caché? El programador necesita programar el Pod en el Nodo que cumpla con la demanda, por lo que la caché debe al menos almacenar en caché la información del Pod y del Nodo, a fin de mejorar el rendimiento del acceso del programador de kube al apiserver.
  3. ¿Por qué caché? Dado que client-go (el autor tiene algunos artículos sobre client-go, puede leer estos artículos si los ignora) ya proporciona capacidades de caché, ¿cuál es el propósito de agregar una capa de caché a kube-Scheduler? La respuesta es simple, para programar. El caché de este artículo no solo almacena en caché la información del pod y el nodo, sino que, lo que es más importante, agrega los resultados de la programación para facilitar la programación, que es el tema central de este artículo.

Para evitar que la traducción de Node pierda su significado original, este artículo cita directamente a Node en lugar de traducirlo a nodos, servidores, etc. Al mismo tiempo, también evita la ambigüedad de traducción de Bind y lo cita directamente sin traducción como vinculante.

El código fuente utilizado en este artículo es la rama release-1.20 de kubenretes, el enlace al documento de la última versión de Kubernetes: https://github.com/jindezgm/k8s-src-analysis/blob/master/kube-scheduler/Cache.md

Cache

Abstracción de caché

El autor ya ha respondido las 3 preguntas de Cache en el artículo anterior, entonces, ¿puedo encontrar algunas respuestas del diseño de la interfaz de Cache primero? Enlace fuente: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/internal/cache/interface.go#L58

type Cache interface {
    // 获取node的数量,用于单元测试使用,本文不做说明
	NodeCount() int
    // 获取Pod的数量,用于单元测试使用,本文不做说明
	PodCount() (int, error)

    // 此处需要给出一个概念:假定Pod,就是将Pod假定调度到指定的Node,但还没有Bind完成。
    // 为什么要这么设计?因为kube-scheduler是通过异步的方式实现Bind,在Bind完成前,
    // 调度器还要调度新的Pod,此时就先假定Pod调度完成了。至于什么是Bind?为什么Bind?
    // 怎么Bind?笔者会在其他文章中解析,此处简单理解为:需要将Pod的调度结果写入etcd,
    // 持久化调度结果,所以也是相对比较耗时的操作。
    // AssumePod会将Pod的资源需求累加到Node上,这样kube-scheduler在调度其他Pod的时候,
    // 就不会占用这部分资源。
	AssumePod(pod *v1.Pod) error

    // 前面提到了,Bind是一个异步过程,当Bind完成后需要调用这个接口通知Cache,
    // 如果完成Bind的Pod长时间没有被确认(确认方法是AddPod),那么Cache就会清理掉假定过期的Pod。
	FinishBinding(pod *v1.Pod) error

	// 删除假定的Pod,kube-scheduler在调用AssumePod后如果遇到其他错误,就需要调用这个接口
	ForgetPod(pod *v1.Pod) error

    // 添加Pod既确认了假定的Pod,也会将假定过期的Pod重新添加回来。
	AddPod(pod *v1.Pod) error

	// 更新Pod,其实就是删除再添加
	UpdatePod(oldPod, newPod *v1.Pod) error

	// 删除Pod.
	RemovePod(pod *v1.Pod) error

	// 获取Pod.
	GetPod(pod *v1.Pod) (*v1.Pod, error)

	// 判断Pod是否假定调度
	IsAssumedPod(pod *v1.Pod) (bool, error)

	// 添加Node的全部信息
	AddNode(node *v1.Node) error

	// 更新Node的全部信息
	UpdateNode(oldNode, newNode *v1.Node) error

	// 删除Node的全部信息
	RemoveNode(node *v1.Node) error

	// 其实就是产生Cache的快照并输出到nodeSnapshot中,那为什么是更新呢?
    // 因为快照比较大,产生快照也是一个比较重的任务,如果能够基于上次快照把增量的部分更新到上一次快照中,
    // 就会变得没那么重了,这就是接口名字是更新快照的原因。文章后面会重点分析这个函数,
    // 因为其他接口非常简单,理解了这个接口基本上就理解了Cache的精髓所在。
	UpdateSnapshot(nodeSnapshot *Snapshot) error

	// Dump会快照Cache,用于调试使用,不是重点,所以本文不会对该函数做说明。
	Dump() *Dump
}

Se puede ver en el diseño de la interfaz de Cache que Cache solo almacena en caché la información de Pod y Node, mientras que la información de Pod y Node se almacena en etcd (puede agregar, eliminar, modificar y verificar a través de kubectl), por lo que puede confirmar que Cache almacena en caché Pod e información de nodo en etcd.

Definición de NodeInfo

En SchedulingQueue, la cola de programación define el tipo QueuedPodInfo y expande los atributos relacionados con la cola de programación sobre la base de la API de Pod. De la misma manera, la API de Nodo es solo un atributo público de Node, y el Nodo en el Cache necesita extender los atributos relacionados con el Cache, por lo que existe el tipo NodeInfo. Enlace fuente: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/framework/types.go#L189

// NodeInfo是Node层的汇聚信息
type NodeInfo struct {
	// Node API对象,无需过多解释
	node *v1.Node
	// 运行在Node上的所有Pod,PodInfo的定义读者自己查看,本文不再扩展了
	Pods []*PodInfo
	// PodsWithAffinity是Pods的子集,所有的Pod都声明了亲和性
	PodsWithAffinity []*PodInfo
	// PodsWithRequiredAntiAffinity是Pods子集,所有的Pod都声明了反亲和性
	PodsWithRequiredAntiAffinity []*PodInfo
	// 本文无关,忽略
	UsedPorts HostPortInfo
	// 此Node上所有Pod的总Request资源,包括假定的Pod,调度器已发送该Pod进行绑定,但可能尚未对其进行调度。
	Requested *Resource
	// Pod的容器资源请求有的时候是0,kube-scheduler为这类容器设置默认的资源最小值,并累加到NonZeroRequested.
    // 也就是说,NonZeroRequested等于Requested加上所有按照默认最小值累加的零资源
    // 这并不反映此节点的实际资源请求,而是用于避免将许多零资源请求的Pod调度到一个Node上。
	NonZeroRequested *Resource
	// Node的可分配的资源量
	Allocatable *Resource
	// 镜像状态,比如Node上有哪些镜像,镜像的大小,有多少Node相应的镜像等。
	ImageStates map[string]*ImageStateSummary
	// 与本文无关,忽略
	TransientInfo *TransientSchedulerInfo
    // 类似于版本,NodeInfo的任何状态变化都会使得Generation增加,比如有新的Pod调度到Node上
    // 这个Generation很重要,可以用于只复制变化的Node对象,后面更新镜像的时候会详细说明
	Generation int64
}

nodeTree

NodeTree organiza los nodos en una estructura de árbol según las zonas. Cuando sea necesario enumerar por zona u ordenar por zona en su totalidad, se utilizará nodeTree. ¿Por qué existe tal demanda, o esa sentencia, la necesidad de programación? Dé un ejemplo que puede no ser apropiado: por ejemplo, es necesario implementar varias copias de Pod en la misma área o en áreas diferentes.

Conexión de fuente: https://github.com/kubernetes/kubernetes/blob/master/pkg/scheduler/internal/cache/node_tree.go#L32

type nodeTree struct {
    // map的键是zone名字,map的值是该区域内所有Node的名字。
	tree     map[string][]string 
    // 所有的zone的名字
	zones    []string
    // Node的数量
	numNodes int
}

nodeTree simplemente organiza los nombres de los nodos en un árbol. Si necesita NodeInfo, necesita encontrar NodeInfo según el nombre del nodo.

Instantánea

Una instantánea es una copia de la caché en un momento determinado. A medida que pasa el tiempo, el estado de la caché se actualiza continuamente. Cuando el programador kube programa un Pod, necesita obtener una instantánea de la caché. En comparación con el acceso directo a la caché, las instantáneas pueden resolver los siguientes problemas:

  1. No habrá cambios en la instantánea, que puede entenderse como de solo lectura, por lo que no es necesario bloquear el acceso a la instantánea para garantizar la atomicidad;
  2. Snapshot y Cache separan las lecturas y escrituras, lo que puede evitar que los bloqueos a gran escala provoquen una degradación del rendimiento del acceso a la caché;

Aunque el estado de la instantánea se retrasa (porque la caché puede actualizarse en cualquier momento) la caché desde el comienzo de la creación, no es un problema para el programador de kube programar un Pod. En cuanto a la razón, explicaré la razón en el proceso de análisis y programación.

Enlace fuente: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/internal/cache/snapshot.go#L29

// 从定义上看,快照只有Node信息,没有Pod信息,其实Node信息中已经有Pod信息了,这个在NodeInfo中已经说明了
type Snapshot struct {
	// nodeInfoMap用于根据Node的key(NS+Name)快速查找Node
	nodeInfoMap map[string]*framework.NodeInfo
	// nodeInfoList是Cache中Node全集列表(不包含已删除的Node),按照nodeTree排序.
	nodeInfoList []*framework.NodeInfo
    // 只要Node上有任何Pod声明了亲和性,那么该Node就要放入havePodsWithAffinityNodeInfoList。
    // 为什么要有这个变量?当然是为了调度,比如PodA需要和PodB调度在一个Node上。
	havePodsWithAffinityNodeInfoList []*framework.NodeInfo
	// havePodsWithRequiredAntiAffinityNodeInfoList和havePodsWithAffinityNodeInfoList相似,
    // 只是Pod声明了反亲和,比如PodA不能和PodB调度在一个Node上
	havePodsWithRequiredAntiAffinityNodeInfoList []*framework.NodeInfo
    // generation是所有NodeInfo.Generation的最大值,因为所有NodeInfo.Generation都源于一个全局的Generation变量,
    // 那么Cache中的NodeInfo.Gerneraion大于该值的就是在快照产生后更新过的Node。
    // kube-scheduler调用Cache.UpdateSnapshot的时候只需要更新快照之后有变化的Node即可
	generation                                   int64
}

Implementación de caché

El pavimento anterior es suficiente. Ahora ingresemos el contenido de la clave. Echemos un vistazo a la definición de la clase de implementación Cache SchedulerCache. Enlace fuente: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/internal/cache/cache.go#L57

// schedulerCache实现了Cache接口
type schedulerCache struct {
    // 这个比较好理解,用来通知schedulerCache停止的chan,说明schedulerCache有自己的协程
	stop   <-chan struct{}
    // 假定Pod一旦完成绑定,就要在指定的时间内确认,否则就会超时,ttl就是指定的过期时间,默认30秒
	ttl    time.Duration
    // 定时清理“假定过期”的Pod,period就是定时周期,默认是1秒钟
    // 前面提到了schedulerCache有自己的协程,就是定时清理超时的假定Pod.
	period time.Duration

	// 锁,说明schedulerCache利用互斥锁实现协程安全,而不是用chan与其他协程交互。
    // 这一点实现和SchedulingQueue是一样的。
	mu sync.RWMutex
    // 假定Pod集合,map的key与podStates相同,都是Pod的NS+NAME,值为true就是假定Pod
    // 其实assumedPods的值没有false的可能,感觉assumedPods用set类型(map[string]struct{}{})更合适
	assumedPods map[string]bool
	// 所有的Pod,此处用的是podState,后面有说明,与SchedulingQueue中提到的QueuedPodInfo类似,
    // podState继承了Pod的API定义,增加了Cache需要的属性
	podStates map[string]*podState
    // 所有的Node,键是Node.Name,值是nodeInfoListItem,后面会有说明,只需要知道map类型就可以了
	nodes     map[string]*nodeInfoListItem
    // 所有的Node再通过双向链表连接起来
	headNode *nodeInfoListItem
    // 节点按照zone组织成树状,前面提到用nodeTree中Node的名字再到nodes中就可以查找到NodeInfo.
	nodeTree *nodeTree
	// 镜像状态,本文不做重点说明,只需要知道Cache还统计了镜像的信息就可以了。
	imageStates map[string]*imageState
}

// podState与继承了Pod的API类型定义,同时扩展了schedulerCache需要的属性.
type podState struct {
	pod *v1.Pod
	// 假定Pod的超时截止时间,用于判断假定Pod是否过期。
	deadline *time.Time
    // 调用Cache.AssumePod的假定Pod不是所有的都需要判断是否过期,因为有些假定Pod可能还在Binding
    // bindingFinished就是用于标记已经Bind完成的Pod,然后开始计时,计时的方法就是设置deadline
    // 还记得Cache.FinishBinding接口么?就是用来设置bindingFinished和deadline的,后面代码会有解析
	bindingFinished bool
}

// nodeInfoListItem定义了nodeInfoList双向链表的item,nodeInfoList的实现非常简单,不多解释。
type nodeInfoListItem struct {
	info *framework.NodeInfo
	next *nodeInfoListItem
	prev *nodeInfoListItem
}

La pregunta es, dado que ya hay variables de nodos (tipo de mapa), ¿por qué agregar otra variable headNode (tipo de lista)? ¿No es superfluo esto? De hecho, no lo es. Los nodos pueden encontrar rápidamente Node basándose en el nombre de Node, mientras que headNode se ordena de acuerdo con una regla determinada. Este punto es el mismo que el uso de map / slice para implementar colas introducidas en SchedulingQueue. En cuanto a por qué se usa la lista en lugar de la división, debe ser que la eficiencia del método de clasificación de la lista vinculada sea mayor que la de la división. Explíquelo más adelante en la actualización de headNode, y lo excluiremos aquí.

A partir de la definición de SchedulerCache, básicamente puede adivinar la implementación de la mayoría de las interfaces de Cache. Este artículo solo explica brevemente la implementación de la interfaz relativamente simple y coloca el texto en algunas funciones clave. Las dos funciones PodCount y NodeCount se utilizan para pruebas unitarias, por lo que este artículo no las explicará.

AssumePod

Cuando el programador de kube encuentra el pod de programación de nodo óptimo, llamará a AssumePod asumiendo la programación del pod y el enlace asincrónico a través de otra corrutina. Suponiendo que en realidad está ocupando previamente los recursos, el programador de kube no tomará esta parte de los recursos al programar el próximo Pod, hasta que el mensaje de confirmación AddPod confirme el éxito de la programación, o el Bind falla, ForgetPod cancela la programación hipotética. Enlace de código: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/internal/cache/cache.go#L361

func (cache *schedulerCache) AssumePod(pod *v1.Pod) error {
	// 获取Pod的唯一key,就是NS+Name,因为kube-scheduler调度整个集群的Pod
    key, err := framework.GetPodKey(pod)
	if err != nil {
		return err
	}

	cache.mu.Lock()
	defer cache.mu.Unlock()
    // 如果Pod已经存在,则不能假定调度。因为在Cache中的Pod要么是假定调度的,要么是完成调度的
	if _, ok := cache.podStates[key]; ok {
		return fmt.Errorf("pod %v is in the cache, so can't be assumed", key)
	}
    
    // 见下面代码注释
	cache.addPod(pod)
	ps := &podState{
		pod: pod,
	}
    // 把Pod添加到map中,并标记为assumed
	cache.podStates[key] = ps
	cache.assumedPods[key] = true
	return nil
}

func (cache *schedulerCache) addPod(pod *v1.Pod) {
    // 查找Pod调度的Node,如果不存在则创建一个虚Node,虚Node只是没有Node API对象。
    // 为什么会这样?可能kube-scheduler调度Pod的时候Node被删除了,可能很快还会添加回来
    // 也可能就彻底删除了,此时先放在这个虚的Node上,如果Node不存在后期还会被迁移。
	n, ok := cache.nodes[pod.Spec.NodeName]
	if !ok {
		n = newNodeInfoListItem(framework.NewNodeInfo())
		cache.nodes[pod.Spec.NodeName] = n
	}
    // AddPod就是把Pod的资源累加到NodeInfo中,本文不做详细说明,感兴趣的读者自行查看源码
    // 但需要知道的是n.info.AddPod(pod)会更新NodeInfo.Generation,表示NodeInfo是最新的
	n.info.AddPod(pod)
    // 将Node放到schedulerCache.headNode队列头部,因为NodeInfo当前是最新的,所以放在头部。
    // 此处可以解答为什么用list而不是slice,因为每次都是将Node直接放在第一个位置,明显list效率更高
    // 所以headNode是按照最近更新排序的
	cache.moveNodeInfoToHead(pod.Spec.NodeName)
}

ForgetPod

Suponiendo que el pod ocupa algunos recursos por adelantado, si hay algún error en la operación posterior (como Bind), debe cancelar la programación hipotética y liberar los recursos. Enlace de código: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/internal/cache/cache.go#L406

func (cache *schedulerCache) ForgetPod(pod *v1.Pod) error {
	// 获取Pod唯一key
    key, err := framework.GetPodKey(pod)
	if err != nil {
		return err
	}

	cache.mu.Lock()
	defer cache.mu.Unlock()
    // 这里有意思了,也就是说Cache假定Pod的Node名字与传入的Pod的Node名字不一致,则返回错误
    // 这种情况会不会发生呢?有可能,但是可能性不大,毕竟多协程修改Pod调度状态会有各种可能性。
    // 此处留挖一个坑,在解析kube-scheduler调度流程的时候看看到底什么极致的情况会触发这种问题。
	currState, ok := cache.podStates[key]
	if ok && currState.pod.Spec.NodeName != pod.Spec.NodeName {
		return fmt.Errorf("pod %v was assumed on %v but assigned to %v", key, pod.Spec.NodeName, currState.pod.Spec.NodeName)
	}

	switch {
	// 只有假定Pod可以被Forget,因为Forget就是为了取消假定Pod的。
	case ok && cache.assumedPods[key]:
        // removePod()就是把假定Pod的资源从NodeInfo中减去,见下面代码注释
		err := cache.removePod(pod)
		if err != nil {
			return err
		}
        // 删除Pod和假定状态
		delete(cache.assumedPods, key)
		delete(cache.podStates, key)
    // 要么Pod不存在,要么Pod已确认调度,这两者都不能够被Forget
	default:
		return fmt.Errorf("pod %v wasn't assumed so cannot be forgotten", key)
	}
	return nil
}

func (cache *schedulerCache) removePod(pod *v1.Pod) error {
    // 找到假定Pod调度的Node
	n, ok := cache.nodes[pod.Spec.NodeName]
	if !ok {
		klog.Errorf("node %v not found when trying to remove pod %v", pod.Spec.NodeName, pod.Name)
		return nil
	}
    // 减去假定Pod的资源,并从NodeInfo的Pod列表移除假定Pod
    // 和n.info.AddPod相同,也会更新NodeInfo.Generation
	if err := n.info.RemovePod(pod); err != nil {
		return err
	}
    // 如果NodeInfo的Pod列表没有任何Pod并且Node被删除,则Node从Cache中删除
    // 否则将NodeInfo移到列表头,因为NodeInfo被更新,需要放到表头
    // 这里需要知道的是,Node被删除Cache不会立刻删除该Node,需要等到Node上所有的Pod从Node中迁移后才删除,
    // 具体实现逻辑后续文章会给出,此处先知道即可。
	if len(n.info.Pods) == 0 && n.info.Node() == nil {
		cache.removeNodeInfoFromList(pod.Spec.NodeName)
	} else {
		cache.moveNodeInfoToHead(pod.Spec.NodeName)
	}
	return nil
}

FinishBinding

Cuando se completa el enlace del Pod supuesto, debes llamar a FinishBinding para notificar al Cache que comience a cronometrar hasta que el Pod supuesto caduque. Si la solicitud de AddPod aún no se recibe, el Pod supuesto caducado se eliminará. Enlace de código: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/internal/cache/cache.go#L382

func (cache *schedulerCache) FinishBinding(pod *v1.Pod) error {
    // 取当前时间
	return cache.finishBinding(pod, time.Now())
}

func (cache *schedulerCache) finishBinding(pod *v1.Pod, now time.Time) error {
    // 获取Pod唯一key
	key, err := framework.GetPodKey(pod)
	if err != nil {
		return err
	}

	cache.mu.RLock()
	defer cache.mu.RUnlock()

	klog.V(5).Infof("Finished binding for pod %v. Can be expired.", key)
    // Pod存在并且是假定状态才行
	currState, ok := cache.podStates[key]
	if ok && cache.assumedPods[key] {
        // 标记为完成Binding,并且设置过期时间,还记得ttl默认是多少么?30秒。
		dl := now.Add(cache.ttl)
		currState.bindingFinished = true
		currState.deadline = &dl
	}
	return nil
}

AddPod

Cuando el Pod Bind sea exitoso, el programador kube recibirá el mensaje y luego llamará a AddPod para confirmar el resultado de la programación. Enlace de código: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/internal/cache/cache.go#L476

func (cache *schedulerCache) AddPod(pod *v1.Pod) error {
    // 获取Pod唯一key
	key, err := framework.GetPodKey(pod)
	if err != nil {
		return err
	}

	cache.mu.Lock()
	defer cache.mu.Unlock()
    
    // 以下是根据Pod在Cache中的状态决定需要如何处理
	currState, ok := cache.podStates[key]
	switch {
    // Pod是假定调度
	case ok && cache.assumedPods[key]:
        // Pod实际调度的Node和假定的不一致?
		if currState.pod.Spec.NodeName != pod.Spec.NodeName {
			klog.Warningf("Pod %v was assumed to be on %v but got added to %v", key, pod.Spec.NodeName, currState.pod.Spec.NodeName)
            // 如果不一致,先从假定调度的NodeInfo中减去Pod占用的资源,然后在累加到新NodeInfo中
            // 这种情况会在什么时候发生?还是留给后续文章分解吧
			if err = cache.removePod(currState.pod); err != nil {
				klog.Errorf("removing pod error: %v", err)
			}
			cache.addPod(pod)
		}
        // 删除假定状态
		delete(cache.assumedPods, key)
        // 清空假定过期时间,理论上从cache.assumedPods删除,假定过期时间自然也就失效了
		cache.podStates[key].deadline = nil
        // 这里有意思了,为什么要在赋值一次?currState中不是已经在AssumePod的时候设置了么?
        // 道理很简单,这是同一个Pod的两个副本,而当前参数‘pod’版本更新
		cache.podStates[key].pod = pod
    // Pod不存在
	case !ok:
		// Pod可能已经假定过期被删除了,需要重新添加回来
		cache.addPod(pod)
		ps := &podState{
			pod: pod,
		}
		cache.podStates[key] = ps
    // Pod已经执行过AddPod,有句高大上名词叫什么来着?对了,幂等!
	default:
		return fmt.Errorf("pod %v was already in added state", key)
	}
	return nil
}

 RemovePod

kube-Scheduler recibe una solicitud para eliminar un Pod. Si el Pod está en la caché, debe llamar a RemovePod. Enlace de código: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/internal/cache/cache.go#L541

func (cache *schedulerCache) RemovePod(pod *v1.Pod) error {
    // 获取Pod唯一key
	key, err := framework.GetPodKey(pod)
	if err != nil {
		return err
	}

	cache.mu.Lock()
	defer cache.mu.Unlock()

    // 根据Pod在Cache中的状态执行相应的操作
	currState, ok := cache.podStates[key]
	switch {
    // 只有执行AddPod的Pod才能够执行RemovePod,假定Pod是不会执行RemovePod的,为什么?
    // 我只能说就是这么设计的,假定Pod是不会执行这个函数的,这涉及到Pod删除的全流程,
    // 已经超纲了。。。,我肯定会有文章解析,此处再挖一个坑。
	case ok && !cache.assumedPods[key]:
        // 卧槽,Pod的Node和AddPod时的Node不一样?这回的选择非常直接,奔溃,已经超时异常解决范围了
        // 如果再继续下去可能会造成调度状态的混乱,不如重启再来。
		if currState.pod.Spec.NodeName != pod.Spec.NodeName {
			klog.Errorf("Pod %v was assumed to be on %v but got added to %v", key, pod.Spec.NodeName, currState.pod.Spec.NodeName)
			klog.Fatalf("Schedulercache is corrupted and can badly affect scheduling decisions")
		}
        // 从NodeInfo中减去Pod的资源
		err := cache.removePod(currState.pod)
		if err != nil {
			return err
		}
        // 从Cache中删除Pod
		delete(cache.podStates, key)
	default:
		return fmt.Errorf("pod %v is not found in scheduler cache, so cannot be removed from it", key)
	}
	return nil
}

 AddNode

Cuando se agrega un nuevo nodo al clúster, kube-Scheduler llama a esta interfaz para notificar al caché. Enlace de código: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/internal/cache/cache.go#L605

func (cache *schedulerCache) AddNode(node *v1.Node) error {
	cache.mu.Lock()
	defer cache.mu.Unlock()

	n, ok := cache.nodes[node.Name]
	if !ok {
        // 如果NodeInfo不存在则创建
		n = newNodeInfoListItem(framework.NewNodeInfo())
		cache.nodes[node.Name] = n
	} else {
        // 已存在,先删除镜像状态,因为后面还会在添加回来
		cache.removeNodeImageStates(n.info.Node())
	}
    // 将Node放到列表头
	cache.moveNodeInfoToHead(node.Name)

    // 添加到nodeTree中
	cache.nodeTree.addNode(node)
    // 添加Node的镜像状态,感兴趣的读者自行了解,本文不做重点
	cache.addNodeImageStates(node, n.info)
    // 只有SetNode的NodeInfo才是真实的Node,否则就是前文提到的虚的Node
	return n.info.SetNode(node)
}

RemoveNode

El nodo se elimina del clúster y el programador kube llama a esta interfaz para notificar a Cache. Enlace de código: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/internal/cache/cache.go#L648

func (cache *schedulerCache) RemoveNode(node *v1.Node) error {
	cache.mu.Lock()
	defer cache.mu.Unlock()

    // 如果Node不存在返回错误
	n, ok := cache.nodes[node.Name]
	if !ok {
		return fmt.Errorf("node %v is not found", node.Name)
	}
    // RemoveNode就是将*v1.Node设置为nil,此时Node就是虚的了
	n.info.RemoveNode()
    // 当Node上没有运行Pod的时候删除Node,否则把Node放在列表头,因为Node状态更新了
    // 熟悉etcd的同学会知道,watch两个路径(Node和Pod)是两个通道,这样会造成两个通道的事件不会按照严格时序到达
    // 这应该是存在虚Node的原因之一。
	if len(n.info.Pods) == 0 {
		cache.removeNodeInfoFromList(node.Name)
	} else {
		cache.moveNodeInfoToHead(node.Name)
	}
    // 虽然nodes只有在NodeInfo中Pod数量为零的时候才会被删除,但是nodeTree会直接删除
    // 说明nodeTree中体现了实际的Node状态,kube-scheduler调度Pod的时候也是利用nodeTree
    // 这样就不会将Pod调度到已经删除的Node上了。
	if err := cache.nodeTree.removeNode(node); err != nil {
		return err
	}
	cache.removeNodeImageStates(node)
	return nil
}

Ejecución de la función de rutina posterior a la limpieza

Como se mencionó anteriormente, Cache tiene su propia corrutina, que se usa para limpiar pods que se supone que caducan. Enlace de código: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/internal/cache/cache.go#L724

func (cache *schedulerCache) run() {
    // 定时1秒钟执行一次cleanupExpiredAssumedPods
	go wait.Until(cache.cleanupExpiredAssumedPods, cache.period, cache.stop)
}

func (cache *schedulerCache) cleanupExpiredAssumedPods() {
    // 取当前时间
	cache.cleanupAssumedPods(time.Now())
}

func (cache *schedulerCache) cleanupAssumedPods(now time.Time) {
	cache.mu.Lock()
	defer cache.mu.Unlock()
	defer cache.updateMetrics()

	// 遍历假定Pod
	for key := range cache.assumedPods {
        // 获取Pod
		ps, ok := cache.podStates[key]
		if !ok {
			klog.Fatal("Key found in assumed set but not in podStates. Potentially a logical error.")
		}
        // 如果Pod没有标记为结束Binding,则忽略,说明Pod还在Binding中
        // 说白了就是没有调用FinishBinding的Pod不用处理
		if !ps.bindingFinished {
			klog.V(5).Infof("Couldn't expire cache for pod %v/%v. Binding is still in progress.",
				ps.pod.Namespace, ps.pod.Name)
			continue
		}
        // 如果当前时间已经超过了Pod假定过期时间,说明Pod假定时间已过期
		if now.After(*ps.deadline) {
            // 此类情况属于异常情况,所以日志等级是waning
			klog.Warningf("Pod %s/%s expired", ps.pod.Namespace, ps.pod.Name)
            // 清理假定过期的Pod
			if err := cache.expirePod(key, ps); err != nil {
				klog.Errorf("ExpirePod failed for %s: %v", key, err)
			}
		}
	}
}

func (cache *schedulerCache) expirePod(key string, ps *podState) error {
    // 从NodeInfo中减去Pod资源、镜像等状态
	if err := cache.removePod(ps.pod); err != nil {
		return err
	}
    // 从Cache中删除Pod
	delete(cache.assumedPods, key)
	delete(cache.podStates, key)
	return nil
}

 De hecho, hay un problema más serio: si se asume que los recursos de Pod caducados simplemente se liberarán, y se programa un nuevo Pod en el mismo Nodo que el Pod caducado recién asumido, AddPod vuelve a agregar el Pod, que puede sobrecargar el recurso Node. El autor deja esta pregunta para que la responda un artículo futuro.

UpdateSnapshot

Bueno, muchos de los presagios en el artículo anterior son para UpdateSnapshot, porque el propósito principal de Cache es proporcionar Node mirroring a kube-Scheduler para que kube-Scheduler pueda programar nuevos Pods de acuerdo con el estado de Node. El Pod en caché existe para calcular el estado de los recursos de Node. Después de todo, los dos son dos rutas en etcd. No hay mucho que decir, solo ve al código. Enlace de código: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/internal/cache/cache.go#L203

// UpdateSnapshot更新的是参数nodeSnapshot,不是更新Cache.
// 也就是Cache需要找到当前与nodeSnapshot的差异,然后更新它,这样nodeSnapshot就与Cache状态一致了
// 至少从函数执行完毕后是一致的。
func (cache *schedulerCache) UpdateSnapshot(nodeSnapshot *Snapshot) error {
	cache.mu.Lock()
	defer cache.mu.Unlock()
    // 与本文关系不大,鉴于不增加复杂性原则,先忽略他,从命名上看很容易立理解
	balancedVolumesEnabled := utilfeature.DefaultFeatureGate.Enabled(features.BalanceAttachedNodeVolumes)

	// 获取nodeSnapshot的版本,笔者习惯叫版本,其实就是版本的概念。
    // 此处需要多说一点:kube-scheudler为Node定义了全局的generation变量,每个Node状态变化都会造成generation+=1然后赋值给该Node
    // nodeSnapshot.generation就是最新NodeInfo.Generation,就是表头的那个NodeInfo。
	snapshotGeneration := nodeSnapshot.generation

    // 介绍Snapshot的时候提到了,快照中有三个列表,分别是全量、亲和性和反亲和性列表
    // 全量列表在没有Node添加或者删除的时候,是不需要更新的
	updateAllLists := false
	// 当有Node的亲和性状态发生了变化(以前没有任何Pod有亲和性声明现在有了,抑或反过来),
    // 则需要更新快照中的亲和性列表
	updateNodesHavePodsWithAffinity := false
	// 同上
	updateNodesHavePodsWithRequiredAntiAffinity := false

    // 遍历Node列表,为什么不遍历Node的map?因为Node列表是按照Generation排序的
    // 只要找到大于nodeSnapshot.generation的所有Node然后把他们更新到nodeSnapshot中就可以了
	for node := cache.headNode; node != nil; node = node.next {
        // 说明Node的状态已经在nodeSnapshot中了,因为但凡Node有任何更新,那么NodeInfo.Generation 
        // 肯定会大于snapshotGeneration,同时该Node后面的所有Node也不用在遍历了,因为他们的版本更低
		if node.info.Generation <= snapshotGeneration {
			break
		}
        // 先忽略
		if balancedVolumesEnabled && node.info.TransientInfo != nil {
			// Transient scheduler info is reset here.
			node.info.TransientInfo.ResetTransientSchedulerInfo()
		}
        // node.info.Node()获取*v1.Node,前文说了,如果Node被删除,那么该值就是为nil
        // 所以只有未被删除的Node才会被更新到nodeSnapshot,因为快照中的全量Node列表是按照nodeTree排序的
        // 而nodeTree都是真实的node
		if np := node.info.Node(); np != nil {
            // 如果nodeSnapshot中没有该Node,则在nodeSnapshot中创建Node,并标记更新全量列表,因为创建了新的Node
			existing, ok := nodeSnapshot.nodeInfoMap[np.Name]
			if !ok {
				updateAllLists = true
				existing = &framework.NodeInfo{}
				nodeSnapshot.nodeInfoMap[np.Name] = existing
			}
            // 克隆NodeInfo,这个比较好理解,肯定不能简单的把指针设置过去,这样会造成多协程读写同一个对象
            // 因为克隆操作比较重,所以能少做就少做,这也是利用Generation实现增量更新的原因
			clone := node.info.Clone()
            // 如果Pod以前或者现在有任何亲和性声明,则需要更新nodeSnapshot中的亲和性列表
			if (len(existing.PodsWithAffinity) > 0) != (len(clone.PodsWithAffinity) > 0) {
				updateNodesHavePodsWithAffinity = true
			}
            // 同上,需要更新非亲和性列表
			if (len(existing.PodsWithRequiredAntiAffinity) > 0) != (len(clone.PodsWithRequiredAntiAffinity) > 0) {
				updateNodesHavePodsWithRequiredAntiAffinity = true
			}
			// 将NodeInfo的拷贝更新到nodeSnapshot中
			*existing = *clone
		}
	}
    // Cache的表头Node的版本是最新的,所以也就代表了此时更新镜像后镜像的版本了
	if cache.headNode != nil {
		nodeSnapshot.generation = cache.headNode.info.Generation
	}

	// 如果nodeSnapshot中node的数量大于nodeTree中的数量,说明有node被删除
    // 所以要从快照的nodeInfoMap中删除已删除的Node,同时标记需要更新node的全量列表
	if len(nodeSnapshot.nodeInfoMap) > cache.nodeTree.numNodes {
		cache.removeDeletedNodesFromSnapshot(nodeSnapshot)
		updateAllLists = true
	}

    // 如果需要更新Node的全量或者亲和性或者反亲和性列表,则更新nodeSnapshot中的Node列表
	if updateAllLists || updateNodesHavePodsWithAffinity || updateNodesHavePodsWithRequiredAntiAffinity {
		cache.updateNodeInfoSnapshotList(nodeSnapshot, updateAllLists)
	}

    // 如果此时nodeSnapshot的node列表与nodeTree的数量还不一致,需要再做一次node全列表更新
    // 此处应该是一个保险操作,理论上不会发生,谁知道会不会有Bug发生呢?多一些容错没有坏处
	if len(nodeSnapshot.nodeInfoList) != cache.nodeTree.numNodes {
		errMsg := fmt.Sprintf("snapshot state is not consistent, length of NodeInfoList=%v not equal to length of nodes in tree=%v "+
			", length of NodeInfoMap=%v, length of nodes in cache=%v"+
			", trying to recover",
			len(nodeSnapshot.nodeInfoList), cache.nodeTree.numNodes,
			len(nodeSnapshot.nodeInfoMap), len(cache.nodes))
		klog.Error(errMsg)
		// We will try to recover by re-creating the lists for the next scheduling cycle, but still return an
		// error to surface the problem, the error will likely cause a failure to the current scheduling cycle.
		cache.updateNodeInfoSnapshotList(nodeSnapshot, true)
		return fmt.Errorf(errMsg)
	}

	return nil
}

// 先思考一个问题:为什么有Node添加或者删除需要更新快照中的全量列表?如果是Node删除了,
// 需要找到Node在全量列表中的位置,然后删除它,最悲观的复杂度就是遍历一遍列表,然后再挪动它后面的Node
// 因为快照的Node列表是用slice实现,所以一旦快照中Node列表有任何更新,复杂度都是Node的数量。
// 那如果是有新的Node添加呢?并不知道应该插在哪里,所以重新创建一次全量列表最为简单有效。
// 亲和性和反亲和性列表道理也是一样的。
func (cache *schedulerCache) updateNodeInfoSnapshotList(snapshot *Snapshot, updateAll bool) {
    // 快照创建亲和性和反亲和性列表
	snapshot.havePodsWithAffinityNodeInfoList = make([]*framework.NodeInfo, 0, cache.nodeTree.numNodes)
	snapshot.havePodsWithRequiredAntiAffinityNodeInfoList = make([]*framework.NodeInfo, 0, cache.nodeTree.numNodes)
    // 如果更新全量列表
	if updateAll {
		// 创建快照全量列表
		snapshot.nodeInfoList = make([]*framework.NodeInfo, 0, cache.nodeTree.numNodes)
		nodesList, err := cache.nodeTree.list()
		if err != nil {
			klog.Error(err)
		}
        // 遍历nodeTree的Node
		for _, nodeName := range nodesList {
            // 理论上快照的nodeInfoMap与nodeTree的状态是一致,此处做了判断用来检测BUG,下面的错误日志也是这么写的
			if nodeInfo := snapshot.nodeInfoMap[nodeName]; nodeInfo != nil {
                // 追加全量、亲和性(按需)、反亲和性列表(按需)
				snapshot.nodeInfoList = append(snapshot.nodeInfoList, nodeInfo)
				if len(nodeInfo.PodsWithAffinity) > 0 {
					snapshot.havePodsWithAffinityNodeInfoList = append(snapshot.havePodsWithAffinityNodeInfoList, nodeInfo)
				}
				if len(nodeInfo.PodsWithRequiredAntiAffinity) > 0 {
					snapshot.havePodsWithRequiredAntiAffinityNodeInfoList = append(snapshot.havePodsWithRequiredAntiAffinityNodeInfoList, nodeInfo)
				}
			} else {
				klog.Errorf("node %q exist in nodeTree but not in NodeInfoMap, this should not happen.", nodeName)
			}
		}
	} else {
        // 如果更新全量列表,只需要遍历快照中的全量列表就可以了
		for _, nodeInfo := range snapshot.nodeInfoList {
            // 按需追加亲和性和反亲和性列表
			if len(nodeInfo.PodsWithAffinity) > 0 {
				snapshot.havePodsWithAffinityNodeInfoList = append(snapshot.havePodsWithAffinityNodeInfoList, nodeInfo)
			}
			if len(nodeInfo.PodsWithRequiredAntiAffinity) > 0 {
				snapshot.havePodsWithRequiredAntiAffinityNodeInfoList = append(snapshot.havePodsWithRequiredAntiAffinityNodeInfoList, nodeInfo)
			}
		}
	}
}

Piense para qué se utiliza la lista de nodos de la instantánea. Cabe mencionar anteriormente que el mapa no tiene ningún orden y la lista está ordenada por nodeTree, lo que es más beneficioso para la programación.Los lectores deberían poder resolverlo. 

para resumir

  1. La caché almacena en caché la información del pod y del nodo, y la información del nodo agrega la cantidad de recursos y la información de duplicación de todos los pods que se ejecutan en el nodo; el nodo se divide en real y virtual, y el nodo eliminado no será eliminado inmediatamente por el caché, pero continuará mantener uno. El Nodo virtual no se eliminará hasta que se borre el Pod en el Nodo, pero el Nodo real se mantiene en el NodeTree, y el uso de NodeTree para programar puede evitar programar el Pod en el Nodo virtual;

  2. kube-Scheduler usa client-go para monitorear (observar) el estado de Pod y Node.Cuando ocurre un evento, llama a AddPod, RemovePod, UpdatePod, AddNode, RemoveNode, UpdateNode de Cache para actualizar el estado de Pod y Node en Cache, por lo que ese programador de kube inicia uno nuevo. El último estado se puede obtener durante una ronda de programación;

  3. kube-Scheduler llamará a UpdateSnapshot para actualizar el estado del nodo local (variable local) en cada ronda de programación. Debido a que los nodos en la caché están ordenados por la actualización más reciente, solo necesita actualizar el nodo cuya generación de nodo en la caché es mayor que la generación de instantáneas locales de kube-planificador a Solo en la instantánea, esto puede evitar una gran cantidad de copias innecesarias;

  4. Una vez que el programador de kube encuentra un Pod de programación de Nodo adecuado, debe llamar a Cache.AssumePod asumiendo que el Pod se ha programado y luego iniciar el Pod de vinculación asincrónica de corrutina al Nodo. ​​Cuando el Pod completa el Bind, llama a Cache.FinishBinding para notificar a la caché;

  5. kube-scheudler llama a Cache.AssumePod para todas las acciones posteriores, una vez que hay un error, llamará a Cache.ForgetPod para eliminar el Pod supuesto y liberar recursos;

  6. El tiempo de espera predeterminado para el Bind Pod es de 30 segundos. El caché tiene un temporizador de rutina (1 segundo) para limpiar el Bind timeout Pod. Si el mensaje de confirmación del Pod (llamar a AddPod) no se recibe después del tiempo de espera, el tiempo de espera del Pod será eliminado, y luego Liberar los recursos ocupados por Cache.AssumePod;

  7. La función principal de Cache es contar el estado de programación de Node (como acumular la cantidad de recursos de Pod, duplicación de estadísticas), y luego enviarlo a kube-Scheduler en forma de duplicación, y kube-Scheduler saca el Pod en espera para ser programado desde la cola de programación (SchedulingQueue), de acuerdo con el Nodo más adecuado para el cálculo de duplicación;

En este punto, es muy fácil de entender al mirar los comentarios en la máquina de estado del Pod en el código fuente:

// State Machine of a pod's events in scheduler's cache:
//
//
//   +-------------------------------------------+  +----+
//   |                            Add            |  |    |
//   |                                           |  |    | Update
//   +      Assume                Add            v  v    |
//Initial +--------> Assumed +------------+---> Added <--+
//   ^                +   +               |       +
//   |                |   |               |       |
//   |                |   |           Add |       | Remove
//   |                |   |               |       |
//   |                |   |               +       |
//   +----------------+   +-----------> Expired   +----> Deleted
//         Forget             Expire
//

El resumen anterior describe el proceso del programador de kube para programar aproximadamente un Pod. De hecho, el proceso de programar un programador de kube para programar un Pod es muy complicado. Para facilitar la comprensión de la posición y función de Cache en el programador de kube, algunos de el contenido está estropeado. El autor analizará en detalle el proceso detallado del Pod de programación del programador de kube en un artículo de seguimiento.

El contenido de este artículo es un poco apresurado y un poco tosco, si tienes tiempo para optimizarlo más tarde, los lectores que tengan preguntas pueden dejar un mensaje para comunicarse.

Supongo que te gusta

Origin blog.csdn.net/weixin_42663840/article/details/112004228
Recomendado
Clasificación