Análisis de marco de kube-planificador

Prefacio

Antes de leer este artículo, se recomienda mirar la documentación del marco de programación oficial y la propuesta de diseño del marco de programación . También es necesario mirar primero el complemento de programación para facilitar la comprensión del contenido de este artículo.

Este artículo extrae y anota la información de los documentos oficiales que el autor considera más importante:

  1. Se han agregado muchas funciones al programador de kubernetes, lo que hace que el código sea cada vez más grande y la lógica cada vez más compleja. Un programador complejo es difícil de mantener, sus errores también son difíciles de encontrar y reparar, y es difícil para los usuarios que modifican el programador ponerse al día e integrar nuevas actualizaciones de versión. Actualmente, el programador de kube proporciona webhooks para ampliar la función (extensor), pero sus capacidades son limitadas. La función del programador es cada vez más compleja y difícil de mantener. Aunque existe una extensión Extender para la interfaz http, su uso es muy limitado e ineficiente (los detalles se presentarán en el artículo que analiza Extender ), por lo que hay un complemento -en marco de programación
  2. El marco de planificación es una arquitectura de diseño de complementos de kube-planificador. Agrega un conjunto de nuevas API de "conexión" al planificador existente. Los complementos se compilan en el planificador (no hay flexibilidad de biblioteca dinámica). Estas API permiten que la mayoría de las funciones de programación se implementen en forma de complementos, al tiempo que mantienen el "núcleo" de programación simple y fácil de mantener. ¿Cómo entiendes esta frase? Es decir, la mayoría de las funciones de programación (como la afinidad y la programación uniforme de recursos) se implementan mediante complementos, y el programador solo necesita ejecutarse de acuerdo con el proceso diseñado por el marco de programación.
  3. Cada vez que se programa un Pod se divide en dos ciclos, a saber, el ciclo de programación y el ciclo de vinculación. El ciclo de programación selecciona un nodo para el pod y el ciclo de vinculación aplica la decisión al clúster. El ciclo de programación se ejecuta en serie, mientras que el ciclo de encuadernación puede ejecutarse simultáneamente. Si se determina que el Pod no es programable o hay un error interno, el período de programación o el período de vinculación se puede terminar. El Pod volverá a la cola de envío y volverá a intentarlo. El ciclo de programación es una sola corrutina, que se llama en serie en el orden de las fases y la configuración del complemento. El ciclo de vinculación debe ejecutarse de forma asíncrona debido a la operación de escritura de almacenamiento, de lo contrario, la eficiencia de programación será demasiado baja. La llamada decisión de aplicar al clúster es escribir un objeto API en un servidor, y los servicios en el clúster que requieren objetos como watch pueden percibir
  4. El marco de programación define algunos puntos de extensión, a los que se llama en uno o más puntos de extensión después del registro del complemento. Algunos de estos complementos cambian las decisiones de programación, mientras que otros solo se utilizan para proporcionar información. El llamado punto de extensión es la interfaz del complemento. Debido a que se pueden implementar varios complementos bajo demanda, se le llama "extensión"; y "punto" es un total de 11 nodos desde la clasificación de la cola de despacho hasta el posprocesamiento del enlace del Pod (también llamado fase). En cuanto a algunas decisiones de cambio, algunas brindan información que es fácil de entender. Por ejemplo, PreXxxPlugins todos brindan información y Xxx todas las decisiones de cambio.

La siguiente figura es un diagrama de diseño del marco de programación, que se deriva de documentos oficiales. Combinando el diagrama y el complemento de programación , tendrá una comprensión básica del marco de programación de kubernetes.

 

Este artículo combina la documentación oficial y la implementación del marco de programación y análisis del código fuente, y el código fuente es la rama de la versión 1.20 de kubernetes.

Marco de programación

Resolver

El concepto de identificador se usa más comúnmente en los lenguajes C / C ++. Generalmente, es un puntero a un objeto. Por razones de seguridad, el identificador también puede ser una clave, y luego el objeto se coloca en un contenedor (como un mapa ). El identificador del marco de programación El identificador es el puntero del marco de programación (consulte los detalles a continuación) y el identificador se proporciona al complemento de programación (descrito en el capítulo de registro del complemento). Debido a que los parámetros de la interfaz del complemento de programación solo incluyen PodInfo, NodeInfo y CycleState, si el complemento de programación necesita acceder a otros objetos, solo puede usar el identificador del marco de programación. Por ejemplo, Clientset requerido para complementos vinculantes, SharedIndexInformer requerido para complementos de programación preventiva, interfaces de acceso a Pod en espera requeridas para complementos aprobados, etc.

Ahora echemos un vistazo a cómo se define el manejo del marco de programación. El enlace del código fuente: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/framework/interface.go#L515

type Handle interface {
    // SharedLister是基于快照的Lister,这个快照就是在调度Cache文章中介绍的NodeInfo的快照。
    // 此处不扩展对SharedLister进行说明,只要有调度Cache的基础就可以了,无非是NodeInfo快照基础上抽象新的接口。
    // 在调度周期开始时获取快照,保持不变,直到Pod完成'Permit'扩展点为止,在绑定阶段是不保证快照不变的。
    // 因此绑定周期中插件不应使用它,否则会发生并发度/写错误的可能性,它们应该改用调度Cache。
    SnapshotSharedLister() SharedLister

    // 此处需要说明一下WaitingPod,就是在'Permit'扩展点返回等待的Pod,需要Handle的实现提供访问这些Pod的接口。
    // 遍历等待中的Pod。
    IterateOverWaitingPods(callback func(WaitingPod))

    // 通过UID获取等待中的Pod
    GetWaitingPod(uid types.UID) WaitingPod

    // 拒绝等待中的Pod
    RejectWaitingPod(uid types.UID)

    // 获取Clientset,用于创建、更新API对象,典型的应用就是绑定Pod。
    ClientSet() clientset.Interface

    // 用于记录调度过程中的各种事件
    EventRecorder() events.EventRecorder

    // 当调度Cache和队列中的信息无法满足插件需求时,可以利用SharedIndexInformer获取指定的API对象。
    // 比如抢占调度插件需要获取所有运行中的Pod。
    SharedInformerFactory() informers.SharedInformerFactory

    // 获取抢占句柄,只开放给实现抢占调度插件的句柄,用来协助实现抢占调度,详情见后面章节注释。
    PreemptHandle() PreemptHandle
}

El significado del identificador es proporcionar una interfaz de servicio para el complemento, ayudar al complemento a realizar su función y no necesita proporcionar la implementación del marco de programación. Aunque el identificador es el puntero del marco de programación, el encanto de la interfaz es que el usuario solo necesita prestar atención a la definición de la interfaz y no le importa la implementación de la interfaz, que también es un método de desacoplamiento.

PreemptHandle

El identificador de preferencia es una interfaz diseñada para el complemento de programación de preferencia (PostFilterPlugin). Imagínense las capacidades que necesitamos proporcionar para el identificador de preferencia para implementar la programación de preferencia por nosotros mismos: 1) PodNominator registra todas las nominaciones (la preferencia es exitosa pero aún está esperando el Pod preferencial para salir) Pod, por lo que el identificador de preferencia debe contener al menos la interfaz PodNominator para que el complemento lo use (por ejemplo, para obtener el Pod designado con una prioridad menor que el Pod actual); 2) El complemento de programación de preferencia -in necesita poder ejecutar el filtro, la puntuación y otros complementos para evaluar si la preferencia es exitosa. A continuación, echemos un vistazo a la definición de control de preferencia, el enlace del código fuente: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/framework/interface.go#L552

type PreemptHandle interface {
    // PodNominator是必须的,他可以获取提名到Node的所有Pod
    PodNominator
    // PluginsRunner下面有注释,用来运行插件评估抢占是否成功并选择最优Node
    PluginsRunner
    // Extenders笔者会专门写一篇文章介绍它,此处只需要知道它的实现是一个HTTP的服务,用来扩展kubernetes无法管理的资源的调度能力。
    Extenders() []Extender
}

// PluginsRunner运行某些插件,用于抢占调度插件评估Pod是否可以通过抢占其他Pod的资源运行在Node上。
type PluginsRunner interface {
    // RunXxxPlugins按照配置的插件循序运行Xxx扩展点的插件。
    // 从接口定义来看只有过滤(包括前处理)和评分(包括前处理)的插件,非常合理。
    // 因为抢占实现逻辑和普通的调度一样,都是过滤后再评分,不同的地方无非是多了被强占Pod的处理。
    RunPreScorePlugins(context.Context, *CycleState, *v1.Pod, []*v1.Node) *Status
    RunScorePlugins(context.Context, *CycleState, *v1.Pod, []*v1.Node) (PluginToNodeScores, *Status)
    RunFilterPlugins(context.Context, *CycleState, *v1.Pod, *NodeInfo) PluginToStatus
    RunPreFilterExtensionAddPod(ctx context.Context, state *CycleState, podToSchedule *v1.Pod, podToAdd *v1.Pod, nodeInfo *NodeInfo) *Status
    RunPreFilterExtensionRemovePod(ctx context.Context, state *CycleState, podToSchedule *v1.Pod, podToRemove *v1.Pod, nodeInfo *NodeInfo) *Status
}

Definición de marco

El prefacio ha explicado en detalle las razones del marco de programación y las ideas de diseño relacionadas. El código se carga directamente aquí y el enlace del código fuente: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/ planificador / marco / interfaz .go # L412

type Framework interface {
    // 调度框架句柄,构建调度插件时使用
    Handle
    // 获取调度队列排序需要的函数,其实就是QueueSortPlugin.Less()
    QueueSortFunc() LessFunc

    // 有没有发现和PluginsRunner很像?应该说PluginsRunner是Framework的子集,也可以理解为Framework的实现也是PluginsRunner的实现。
    //Framework这些接口主要是为了每个扩展点抽象一个接口,因为每个扩展点有多个插件,该接口的实现负责按照配置顺序调用插件。
    // 大部分扩展点如果有一个插件返回不成功,则后续的插件就不会被调用了,所以同一个扩展点的插件间大多是“逻辑与”的关系。
    RunPreFilterPlugins(ctx context.Context, state *CycleState, pod *v1.Pod) *Status
    RunFilterPlugins(ctx context.Context, state *CycleState, pod *v1.Pod, nodeInfo *NodeInfo) PluginToStatus
    RunPostFilterPlugins(ctx context.Context, state *CycleState, pod *v1.Pod, filteredNodeStatusMap NodeToStatusMap) (*PostFilterResult, *Status)
    RunPreFilterExtensionAddPod(ctx context.Context, state *CycleState, podToSchedule *v1.Pod, podToAdd *v1.Pod, nodeInfo *NodeInfo) *Status
    RunPreFilterExtensionRemovePod(ctx context.Context, state *CycleState, podToSchedule *v1.Pod, podToAdd *v1.Pod, nodeInfo *NodeInfo) *Status
    RunPreScorePlugins(ctx context.Context, state *CycleState, pod *v1.Pod, nodes []*v1.Node) *Status
    RunScorePlugins(ctx context.Context, state *CycleState, pod *v1.Pod, nodes []*v1.Node) (PluginToNodeScores, *Status)
    RunPreBindPlugins(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string) *Status
    RunPostBindPlugins(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string)
    RunReservePluginsReserve(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string) *Status
    RunReservePluginsUnreserve(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string)
    RunPermitPlugins(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string) *Status
    RunBindPlugins(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string) *Status

    // 如果Pod正在等待(PermitPlugin返回等待状态),则会被阻塞直到Pod被拒绝或者批准。
    WaitOnPermit(ctx context.Context, pod *v1.Pod) *Status
    
    // 用来检测Framework是否有FilterPlugin、PostFilterPlugin、ScorePlugin,没难度,不解释
    HasFilterPlugins() bool
    HasPostFilterPlugins() bool
    HasScorePlugins() bool

    // 列举插件,其中:
    // 1. key是扩展点的名字,比如PreFilterPlugin对应扩展点的名字就是PreFilter,规则就是将插件类型名字去掉Plugin;
    // 2. value是扩展点所有插件配置,类型是slice
    ListPlugins() map[string][]config.Plugin

    // 此处需要引入SchedulingProfile概念,SchedulingProfile允许配置调度框架的扩展点。
    // SchedulingProfile包括使能/禁止每个扩展点的插件,每个插件的权重(仅在Score扩展点有效)以及插件参数;
    // 这个接口是用来获取SchedulingProfile的名字,因为v1.Pod.Spec.SchedulerName就是指定SchedulingProfile名字来选择Framework。
    ProfileName() string
}

Básicamente, se puede inferir de la definición de interfaz del marco:

  1. El Framework se crea en base al SchedulingProfile, que es una relación de uno a uno. La habilitación / desactivación, el peso y los parámetros de los complementos en el Framework son todos del SchedulingProfile;
  2. El marco proporciona la interfaz RunXxxPlugins () para ejecutar todos los complementos sucesivamente de acuerdo con los puntos de extensión en el diagrama de diseño del marco de programación.La secuencia de llamada de los complementos del punto de extensión también se configura a través de SchedulingProfile;
  3. Un Framework es un planificador, y el complemento de planificación configurado es el algoritmo de planificación (puede ver la idea del diseñador en ese momento en el nombre del paquete de kubernetes / pkg / planificador / algorítmprovider), y kube-planificador puede llamar a la interfaz de Framework de acuerdo con un proceso fijo De esta manera, kube-planificador es relativamente simple y fácil de mantener;

Aquí hay una analogía inapropiada: Framework es similar a la definición de interfaz de una biblioteca dinámica, cada SchedulingProfile corresponde a una biblioteca dinámica que implementa la interfaz de Framework, y el nombre de SchedulingProfile es el nombre de la biblioteca dinámica. El programador de kube carga la biblioteca dinámica a pedido y luego ejecuta la programación de acuerdo con el marco seleccionado por el pod. Esta es una comprensión desde la perspectiva de C / C ++. Aunque los complementos del programador de kube se compilan estáticamente y luego se generan dinámicamente a través de la configuración, el principio es el mismo, siempre que sea útil para la comprensión.

Implementación del marco

frameworkImpl

¿Por qué se llama marco? Debido a que es fijo, la diferencia es que la configuración conduce a una combinación diferente de complementos. En otras palabras, incluso si hay varios SchedulingProfiles, solo hay una implementación de Framework. No son más que los complementos a los que hacen referencia las variables miembro de Framework. Eche un vistazo al código de implementación del Framework, el enlace del código fuente: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/framework/runtime/framework.go#L66

