kube-scheduler proporciona recursos para archivos de configuración, como un archivo de configuración para kube-scheduler, y el archivo se especifica mediante --config= al inicio. La KubeSchedulerConfiguration que se usa actualmente en cada versión de kubernetes es:
Las versiones anteriores a la 1.21 usan v1beta1;
La versión 1.22 usa v1beta2, pero conserva v1beta1;
Las versiones 1.23, 1.24 y 1.25 usan v1beta3, pero conservan v1beta2 y eliminan v1beta1;
Como se muestra a continuación, es un ejemplo simple de kubeSchedulerConfiguration, donde kubeconfig tiene la misma función que el parámetro de inicio --kubeconfig, y kubeSchedulerConfiguration es similar a los archivos de configuración de otros componentes, como kubeletConfiguration es un archivo de configuración que se inicia como un servicio:
–kubeconfig y --config no se pueden especificar al mismo tiempo Si se especifica --config, otros parámetros fallarán naturalmente.
② kubeSchedulerConfiguration 使用
A través del archivo de configuración, los usuarios pueden personalizar varios programadores y configurar los puntos de extensión de cada etapa, y el complemento proporciona un comportamiento de programación en todo el contexto de programación a través de estos puntos de extensión.
La configuración que se muestra a continuación es un ejemplo para configurar el punto de extensión (si name="*", todos los complementos correspondientes al punto de extensión estarán deshabilitados/habilitados):
Dado que kubernetes proporciona varios programadores, naturalmente admite varios archivos de configuración para los archivos de configuración. El perfil también tiene la forma de una lista. Solo necesita especificar varias listas de configuración. El siguiente es un ejemplo de varios archivos de configuración. Si hay varias extensiones También se pueden configurar varios puntos de extensión para cada programador:
kube-scheduler proporciona muchos complementos como métodos de programación de forma predeterminada, y estos complementos se habilitarán si no están configurados de forma predeterminada, como:
ImageLocality: la programación estará más sesgada hacia los nodos con imágenes de contenedores, punto de extensión: puntuación;
TaintToleration: realice la función de corrupción y tolerancia, puntos de extensión: filtro, preScore, puntuación;
NodeName: implemente el método de programación más simple NodeName en la estrategia de programación, punto de extensión: filtro;
NodePorts: la programación verificará si el puerto del nodo está ocupado, puntos de extensión: prefiltro, filtro;
NodeAffinity: proporciona funciones relacionadas con la afinidad de nodos, puntos de extensión: filtro, puntuación;
PodTopologySpread: realice la función del dominio de topología Pod, puntos de extensión: preFilter, filter, preScore, score;
NodeResourcesFit: este complemento verificará si el nodo tiene todos los recursos solicitados por el Pod, utilizando una de las siguientes tres estrategias: LeastAllocated (predeterminado) MostAllocated y RequestedToCapacityRatio, puntos de extensión: preFilter, filter, score;
VolumeBinding: Comprobar si el nodo tiene o puede enlazar el volumen solicitado, puntos de extensión: preFilter, filter, reserve, preBind, score;
Restricciones de volumen: compruebe si el volumen instalado en el nodo cumple con las restricciones específicas del proveedor de volumen, punto de extensión: filtro;
VolumeZone: comprueba si los volúmenes solicitados cumplen con los requisitos de zona que puedan tener, punto de extensión: filtro;
InterPodAffinity: realiza la función de afinidad y antiafinidad entre Pods, puntos de extensión: preFilter, filter, preScore, score;
PrioritySort: proporciona una clasificación basada en la prioridad predeterminada, punto de extensión: queueSort.
2. ¿Cómo extender kube-scheduler?
Cuando piensas en escribir un programador por primera vez, generalmente piensas que extender kube-scheduler es algo muy difícil. De hecho, el kubernetes oficial ya ha pensado en estas cosas. Por esta razón, kubernetes introdujo el concepto de marco en versión 1.15 El marco tiene como objetivo hacer que el programador sea más extensible.
El marco lo usa como complemento al redefinir cada punto de extensión y permite a los usuarios registrarse fuera del árbol de extensiones para que puedan registrarse en kube-scheduler.
① Definir entrada
El programador permite la personalización, pero solo necesita hacer referencia al NewSchedulerCommand correspondiente e implementar la lógica de los complementos:
El NewSchedulerCommand permite la inyección de complementos fuera del árbol, es decir, la inyección de complementos personalizados externos. En este caso, no es necesario modificar el código fuente para definir un programador, sino completar un programador personalizado solo implementándolo usted mismo:
// WithPlugin 用于注入out of tree plugins 因此scheduler代码中没有其引用。
func WithPlugin(name string, factory runtime.PluginFactory)Option{
returnfunc(registry runtime.Registry) error {
return registry.Register(name, factory)}}
② Implementación de complementos
La implementación del complemento solo necesita implementar la interfaz del punto de extensión correspondiente. El NodeAffinity del complemento incorporado se puede encontrar observando su estructura. La implementación del complemento es implementar la interfaz abstracta del punto de extensión correspondiente:
Defina la estructura del complemento: framework.FrameworkHandle se usa para llamar entre la API de Kubernetes y el planificador. Se puede ver en la estructura que incluye lister, informador, etc. Este parámetro también debe implementarse:
Implementar el punto de extensión correspondiente:
func(pl *NodeAffinity)Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string)(int64,*framework.Status){
nodeInfo, err := pl.handle.SnapshotSharedLister().NodeInfos().Get(nodeName)if err != nil {
return0, framework.NewStatus(framework.Error, fmt.Sprintf("getting node %q from Snapshot: %v", nodeName, err))}
node := nodeInfo.Node()if node == nil {
return0, framework.NewStatus(framework.Error, fmt.Sprintf("getting node %q from Snapshot: %v", nodeName, err))}
affinity := pod.Spec.Affinity
var count int64
// A nil element of PreferredDuringSchedulingIgnoredDuringExecution matches no objects.// An element of PreferredDuringSchedulingIgnoredDuringExecution that refers to an// empty PreferredSchedulingTerm matches all objects.if affinity != nil && affinity.NodeAffinity!= nil && affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution!= nil {
// Match PreferredDuringSchedulingIgnoredDuringExecution term by term.for i := range affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution{
preferredSchedulingTerm :=&affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution[i]if preferredSchedulingTerm.Weight==0{
continue}// TODO: Avoid computing it for all nodes if this becomes a performance problem.
nodeSelector, err := v1helper.NodeSelectorRequirementsAsSelector(preferredSchedulingTerm.Preference.MatchExpressions)if err != nil {
return0, framework.NewStatus(framework.Error, err.Error())}if nodeSelector.Matches(labels.Set(node.Labels)){
count +=int64(preferredSchedulingTerm.Weight)}}}return count, nil
}
Finalmente, proporcione un método para registrar esta extensión mediante la implementación de una nueva función, que se puede inyectar en el programador como complementos fuera del árbol en main.go:
// New initializes a new plugin and returns it.
func New(_ runtime.Object, h framework.FrameworkHandle)(framework.Plugin, error){
return&NodeAffinity{
handle: h}, nil
}
3. Programación basada en el tráfico de red
A través de la comprensión anterior de cómo extender el complemento del programador, a continuación se completará un ejemplo de programación basada en el tráfico. Por lo general, el tráfico de red utilizado por un Nodo en la red durante un período de tiempo también es una situación muy común en el entorno de producción.
Por ejemplo, entre varios hosts con una configuración equilibrada, el host A se ejecuta como un script de pedido de servicio y el host B se ejecuta como un servicio normal. Debido a que el pedido requiere descargar una gran cantidad de datos, pero los recursos de hardware rara vez están ocupados, en este momento , si un pod está programado para este nodo, el negocio de ambas partes puede verse afectado (el agente front-end piensa que el nodo tiene una pequeña cantidad de conexiones y se programará en grandes cantidades, y el script de orden reducir la eficiencia debido a la ocupación del ancho de banda de la red).
① Configuración del entorno
Un clúster de kubernetes debe tener al menos dos nodos.
Todos los clústeres de kubernetes proporcionados deben instalar Prometheus node_exporter, que puede estar dentro o fuera del clúster, y aquí se usa el que está fuera del clúster.
El ejemplo se divide aproximadamente en los siguientes pasos:
Defina la API del complemento, y el complemento se llama NetworkTraffic;
Defina el punto de extensión, aquí se utiliza el punto de extensión Puntuación y se define el algoritmo de puntuación;
Definir la forma de obtener la puntuación (obtener los datos correspondientes del indicador Prometheus);
Defina la entrada de parámetros para el programador personalizado;
Implementar el proyecto en el clúster (implementación dentro del clúster e implementación fuera del clúster);
Ejemplo de verificación de resultados.
El ejemplo seguirá el complemento integrado nodeaffinity para completar la escritura del código. La razón para elegir este complemento es que este complemento es relativamente simple y tiene básicamente el mismo propósito que la necesidad. De hecho, otro complemento -ins tienen el mismo efecto.
② Manejo de errores
Al inicializar el proyecto, ir a mod tidy y otras operaciones, encontrará muchos de los siguientes errores:
Este problema se mencionó en el problema de Kubernetes n.° 79384. Después de un vistazo superficial, no explicó por qué ocurrió este problema. En la parte inferior, un jefe proporcionó una secuencia de comandos. Cuando el problema anterior no se pueda resolver, ejecute la secuencia de comandos directamente y será normal:
#!/bin/sh
set -euo pipefail
VERSION=${
1#"v"}if[-z "$VERSION"]; then
echo "Must specify version!"
exit 1
fi
MODS=($(
curl -sS https://raw.githubusercontent.com/kubernetes/kubernetes/v${
VERSION}/go.mod|
sed -n 's|.*k8s.io/\(.*\)=>./staging/src/k8s.io/.*|k8s.io/\1|p'
))forMODin"${MODS[@]}";doV=$(
go moddownload-json "${MOD}@kubernetes-${VERSION}"|
sed -n 's|.*"Version":"\(.*\)".*|\1|p'
)
go modedit"-replace=${MOD}=${MOD}@${V}"
done
go get "k8s.io/kubernetes@v${VERSION}"
③ Definir API de complemento
A través de la descripción del contenido anterior, entendemos que definir un complemento solo necesita implementar la interfaz abstracta del punto de extensión correspondiente, luego se puede inicializar el directorio del proyecto pkg/networtraffic/networktraffice.go.
Defina el nombre del complemento y las variables:
constName="NetworkTraffic"
var _ = framework.ScorePlugin(&NetworkTraffic{
})
A continuación, los resultados deben normalizarse.Se puede ver en el código fuente que el punto de extensión Score necesita implementar más que solo este método único:
// Run NormalizeScore method for each ScorePlugin in parallel.
parallelize.Until(ctx,len(f.scorePlugins),func(index int){
pl := f.scorePlugins[index]
nodeScoreList := pluginToNodeScores[pl.Name()]if pl.ScoreExtensions()== nil {
return}
status := f.runScoreExtension(ctx, pl, state, pod, nodeScoreList)if!status.IsSuccess(){
err := fmt.Errorf("normalize score plugin %q failed with error %v", pl.Name(), status.Message())
errCh.SendErrorWithCancel(err, cancel)return}})
Del código anterior, puede entender que para implementar Score, debe implementar ScoreExtensions y regresar directamente si no está implementado. De acuerdo con el ejemplo en nodeaffinity, se encuentra que este método solo devuelve el objeto de punto de extensión en sí mismo, y la normalización específica es la operación de puntuación real en NormalizeScore.
// NormalizeScore invoked after scoring all nodes.func(pl *NodeAffinity)NormalizeScore(ctx context.Context, state *framework.CycleState, pod *v1.Pod, scores framework.NodeScoreList)*framework.Status{
return pluginhelper.DefaultNormalizeScore(framework.MaxNodeScore,false, scores)}// ScoreExtensions of the Score plugin.func(pl *NodeAffinity)ScoreExtensions() framework.ScoreExtensions{
return pl
}
En el marco de programación, el método para realizar la operación también es NormalizeScore():
func(f *frameworkImpl)runScoreExtension(ctx context.Context, pl framework.ScorePlugin, state *framework.CycleState, pod *v1.Pod, nodeScoreList framework.NodeScoreList)*framework.Status{
if!state.ShouldRecordPluginMetrics(){
return pl.ScoreExtensions().NormalizeScore(ctx, state, pod, nodeScoreList)}
startTime := time.Now()
status := pl.ScoreExtensions().NormalizeScore(ctx, state, pod, nodeScoreList)
f.metricsRecorder.observePluginDurationAsync(scoreExtensionNormalize, pl.Name(), status, metrics.SinceInSeconds(startTime))return status
}
En NormalizeScore es necesario implementar un algoritmo específico para la selección de nodos, la fórmula del algoritmo implementado será la puntuación más alta, el ancho de banda actual más alto y el ancho de banda más alto, esto asegura que la máquina con una ocupación de mayor ancho de banda tenga una puntuación más baja. Por ejemplo, si el ancho de banda más alto es 200 000 y el ancho de banda actual del nodo es 140 000, la puntuación del nodo es:
// 如果返回framework.ScoreExtensions 就需要实现framework.ScoreExtensionsfunc(n *NetworkTraffic)ScoreExtensions() framework.ScoreExtensions{
return n
}// NormalizeScore与ScoreExtensions是固定格式func(n *NetworkTraffic)NormalizeScore(ctx context.Context, state *framework.CycleState, pod *corev1.Pod, scores framework.NodeScoreList)*framework.Status{
var higherScore int64
for _, node := range scores {
if higherScore < node.Score{
higherScore = node.Score}}// 计算公式为,满分 - (当前带宽 / 最高最高带宽 * 100)// 公式的计算结果为,带宽占用越大的机器,分数越低for i, node := range scores {
scores[i].Score= framework.MaxNodeScore-(node.Score*100/ higherScore)
klog.Infof("[NetworkTraffic] Nodes final score: %v", scores)}
klog.Infof("[NetworkTraffic] Nodes final score: %v", scores)return nil
}
En kubernetes el número máximo de nodos soporta 5000. ¿No significa que el looping consume mucho rendimiento a la hora de obtener la máxima puntuación?, de hecho no hay de qué preocuparse. El programador proporciona un parámetro percentOfNodesToScore, que determina la cantidad de ciclos de implementación.
⑤ Configure el nombre del complemento
Para que el complemento se use al registrarse, también debe configurarse con un nombre:
// Name returns name of the plugin. It is used in logs, etc.func(n *NetworkTraffic)Name() string {
returnName}
⑥ Definir los parámetros a pasar en
También hay un prometheusHandle en la extensión del complemento de red, que es la acción de operar el servidor de prometheus para obtener los indicadores. Primero, debe definir una estructura PrometheusHandle:
Con la estructura, debe consultar acciones e indicadores. Para los indicadores, aquí se utiliza node_network_receive_bytes_total como método de cálculo para obtener el tráfico de red del nodo. Dado que el entorno se implementa fuera del clúster, no hay un nombre de host de nodo y se obtiene a través de promQL. La declaración completa es la siguiente:
Debido a que es necesario especificar la dirección de Prometheus, el nombre de la tarjeta de red y el tamaño de los datos obtenidos, la estructura completa es la siguiente. Además, la estructura de parámetros debe seguir el nombre en el formato Args:
typeNetworkTrafficArgsstruct{
IP string `json:"ip"`
DeviceName string `json:"deviceName"`
TimeRange int `json:"timeRange"`
}
Para hacer de este tipo de datos una estructura que KubeSchedulerConfiguration pueda analizar, se requiere un paso más, que es expandir el tipo de recurso correspondiente al extender APIServer. Aquí, kubernetes proporciona dos métodos para extender el tipo de recurso de KubeSchedulerConfiguration:
Una es que la función framework.DecodeInto proporcionada en la versión anterior puede realizar esta operación:
Otra forma es implementar el método de copia profunda correspondiente, como en NodeLabel:
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object// NodeLabelArgs holds arguments used to configure the NodeLabel plugin.typeNodeLabelArgsstruct{
metav1.TypeMeta// PresentLabels should be present for the node to be considered a fit for hosting the podPresentLabels[]string
// AbsentLabels should be absent for the node to be considered a fit for hosting the podAbsentLabels[]string
// Nodes that have labels in the list will get a higher score.PresentLabelsPreference[]string
// Nodes that don't have labels in the list will get a higher score.AbsentLabelsPreference[]string
}
Finalmente, regístrelo en el registro, todo el comportamiento es similar a extender APIServer:
// addKnownTypes registers known types to the given scheme
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,&KubeSchedulerConfiguration{
},&Policy{
},&InterPodAffinityArgs{
},&NodeLabelArgs{
},&NodeResourcesFitArgs{
},&PodTopologySpreadArgs{
},&RequestedToCapacityRatioArgs{
},&ServiceAffinityArgs{
},&VolumeBindingArgs{
},&NodeResourcesLeastAllocatedArgs{
},&NodeResourcesMostAllocatedArgs{
},)
scheme.AddKnownTypes(schema.GroupVersion{
Group:"",Version: runtime.APIVersionInternal},&Policy{
})return nil
}
Para generar funciones de copia profunda y otros archivos, puede usar el script kubernetes/hack/update-codegen.sh en la base de código de kubernetes Para mayor comodidad, aquí se usa el método framework.DecodeInto.
⑧ Despliegue del proyecto
Prepare el perfil del programador, puede ver que los parámetros personalizados se pueden reconocer como el tipo de recurso de KubeSchedulerConfiguration:
Después del inicio, para facilitar la verificación, el servicio kube-scheduler original se cierra, porque el kube-scheduler original se usó como maestro en HA, por lo que el programador personalizado no se usará para causar que el pod esté pendiente.
⑨ Resultado de la verificación
Prepare un pod que debe implementarse, especificando el nombre del programador que se usará:
El entorno experimental aquí es un clúster de Kubernetes con dos nodos, maestro y nodo 01, porque el maestro tiene más servicios que el nodo 01. En este caso, pase lo que pase, el resultado de la programación siempre se programará para el nodo 01: