Parte 2: Implementación
Enfoque de Scheduler Extender
El enfoque de scheduler extender es una forma de extender la funcionalidad del scheduler predeterminado. En este enfoque, el scheduler predeterminado llama a un servicio externo (scheduler extender) mediante solicitudes HTTP para proporcionar funciones adicionales de filtrado y prioridad.
Arquitectura de Scheduler Extender
El siguiente diagrama muestra la arquitectura del enfoque de scheduler extender:
Flujo de trabajo de Scheduler Extender
El flujo de trabajo de scheduler extender es el siguiente:
Implementación de Scheduler Extender
Un scheduler extender debe proporcionar los siguientes endpoints HTTP:
- Filter: Filtra los nodes donde el Pod no puede ejecutarse.
- Prioritize: Asigna puntuaciones de prioridad a los nodes.
- Bind: Vincula el Pod a un node (opcional).
- Prefilter: Inspecciona el Pod antes del filtrado (opcional).
- Prescore: Inspecciona el Pod antes de la puntuación (opcional).
Aquí tienes un ejemplo sencillo de scheduler extender usando Go:
package main
import (
"encoding/json"
"log"
"net/http"
"github.com/julienschmidt/httprouter"
extenderv1 "k8s.io/kube-scheduler/extender/v1"
)
func main() {
router := httprouter.New()
router.POST("/filter", filterHandler)
router.POST("/prioritize", prioritizeHandler)
log.Fatal(http.ListenAndServe(":8888", router))
}
// Filter handler
func filterHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
var extenderArgs extenderv1.ExtenderArgs
var extenderFilterResult extenderv1.ExtenderFilterResult
if err := json.NewDecoder(r.Body).Decode(&extenderArgs); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Simple example allowing all nodes
extenderFilterResult.Nodes = extenderArgs.Nodes
extenderFilterResult.FailedNodes = make(map[string]string)
// Filter nodes based on specific conditions
// Example: Allow only nodes with GPUs for pods requiring GPUs
if requiresGPU(&extenderArgs.Pod) {
filteredNodes := &extenderv1.NodeList{
Items: make([]extenderv1.Node, 0),
}
for _, node := range extenderArgs.Nodes.Items {
if hasGPU(&node) {
filteredNodes.Items = append(filteredNodes.Items, node)
} else {
extenderFilterResult.FailedNodes[node.Name] = "Node does not have GPU"
}
}
extenderFilterResult.Nodes = filteredNodes
}
if err := json.NewEncoder(w).Encode(extenderFilterResult); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// Prioritize handler
func prioritizeHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
var extenderArgs extenderv1.ExtenderArgs
var hostPriorityList extenderv1.HostPriorityList
if err := json.NewDecoder(r.Body).Decode(&extenderArgs); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Assign scores to each node
hostPriorityList = make(extenderv1.HostPriorityList, len(extenderArgs.Nodes.Items))
for i, node := range extenderArgs.Nodes.Items {
// Simple example: assign same score to all nodes
hostPriorityList[i] = extenderv1.HostPriority{
Host: node.Name,
Score: 1,
}
// Adjust score based on specific conditions
// Example: Assign higher score to nodes with more GPU memory
if requiresGPU(&extenderArgs.Pod) && hasGPU(&node) {
gpuMemory := getGPUMemory(&node)
hostPriorityList[i].Score = int64(gpuMemory / 1024) // Convert to GB
}
}
if err := json.NewEncoder(w).Encode(hostPriorityList); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// Function to check GPU requirements
func requiresGPU(pod *extenderv1.Pod) bool {
// Check GPU requirements from pod's resource requests
for _, container := range pod.Spec.Containers {
if _, ok := container.Resources.Requests["nvidia.com/gpu"]; ok {
return true
}
}
return false
}
// Function to check if node has GPU
func hasGPU(node *extenderv1.Node) bool {
// Check GPU from node's capacity
if _, ok := node.Status.Capacity["nvidia.com/gpu"]; ok {
return true
}
return false
}
// Function to check node's GPU memory
func getGPUMemory(node *extenderv1.Node) int {
// Check GPU memory from node labels
if memoryStr, ok := node.Labels["gpu.nvidia.com/memory"]; ok {
var memory int
fmt.Sscanf(memoryStr, "%d", &memory)
return memory
}
return 0
}Deployment de Scheduler Extender
Compila el scheduler extender como una imagen de contenedor y despliégalo en Kubernetes:
apiVersion: apps/v1
kind: Deployment
metadata:
name: scheduler-extender
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: scheduler-extender
template:
metadata:
labels:
app: scheduler-extender
spec:
containers:
- name: scheduler-extender
image: your-registry/scheduler-extender:latest
ports:
- containerPort: 8888
resources:
requests:
cpu: "100m"
memory: "100Mi"
limits:
cpu: "200m"
memory: "200Mi"
---
apiVersion: v1
kind: Service
metadata:
name: scheduler-extender
namespace: kube-system
spec:
selector:
app: scheduler-extender
ports:
- port: 8888
targetPort: 8888Configuración del scheduler
Para usar el scheduler extender, debes modificar la configuración del scheduler predeterminado. En EKS, puedes configurarlo de la siguiente manera:
- Crear un archivo de configuración del scheduler:
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
clientConnection:
kubeconfig: /etc/kubernetes/scheduler.conf
extenders:
- urlPrefix: "http://scheduler-extender.kube-system.svc.cluster.local:8888"
filterVerb: "filter"
prioritizeVerb: "prioritize"
weight: 1
enableHTTPS: false
nodeCacheCapable: false- Crear la configuración del scheduler como ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: scheduler-config
namespace: kube-system
data:
scheduler-config.yaml: |
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
clientConnection:
kubeconfig: /etc/kubernetes/scheduler.conf
extenders:
- urlPrefix: "http://scheduler-extender.kube-system.svc.cluster.local:8888"
filterVerb: "filter"
prioritizeVerb: "prioritize"
weight: 1
enableHTTPS: false
nodeCacheCapable: false- Deployment del scheduler personalizado:
apiVersion: apps/v1
kind: Deployment
metadata:
name: custom-scheduler
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: custom-scheduler
template:
metadata:
labels:
app: custom-scheduler
spec:
serviceAccountName: custom-scheduler
containers:
- name: kube-scheduler
image: k8s.gcr.io/kube-scheduler:v1.23.0
command:
- kube-scheduler
- --config=/etc/kubernetes/scheduler-config.yaml
- --v=3
volumeMounts:
- name: scheduler-config
mountPath: /etc/kubernetes/scheduler-config.yaml
subPath: scheduler-config.yaml
- name: kubeconfig
mountPath: /etc/kubernetes/scheduler.conf
readOnly: true
volumes:
- name: scheduler-config
configMap:
name: scheduler-config
- name: kubeconfig
hostPath:
path: /etc/kubernetes/scheduler.conf
type: FilePlugins del Scheduler Framework
El scheduler framework introducido en Kubernetes 1.15 proporciona una arquitectura basada en plugins. Este enfoque te permite implementar plugins en varias etapas del pipeline de scheduling.
Arquitectura del Scheduler Framework
El siguiente diagrama muestra la arquitectura del scheduler framework:
Configuración de plugins del Scheduler Framework
El siguiente diagrama muestra la configuración de plugins del scheduler framework:
Puntos de extensión del Scheduling Framework
El scheduling framework proporciona los siguientes puntos de extensión:
- QueueSort: Determina el orden de los Pods en la cola de scheduling.
- PreFilter: Inspecciona los Pods y prepara datos de filtrado antes del filtrado.
- Filter: Filtra los nodes donde el Pod no puede ejecutarse.
- PreScore: Inspecciona los Pods y prepara datos de puntuación antes de la puntuación.
- Score: Asigna puntuaciones a los nodes.
- NormalizeScore: Normaliza las puntuaciones de cada scoring plugin.
- Reserve: Reserva recursos para el Pod.
- Permit: Determina si el Pod puede programarse.
- PreBind: Realiza las operaciones necesarias antes del binding.
- Bind: Vincula el Pod a un node.
- PostBind: Realiza las operaciones necesarias después del binding.
Implementación de Scheduler Plugin
Aquí tienes un ejemplo sencillo de scheduler plugin usando Go:
package main
import (
"context"
"fmt"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubernetes/pkg/scheduler/framework"
)
// GPUSchedulerPlugin is a plugin that filters and scores nodes based on GPU requirements.
type GPUSchedulerPlugin struct{}
var _ framework.FilterPlugin = &GPUSchedulerPlugin{}
var _ framework.ScorePlugin = &GPUSchedulerPlugin{}
// Name returns the name of the plugin.
func (gsp *GPUSchedulerPlugin) Name() string {
return "GPUScheduler"
}
// Filter filters out nodes where the pod cannot run.
func (gsp *GPUSchedulerPlugin) Filter(ctx context.Context, state *framework.CycleState, pod *v1.Pod, node *framework.NodeInfo) *framework.Status {
// For pods with GPU requirements, allow only nodes with GPUs
if requiresGPU(pod) && !hasGPU(node.Node()) {
return framework.NewStatus(framework.Unschedulable, "Node does not have GPU")
}
return framework.NewStatus(framework.Success, "")
}
// Score assigns scores to nodes.
func (gsp *GPUSchedulerPlugin) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
nodeInfo, err := state.Read(framework.NodeInfoKey)
if err != nil {
return 0, framework.NewStatus(framework.Error, fmt.Sprintf("Error reading node info: %v", err))
}
node := nodeInfo.(*framework.NodeInfo).Node()
// For pods with GPU requirements, assign scores based on GPU memory
if requiresGPU(pod) && hasGPU(node) {
gpuMemory := getGPUMemory(node)
return int64(gpuMemory / 1024), framework.NewStatus(framework.Success, "") // Convert to GB
}
return 0, framework.NewStatus(framework.Success, "")
}
// ScoreExtensions returns extensions for the score plugin.
func (gsp *GPUSchedulerPlugin) ScoreExtensions() framework.ScoreExtensions {
return gsp
}
// NormalizeScore normalizes the scores.
func (gsp *GPUSchedulerPlugin) NormalizeScore(ctx context.Context, state *framework.CycleState, pod *v1.Pod, scores framework.NodeScoreList) *framework.Status {
// Find maximum score
var maxScore int64 = 1
for _, score := range scores {
if score.Score > maxScore {
maxScore = score.Score
}
}
// Normalize scores (0-100 range)
for i := range scores {
if maxScore > 0 {
scores[i].Score = scores[i].Score * 100 / maxScore
} else {
scores[i].Score = 0
}
}
return framework.NewStatus(framework.Success, "")
}
// Function to check GPU requirements
func requiresGPU(pod *v1.Pod) bool {
// Check GPU requirements from pod's resource requests
for _, container := range pod.Spec.Containers {
if _, ok := container.Resources.Requests["nvidia.com/gpu"]; ok {
return true
}
}
return false
}
// Function to check if node has GPU
func hasGPU(node *v1.Node) bool {
// Check GPU from node's capacity
if _, ok := node.Status.Capacity["nvidia.com/gpu"]; ok {
return true
}
return false
}
// Function to check node's GPU memory
func getGPUMemory(node *v1.Node) int {
// Check GPU memory from node labels
if memoryStr, ok := node.Labels["gpu.nvidia.com/memory"]; ok {
var memory int
fmt.Sscanf(memoryStr, "%d", &memory)
return memory
}
return 0
}
// New creates a new instance of the plugin.
func New(_ runtime.Object, _ framework.Handle) (framework.Plugin, error) {
return &GPUSchedulerPlugin{}, nil
}Registro de Scheduler Plugin
Para registrar un scheduler plugin, debes modificar el archivo de configuración del scheduler:
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
clientConnection:
kubeconfig: /etc/kubernetes/scheduler.conf
profiles:
- schedulerName: custom-scheduler
plugins:
filter:
enabled:
- name: GPUScheduler
score:
enabled:
- name: GPUScheduler
weight: 10
pluginConfig:
- name: GPUScheduler
args: {}Implementación del Scheduler Framework en EKS
Al implementar el scheduler framework en Amazon EKS, considera lo siguiente:
- Compilación de imagen de contenedor: Compila el scheduler plugin personalizado como una imagen de contenedor y publícala en un registro de contenedores como Amazon ECR.
- Configuración del scheduler: Crea la configuración del scheduler como ConfigMap y móntala en el Pod del scheduler personalizado.
- Permisos RBAC: Configura permisos RBAC adecuados para que el scheduler personalizado pueda acceder a los recursos necesarios.
- Etiquetado de nodes: Etiqueta los nodes según características de hardware específicas (por ejemplo, GPUs).
Arquitectura de EKS Scheduler Framework
El siguiente diagrama muestra cómo implementar el scheduler framework en EKS:
Pasos de implementación de EKS Scheduler Framework
- Desarrollo de Scheduler Plugin personalizado:
// main.go
package main
import (
"os"
"k8s.io/component-base/logs"
"k8s.io/kubernetes/cmd/kube-scheduler/app"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/defaultbinder"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/defaultpreemption"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/nodeaffinity"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/nodename"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/nodeports"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/noderesources"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/nodeunschedulable"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/podtopologyspread"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/queuesort"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/tainttoleration"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/volumebinding"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/volumerestrictions"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/volumezone"
// Import custom plugins
"example.com/gpu-scheduler/pkg/gpuplugin"
"example.com/gpu-scheduler/pkg/spotplugin"
"example.com/gpu-scheduler/pkg/azplugin"
)
func main() {
command := app.NewSchedulerCommand(
app.WithPlugin(gpuplugin.Name, gpuplugin.New),
app.WithPlugin(spotplugin.Name, spotplugin.New),
app.WithPlugin(azplugin.Name, azplugin.New),
// Include default plugins
app.WithPlugin(defaultpreemption.Name, defaultpreemption.New),
app.WithPlugin(noderesources.FitName, noderesources.NewFit),
app.WithPlugin(noderesources.BalancedAllocationName, noderesources.NewBalancedAllocation),
app.WithPlugin(nodename.Name, nodename.New),
app.WithPlugin(nodeports.Name, nodeports.New),
app.WithPlugin(nodeaffinity.Name, nodeaffinity.New),
app.WithPlugin(nodeunschedulable.Name, nodeunschedulable.New),
app.WithPlugin(tainttoleration.Name, tainttoleration.New),
app.WithPlugin(volumerestrictions.Name, volumerestrictions.New),
app.WithPlugin(volumebinding.Name, volumebinding.New),
app.WithPlugin(volumezone.Name, volumezone.New),
app.WithPlugin(podtopologyspread.Name, podtopologyspread.New),
app.WithPlugin(defaultbinder.Name, defaultbinder.New),
app.WithPlugin(queuesort.Name, queuesort.New),
)
logs.InitLogs()
defer logs.FlushLogs()
if err := command.Execute(); err != nil {
os.Exit(1)
}
}- Creación de Dockerfile:
FROM golang:1.17 as builder
WORKDIR /go/src/example.com/gpu-scheduler
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o kube-scheduler .
FROM alpine:3.14
RUN apk --no-cache add ca-certificates
WORKDIR /
COPY --from=builder /go/src/example.com/gpu-scheduler/kube-scheduler .
ENTRYPOINT ["/kube-scheduler"]- Compilación y publicación de imagen:
docker build -t your-registry/gpu-scheduler:latest .
docker push your-registry/gpu-scheduler:latest- Creación del ConfigMap de configuración del scheduler:
apiVersion: v1
kind: ConfigMap
metadata:
name: gpu-scheduler-config
namespace: kube-system
data:
scheduler-config.yaml: |
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
clientConnection:
kubeconfig: /etc/kubernetes/scheduler.conf
profiles:
- schedulerName: gpu-scheduler
plugins:
queueSort:
enabled:
- name: PrioritySort
preFilter:
enabled:
- name: NodeResourcesFit
- name: NodePorts
- name: PodTopologySpread
- name: InterPodAffinity
- name: VolumeBinding
- name: NodeAffinity
- name: GPUScheduler
filter:
enabled:
- name: NodeUnschedulable
- name: NodeName
- name: TaintToleration
- name: NodeAffinity
- name: NodePorts
- name: NodeResourcesFit
- name: VolumeRestrictions
- name: EBSLimits
- name: VolumeBinding
- name: VolumeZone
- name: PodTopologySpread
- name: InterPodAffinity
- name: GPUScheduler
- name: SpotScheduler
- name: AZScheduler
preScore:
enabled:
- name: InterPodAffinity
- name: PodTopologySpread
- name: TaintToleration
- name: NodeAffinity
- name: GPUScheduler
score:
enabled:
- name: NodeResourcesBalancedAllocation
weight: 1
- name: ImageLocality
weight: 1
- name: InterPodAffinity
weight: 1
- name: NodeResourcesFit
weight: 1
- name: NodeAffinity
weight: 1
- name: PodTopologySpread
weight: 2
- name: TaintToleration
weight: 1
- name: GPUScheduler
weight: 10
- name: SpotScheduler
weight: 5
- name: AZScheduler
weight: 3
reserve:
enabled:
- name: VolumeBinding
permit:
enabled: []
preBind:
enabled:
- name: VolumeBinding
bind:
enabled:
- name: DefaultBinder
postBind:
enabled: []
pluginConfig:
- name: GPUScheduler
args: {}
- name: SpotScheduler
args: {}
- name: AZScheduler
args: {}- Deployment del scheduler personalizado:
apiVersion: v1
kind: ServiceAccount
metadata:
name: gpu-scheduler
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: gpu-scheduler
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: [""]
resources: ["pods/binding"]
verbs: ["create"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: gpu-scheduler
subjects:
- kind: ServiceAccount
name: gpu-scheduler
namespace: kube-system
roleRef:
kind: ClusterRole
name: gpu-scheduler
apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gpu-scheduler
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: gpu-scheduler
template:
metadata:
labels:
app: gpu-scheduler
spec:
serviceAccountName: gpu-scheduler
containers:
- name: gpu-scheduler
image: your-registry/gpu-scheduler:latest
args:
- --config=/etc/kubernetes/scheduler-config.yaml
- --v=3
volumeMounts:
- name: scheduler-config
mountPath: /etc/kubernetes/scheduler-config.yaml
subPath: scheduler-config.yaml
volumes:
- name: scheduler-config
configMap:
name: gpu-scheduler-config- Especificación del scheduler en el Pod:
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
schedulerName: gpu-scheduler
containers:
- name: cuda-container
image: nvidia/cuda:11.0-base
resources:
limits:
nvidia.com/gpu: 1Conclusión
En este capítulo, cubrimos la implementación de schedulers personalizados usando el enfoque de scheduler extender y plugins del scheduler framework. También exploramos cómo implementar el scheduler framework en clusters de EKS.
En el próximo capítulo, veremos casos de implementación de schedulers personalizados en EKS y métodos de monitoreo.
Cuestionario
Para comprobar lo que has aprendido en este capítulo, prueba el cuestionario del tema.