type frameworkImpl struct {
    // 调度插件注册表(详情参看调度插件文档),这个非常有用,frameworkImpl的构造函数需要用它来创建配置的插件。
    registry              Registry
    // 这个是为实现Handle.SnapshotSharedLister()接口准备的,是创建frameworkImpl时传入的,不是frameworkImpl自己创建的。
    // 至于framework.SharedLister类型会在调度器的文章中介绍,此处暂时不用关心,因为不影响阅读。
    snapshotSharedLister  framework.SharedLister
    // 这是为实现Handle.GetWaitingPod/RejectWaitingPod/IterateOverWaitingPods()接口准备的。
    // 关于waitingPodsMap请参看https://github.com/jindezgm/k8s-src-analysis/blob/master/kube-scheduler/WaitingPods.md
    waitingPods           *waitingPodsMap
    // 插件名字与权重的映射,用来根据插件名字获取权重,为什么要有这个变量呢?因为插件的权重是配置的,对于每个Framework都不同。
    // 所以pluginNameToWeightMap是在构造函数中根据SchedulingProfile生成的。
    pluginNameToWeightMap map[string]int
    // 所有扩展点的插件,为实现Framework.RunXxxPlugins()准备的,不难理解,就是每个扩展点遍历插件执行就可以了。
    // 下面所有插件都是在构造函数中根据SchedulingProfile生成的。
    queueSortPlugins      []framework.QueueSortPlugin
    preFilterPlugins      []framework.PreFilterPlugin
    filterPlugins         []framework.FilterPlugin
    postFilterPlugins     []framework.PostFilterPlugin
    preScorePlugins       []framework.PreScorePlugin
    scorePlugins          []framework.ScorePlugin
    reservePlugins        []framework.ReservePlugin
    preBindPlugins        []framework.PreBindPlugin
    bindPlugins           []framework.BindPlugin
    postBindPlugins       []framework.PostBindPlugin
    permitPlugins         []framework.PermitPlugin

    // 这些是为实现Handle.Clientset/EventHandler/SharedInformerFactory()准备的,因为Framework继承了Handle。
    // 这些成员变量是同构构造函数的参数出入的,不是frameworkImpl自己创建的。
    clientSet       clientset.Interface
    eventRecorder   events.EventRecorder
    informerFactory informers.SharedInformerFactory

    metricsRecorder *metricsRecorder
    // 为实现Handle.ProfileName()准备的。
    profileName     string

    // 为实现Framework.PreemptHandle()准备的
    preemptHandle framework.PreemptHandle

    // 如果为true,RunFilterPlugins第一次失败后不返回,应该积累有失败的状态。
    runAllFilters bool
}

Constructor de framework

Desde la perspectiva de las variables miembro de frameworkImpl, básicamente puede imaginar cómo implementar la interfaz del Framework, por lo que este artículo solo selecciona algunas implementaciones de interfaz representativas, y otros lectores pueden leerlas por sí mismos sin ninguna dificultad. En primer lugar, el constructor de frameworkImpl es una función más crítica, porque algunas variables miembro clave se crean en el constructor. Enlace fuente: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/framework/runtime/framework.go#L235

// NewFramework()是frameworkImpl的构造函数,前三个参数在调度插件的文档中有介绍,此处简单描述一下:
// 1. r: 插件注册表,可以根据插件的名字创建插件;
// 2. plugins: 插件开关配置,就是使能/禁止哪些插件;
// 3. args: 插件自定义参数,用来创建插件;
// 4. opts: 构造frameworkImpl的选项参数,frameworkImpl有不少成员变量是通过opts传入进来的;
func NewFramework(r Registry, plugins *config.Plugins, args []config.PluginConfig, opts ...Option) (framework.Framework, error) {
    // 应用所有选项,Option这种玩法不稀奇了,golang有太多项目采用类似的方法
    options := defaultFrameworkOptions
    for _, opt := range opts {
        opt(&options)
    }

    // 可以看出来frameworkImpl很多成员变量来自opts
    f := &frameworkImpl{
        registry:              r,
        snapshotSharedLister:  options.snapshotSharedLister,
        pluginNameToWeightMap: make(map[string]int),
        waitingPods:           newWaitingPodsMap(),
        clientSet:             options.clientSet,
        eventRecorder:         options.eventRecorder,
        informerFactory:       options.informerFactory,
        metricsRecorder:       options.metricsRecorder,
        profileName:           options.profileName,
        runAllFilters:         options.runAllFilters,
    }
    // preemptHandle实现了PreemptHandle接口,其中Extenders和PodNominator都是通过opts传入的。
    // 因为frameworkImpl实现了Framework接口,所以也就是实现了PluginsRunner接口。
    f.preemptHandle = &preemptHandle{
        extenders:     options.extenders,
        PodNominator:  options.podNominator,
        PluginsRunner: f,
    }
    if plugins == nil {
        return f, nil
    }

    // pluginsNeeded()函数名是获取需要的插件,就是把plugins中所有使能(Enable)的插件转换成map[string]config.Plugin。
    // pg中是所有使能的插件,key是插件名字,value是config.Plugin(插件权重),试问为什么要转换成map?
    // 笔者在调度插件的文章中提到了,很多插件实现了不同扩展点的插件接口,这会造成plugins中有很多相同名字的插件,只是分布在不同的扩展点。
    // 因为map有去重能力,这样可以避免相同的插件创建多个对象。
    pg := f.pluginsNeeded(plugins)

    // pluginConfig存储的是所有使能插件的参数
    pluginConfig := make(map[string]runtime.Object, len(args))
    for i := range args {
        // 遍历所有插件参数,并找到使能插件的参数,因为pg是所有使能的插件。
        name := args[i].Name
        if _, ok := pluginConfig[name]; ok {
            return nil, fmt.Errorf("repeated config for plugin %s", name)
        }
        pluginConfig[name] = args[i].Args
    }
    // outputProfile是需要输出的KubeSchedulerProfile对象。
    // 前文提到的SchedulingProfile对应的类型就是KubeSchedulerProfile,包括Profile的名字、插件配置以及插件参数。
    // 因为在构造frameworkImpl的时候会过滤掉不用的插件参数,所以调用者如果需要,可以通过opts传入回调函数捕获KubeSchedulerProfile。
    outputProfile := config.KubeSchedulerProfile{
        SchedulerName: f.profileName,
        Plugins:       plugins,
        PluginConfig:  make([]config.PluginConfig, 0, len(pg)),
    }

    // 此处需要区分framework.Plugin和config.Plugin,前者是插件接口基类,后者是插件权重配置,所以接下来就是创建所有使能的插件了。
    pluginsMap := make(map[string]framework.Plugin)
    var totalPriority int64
    // 遍历插件注册表
    for name, factory := range r {
        // 如果没有使能,则跳过。这里的遍历有点意思,为什么不遍历pg,然后查找r,这样不是更容易理解么?
        // 其实遍历pg会遇到一个问题,如果r中没有找到怎么办?也就是配置了一个没有注册的插件,报错么?
        // 而当前的遍历方法传达的思想是,就这多插件,使能了哪个就创建哪个,不会出错,不需要异常处理。
        if _, ok := pg[name]; !ok {
            continue
        }
        // getPluginArgsOrDefault()根据插件名字从pluginConfig获取参数,如果没有则返回默认参数,函数名字已经告诉我们一切了。
        args, err := getPluginArgsOrDefault(pluginConfig, name)
        if err != nil {
            return nil, fmt.Errorf("getting args for Plugin %q: %w", name, err)
        }
        // 输出插件参数,这也是为什么有捕获KubeSchedulerProfile的功能,因为在没有配置参数的情况下要创建默认参数。
        // 捕获KubeSchedulerProfile可以避免调用者再实现一次类似的操作,前提是有捕获的需求。
        if args != nil {
            outputProfile.PluginConfig = append(outputProfile.PluginConfig, config.PluginConfig{
                Name: name,
                Args: args,
            })
        }
        // 利用插件工厂创建插件,传入插件参数和框架句柄
        p, err := factory(args, f)
        if err != nil {
            return nil, fmt.Errorf("initializing plugin %q: %w", name, err)
        }
        pluginsMap[name] = p

        // 记录配置的插件权重
        f.pluginNameToWeightMap[name] = int(pg[name].Weight)
        // f.pluginNameToWeightMap[name] == 0有两种可能:1)没有配置权重,2)配置权重为0
        // 无论哪一种,权重为0是不允许的,即便它不是ScorePlugin,如果为0则使用默认值1
        if f.pluginNameToWeightMap[name] == 0 {
            f.pluginNameToWeightMap[name] = 1
        }
        // 需要了解的是framework.MaxTotalScore的值是64位整数的最大值,framework.MaxNodeScore的值是100;
        // 每个插件的标准化分数是[0, 100], 所有插件最大标准化分数乘以权重的累加之和不能超过int64最大值,
        // 否则在计算分数的时候可能会溢出,所以此处需要校验配置的权重是否会导致计算分数溢出。
        if int64(f.pluginNameToWeightMap[name])*framework.MaxNodeScore > framework.MaxTotalScore-totalPriority {
            return nil, fmt.Errorf("total score of Score plugins could overflow")
        }
        totalPriority += int64(f.pluginNameToWeightMap[name]) * framework.MaxNodeScore
    }

    // pluginsMap是所有已经创建好的插件,但是是map结构,现在需要把这些插件按照扩展点分到不同的slice中。
    // 即map[string]framework.Plugin->[]framework.QueueSortPlugin,[]framework.PreFilterPlugin...的过程。
    // 关于getExtensionPoints()和updatePluginList()的实现还是挺有意思的,用了反射,感兴趣的同学可以看一看。
    for _, e := range f.getExtensionPoints(plugins) {
        if err := updatePluginList(e.slicePtr, e.plugins, pluginsMap); err != nil {
            return nil, err
        }
    }

    // 检验ScorePlugin的权重不能为0,当前已经起不了作用了,因为在创建插件的时候没有配置权重的插件的权重都是1.
    // 但是这些代码还是有必要的,未来保不齐会修改前面的代码,这些校验可能就起到作用了。关键是在构造函数中,只会执行一次,这点校验计算量可以忽略不计。
    for _, scorePlugin := range f.scorePlugins {
        if f.pluginNameToWeightMap[scorePlugin.Name()] == 0 {
            return nil, fmt.Errorf("score plugin %q is not configured with weight", scorePlugin.Name())
        }
    }

    // 不能没有调度队列的排序插件,并且也不能有多个,多了没用。
    // 那么如果同时存在两种优先级计算方法怎么办?用不同的Framework,即不同的SchedulingProfile.
    if len(f.queueSortPlugins) == 0 {
        return nil, fmt.Errorf("no queue sort plugin is enabled")
    }
    if len(f.queueSortPlugins) > 1 {
        return nil, fmt.Errorf("only one queue sort plugin can be enabled")
    }
    // 不能没有绑定插件,否则调度的结果没法应用到集群
    if len(f.bindPlugins) == 0 {
        return nil, fmt.Errorf("at least one bind plugin is needed")
    }
    // 如果调用者需要捕获KubeSchedulerProfile,则将KubeSchedulerProfile回调给调用者
    if options.captureProfile != nil {
        if len(outputProfile.PluginConfig) != 0 {
            // 按照Profile名字排序
            sort.Slice(outputProfile.PluginConfig, func(i, j int) bool {
                return outputProfile.PluginConfig[i].Name < outputProfile.PluginConfig[j].Name
            })
        } else {
            outputProfile.PluginConfig = nil
        }
        options.captureProfile(outputProfile)
    }

    return f, nil
}

No sé lo que siente el lector, de todos modos, después de leer el constructor frameworkImpl, siento que he abierto los dos canales de Ren y Du, y el Framework estará completo a la vez. A continuación, el autor lo lleva a ver algunas de las implementaciones de interfaz de Framework más distintivas.

RunFilterPlugins

¿Por qué RunFilterPlugins? Debido a que hay más verificación de runAllFilters que otros RunXxxPlugins, el enlace del código fuente: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/framework/runtime/framework.go#L527

func (f *frameworkImpl) RunFilterPlugins(
    ctx context.Context,
    state *framework.CycleState,
    pod *v1.Pod,
    nodeInfo *framework.NodeInfo,
) framework.PluginToStatus {
    // 之前一直没有介绍PluginToStatus,它是一个map类型,用于记录每个插件的执行状态,此处状态和HTTP返回的status类似,是一种结果。
    statuses := make(framework.PluginToStatus)
    // 遍历所有的FilterPlugin,因为filterPlugins是slice类型,所以每次都是按照配置的顺序执行
    for _, pl := range f.filterPlugins {
        // runFilterPlugin()是执行单个FilterPlugin插件的函数,下面有注释
        pluginStatus := f.runFilterPlugin(ctx, pl, state, pod, nodeInfo)
        // 此处需要简单说明一下插件返回的状态包括:
        // 1. Success: 成功,没什么好解释的
        // 2. Error: 插件内部错误,比如插件调用Clientset报错
        // 3. Unschedulable: 不可调度,比如资源不足,但是可以通过抢占的方式解决
        // 4. UnschedulableAndUnresolvable: 不可调度并且无法解决,意思就是抢占调度也解决不了
        // 5. Wait: PermitPlugin专属状态,用来延迟Pod的绑定
        // 6. Skip: BindPlugin专属状态,如果BindPlugin不能绑定Pod则返回这个状态
        // 过滤成功就继续循环用下一个插件过滤,过滤失败则需要特殊处理一下
        if !pluginStatus.IsSuccess() {
            // 如果插件返回的不是不可调度,那只能是内部错误,那么就直接返回错误,因为内部错误后续是无法解决的,抢占也不行
            if !pluginStatus.IsUnschedulable() {
                errStatus := framework.NewStatus(framework.Error, fmt.Sprintf("running %q filter plugin for pod %q: %v", pl.Name(), pod.Name, pluginStatus.Message()))
                return map[string]*framework.Status{pl.Name(): errStatus}
            }
            // 当前只有Unschedulable或者UnschedulableAndUnresolvable,二者都是不可调度状态。
            statuses[pl.Name()] = pluginStatus
            // 如果配置了runAllFilters = true,剩下的FilterPlugin也需要执行一次,这样的设计时为什么?
            // 笔者猜测是为了抢占调度准备,因为后续插件可能还会返回不可调度状态,比如当前因为CPU不可调度,后续可能因为PVC不可调度。
            // 如果提供给抢占调度插件的状态中只有CPU不满足,那么抢占了CPU资源也会因为PVC无法满足而过滤失败。
            // 更关键的是,如果后续插件返回了UnschedulableAndUnresolvable,那么就没必要调用抢占调度插件了。
            // 所以只有运行全部插件才能为Pod后续操作提供最准确的参考。
            if !f.runAllFilters {
                return statuses
            }
        }
    }

    return statuses
}

// runFilterPlugin()运行一个FilterPlugin,非常简单,单独封装一个函数就是为了记录metrics。
func (f *frameworkImpl) runFilterPlugin(ctx context.Context, pl framework.FilterPlugin, state *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
    if !state.ShouldRecordPluginMetrics() {
        return pl.Filter(ctx, state, pod, nodeInfo)
    }
    startTime := time.Now()
    status := pl.Filter(ctx, state, pod, nodeInfo)
    f.metricsRecorder.observePluginDurationAsync(Filter, pl.Name(), status, metrics.SinceInSeconds(startTime))
    return status
}

RunScorePlugins

¿Por qué RunScorePlugins? Porque puede hacernos saber cómo Framework usa ScorePlugin para calcular la puntuación de Node, el enlace del código fuente: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/framework/runtime/framework. ir # L635

func (f *frameworkImpl) RunScorePlugins(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodes []*v1.Node) (ps framework.PluginToNodeScores, status *framework.Status) {
    startTime := time.Now()
    defer func() {
        metrics.FrameworkExtensionPointDuration.WithLabelValues(score, status.Code().String(), f.profileName).Observe(metrics.SinceInSeconds(startTime))
    }()
    // 此处需要简单说明一下PluginToNodeScores类型,它是一个map[string][]{Name, Score}(伪代码,Name是Node名字,Score是Node分数)类型。
    // 说白了就是每个ScorePlugin给每个Node的分数,此处的Node是所有RunFilterPlugins()返回成功的Node集合。
    pluginToNodeScores := make(framework.PluginToNodeScores, len(f.scorePlugins))
    // 提前把所有ScorePlugin对所有的Node的评分的内存申请出来,这样后面并行计算分数时就不用加锁了
    for _, pl := range f.scorePlugins {
        pluginToNodeScores[pl.Name()] = make(framework.NodeScoreList, len(nodes))
    }
    // 为并行计算分数创建context和错误chan,请参看parallelize包了解细节
    ctx, cancel := context.WithCancel(ctx)
    errCh := parallelize.NewErrorChannel()

    // parallelize.Until()是一个比较有用的工具,就是并行的执行函数。需要注意len(nodes)不是并行度,它是处理总量。
    // parallelize的并行度是可设置的,默认值是16,如果处理的总量超过并行度, parallelize.Until()会分片处理,即一个协程处理一小部分。
    // 所以下面的代码可以假定Node间并行的执行所有ScorePlugin计算分数。
    parallelize.Until(ctx, len(nodes), func(index int) {
        // 遍历所有的ScorePlugin
        for _, pl := range f.scorePlugins {
            // 计算第index个Node的分数
            nodeName := nodes[index].Name
            s, status := f.runScorePlugin(ctx, pl, state, pod, nodeName)
            // 评分失败退出函数
            if !status.IsSuccess() {
                err := fmt.Errorf("plugin %q failed with: %w", pl.Name(), status.AsError())
                // errCh.SendErrorWithCancel()会调用cancel(),所以其他的并行的协程也会退出。
                errCh.SendErrorWithCancel(err, cancel)
                return
            }
            // 记录插件给第index个Node的分数,这就是提前为pluginToNodeScores申请内存的好处,不需要再加锁了
            pluginToNodeScores[pl.Name()][index] = framework.NodeScore{
                Name:  nodeName,
                Score: s,
            }
        }
    })
    // 如果有任何ScorePlugin出错,则返回错误状态
    if err := errCh.ReceiveError(); err != nil {
        klog.ErrorS(err, "Failed running Score plugins", "pod", klog.KObj(pod))
        return nil, framework.AsStatus(fmt.Errorf("running Score plugins: %w", err))
    }

    // 并行的标准化Node分数,每个ScorePlugin对所有的Node分数标准化处理,ScorePlugin之间是并行的
    parallelize.Until(ctx, len(f.scorePlugins), func(index int) {
        // 判断ScorePlugin是否有扩展接口,如果没有就不用标准化分数处理了
        pl := f.scorePlugins[index]
        nodeScoreList := pluginToNodeScores[pl.Name()]
        if pl.ScoreExtensions() == nil {
            return
        }
        // 调用ScorePlugin的扩展接口标准化所有Node的分数
        status := f.runScoreExtension(ctx, pl, state, pod, nodeScoreList)
        if !status.IsSuccess() {
            err := fmt.Errorf("plugin %q failed with: %w", pl.Name(), status.AsError())
            errCh.SendErrorWithCancel(err, cancel)
            return
        }
    })
    // 如果有任何ScorePlugin出错,则返回错误状态
    if err := errCh.ReceiveError(); err != nil {
        klog.ErrorS(err, "Failed running Normalize on Score plugins", "pod", klog.KObj(pod))
        return nil, framework.AsStatus(fmt.Errorf("running Normalize on Score plugins: %w", err))
    }

    // 并行的在Node标准化分数基础上乘以权重
    parallelize.Until(ctx, len(f.scorePlugins), func(index int) {
        pl := f.scorePlugins[index]
        // 获取插件的权重以及所有Node的标准化分数
        weight := f.pluginNameToWeightMap[pl.Name()]
        nodeScoreList := pluginToNodeScores[pl.Name()]

        // 遍历Node标准化分数
        for i, nodeScore := range nodeScoreList {
            // 如果Node标准化分数超过[0, 100]范围,则返回错误
            if nodeScore.Score > framework.MaxNodeScore || nodeScore.Score < framework.MinNodeScore {
                err := fmt.Errorf("plugin %q returns an invalid score %v, it should in the range of [%v, %v] after normalizing", pl.Name(), nodeScore.Score, framework.MinNodeScore, framework.MaxNodeScore)
                errCh.SendErrorWithCancel(err, cancel)
                return
            }
            // Node分数就是标准化分数乘以插件的权重
            nodeScoreList[i].Score = nodeScore.Score * int64(weight)
        }
    })
    // 如果有任何ScorePlugin的标准化分数超出范围,则返回错误状态
    if err := errCh.ReceiveError(); err != nil {
        klog.ErrorS(err, "Failed applying score defaultWeights on Score plugins", "pod", klog.KObj(pod))
        return nil, framework.AsStatus(fmt.Errorf("applying score defaultWeights on Score plugins: %w", err))
    }

    // 返回每个ScorePlugin对所有Node的评分,并且已经乘上了插件对应的权重
    return pluginToNodeScores, nil
}

// runScorePlugin()调用一个ScorePlugin计算Node的分数,单独封装一个函数就是为了记录metrics。
func (f *frameworkImpl) runScorePlugin(ctx context.Context, pl framework.ScorePlugin, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
    if !state.ShouldRecordPluginMetrics() {
        return pl.Score(ctx, state, pod, nodeName)
    }
    startTime := time.Now()
    s, status := pl.Score(ctx, state, pod, nodeName)
    f.metricsRecorder.observePluginDurationAsync(score, pl.Name(), status, metrics.SinceInSeconds(startTime))
    return s, status
}

RunBindPlugins

¿Por qué RunBindPlugins? Debido a que algunos BindPlugin devolverán el estado de omisión, el enlace de origen: https://github.com/kubernetes/kubernetes/blob/release-1.20/pkg/scheduler/framework/runtime/framework.go#L762

func (f *frameworkImpl) RunBindPlugins(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (status *framework.Status) {
    startTime := time.Now()
    defer func() {
        metrics.FrameworkExtensionPointDuration.WithLabelValues(bind, status.Code().String(), f.profileName).Observe(metrics.SinceInSeconds(startTime))
    }()
    // 没有BindPlugin,等同于所有的BindPlugin都返回了Skip状态。理论上不会出现这种情况,因为在Framework构造函数做了相关校验。
    if len(f.bindPlugins) == 0 {
        return framework.NewStatus(framework.Skip, "")
    }
    // 遍历BindPlugin
    for _, bp := range f.bindPlugins {
        // 执行插件的绑定接口
        status = f.runBindPlugin(ctx, bp, state, pod, nodeName)
        // 如果返回Skip状态,则忽略这个插件。也就是说BindPlugin可以根据Pod选择是否执行绑定。
        // 虽然当前只有DefaultBinder一种实现,笔者猜测:设计者认为有的绑定可能比较复杂,交给特定的插件绑定。
        if status != nil && status.Code() == framework.Skip {
            continue
        }
        // 如果绑定失败则返回错误状态
        if !status.IsSuccess() {
            err := status.AsError()
            klog.ErrorS(err, "Failed running Bind plugin", "plugin", bp.Name(), "pod", klog.KObj(pod))
            return framework.AsStatus(fmt.Errorf("running Bind plugin %q: %w", bp.Name(), err))
        }
        return status
    }
    return status
}

// runBindPlugin()执行一个插件的绑定操作,单独封装一个函数就是为了记录metrics。
func (f *frameworkImpl) runBindPlugin(ctx context.Context, bp framework.BindPlugin, state *framework.CycleState, pod *v1.Pod, nodeName string) *framework.Status {
    if !state.ShouldRecordPluginMetrics() {
        return bp.Bind(ctx, state, pod, nodeName)
    }
    startTime := time.Now()
    status := bp.Bind(ctx, state, pod, nodeName)
    f.metricsRecorder.observePluginDurationAsync(bind, bp.Name(), status, metrics.SinceInSeconds(startTime))
    return status
}

para resumir

  1. Existe una correspondencia uno a uno entre el perfil y el marco. Kube-planificador creará un marco para un perfil. Puede seleccionar el marco para realizar la programación configurando Pod.Spec.SchedulerName;
  2. Framework divide la programación de un Pod en un período de programación y un período de vinculación. El período de programación de cada Pod es en serie, pero el período de vinculación puede ser paralelo;
  3. Framework define el concepto de puntos de extensión y define una interfaz para cada extensión, a saber, XxxPlugin;
  4. Framework define un punto de entrada para cada punto de extensión, RunXxxPlugins, Framework llamará a los complementos a su vez de acuerdo con el orden de los complementos configurados por Perfil;
  5. Los complementos de marco definen identificadores y identificadores de preferencia para proporcionar interfaces para que los complementos implementen funciones específicas;

Supongo que te gusta

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