Kubernetes-Scheduling entmystifizieren: Ein tiefer Einblick in Predicates und Priorities

Kubernetes-Scheduling entmystifizieren: Ein tiefer Einblick in Predicates und Priorities

Der heutige Artikel stellt hauptsächlich die Phasen im Scheduling-Prozess vor, in denen die Scheduling-Strategien von Predicates und Priorities zum Tragen kommen.

Predicates

Schauen wir uns zunächst Predicates an. Die Rolle von Predicates im Scheduling-Prozess kann man sich wie einen Filter vorstellen, der eine Reihe von Knoten, die die Bedingungen erfüllen, aus allen Knoten im aktuellen Cluster gemäß der Scheduling-Richtlinie „herausfiltert“. Diese Knoten sind alle potenziellen Hosts für den Pod, der darauf wartet, eingeplant zu werden. In Kubernetes gibt es vier Standard-Scheduling-Richtlinien.

Der erste Typ heißt GeneralPredicates.

Wie der Name schon sagt, ist diese Gruppe von Filterregeln für die grundlegendsten Scheduling-Richtlinien verantwortlich. Zum Beispiel berechnet PodFitsResources, ob die CPU- und Arbeitsspeicher-Ressourcen des Hosts ausreichen. Wie bereits erwähnt, prüft PodFitsResources nur das requests-Feld des Pods. Es ist wichtig zu beachten, dass der Kubernetes-Scheduler keine spezifischen Ressourcentypen für Hardware-Ressourcen wie GPUs definiert, sondern diese im Key-Value-Format als Extended Resource beschreibt. Zum Beispiel:

apiVersion: v1
kind: Pod
metadata:
  name: extended-resource-demo
spec:
  containers:
  - name: extended-resource-demo-ctr
    image: nginx
    resources:
      requests:
        alpha.kubernetes.io/nvidia-gpu: "2"
      limits:
        alpha.kubernetes.io/nvidia-gpu: "2"

Man sieht, dass unser Pod die Verwendung von zwei NVIDIA-GPUs mit der Definition alpha.kubernetes.io/nvidia-gpu=2 deklariert. In PodFitsResources weiß der Scheduler nicht wirklich, dass die Bedeutung dieses Key-Felds GPU ist, sondern verwendet direkt den nachfolgenden Value für die Berechnung. Natürlich muss im Capacity-Feld des Nodes auch die Gesamtzahl der GPUs auf diesem Host angegeben werden, z. B. alpha.kubernetes.io/nvidia-gpu=4. Diese Prozesse werde ich später bei der Erklärung des Device Plugin im Detail vorstellen.
PodFitsHost prüft, ob der Name des Hosts mit spec.nodeName des Pods übereinstimmt.
PodFitsHostPorts prüft, ob die vom Pod angeforderten Host-Ports (spec.nodePort) mit bereits verwendeten Ports kollidieren.
PodMatchNodeSelector prüft, ob die vom Pod über nodeSelector oder nodeAffinity angegebenen Knoten mit dem untersuchten Knoten übereinstimmen, und so weiter.
Man sieht, dass eine solche Gruppe von GeneralPredicates die grundlegendste Filterbedingung für Kubernetes ist, um zu prüfen, ob ein Pod auf einem Node laufen kann. Daher werden GeneralPredicates auch direkt von anderen Komponenten (z. B. kubelet) aufgerufen.

Der zweite Typ sind die volumenbezogenen Filterregeln.

Diese Gruppe von Filterregeln ist für die Scheduling-Richtlinie in Bezug auf persistente Volumes des Containers zuständig.
NoDiskConflict prüft, ob es einen Konflikt zwischen den von mehreren Pods deklarierten persistenten Volumes gibt. Beispielsweise dürfen AWS-EBS-Volumes nicht gleichzeitig von zwei Pods verwendet werden. Wenn also ein EBS-Volume namens A bereits auf einem bestimmten Knoten gemountet ist, kann ein anderer Pod, der ebenfalls die Verwendung dieses A-Volumes deklariert, nicht auf diesen Knoten eingeplant werden.
MaxPDVolumeCountPredicate prüft, ob die Anzahl eines bestimmten Typs von persistenten Volumes auf einem Knoten einen bestimmten Wert überschritten hat. Wenn ja, kann der Pod, der die Verwendung dieses Typs von persistenten Volumes deklariert, nicht mehr auf diesen Knoten eingeplant werden.
VolumeZonePredicate prüft, ob das Zone-Label (Hochverfügbarkeitsdomäne) des persistenten Volumes mit dem Zone-Label des untersuchten Knotens übereinstimmt.
Außerdem gibt es eine Regel namens VolumeBindingPredicate. Sie prüft, ob das nodeAffinity-Feld des zum Pod gehörigen PVs mit dem Label eines bestimmten Knotens übereinstimmt. Local Persistent Volumes (lokale persistente Volumes) müssen nodeAffinity verwenden, um an einen bestimmten Knoten gebunden zu werden. Das bedeutet, dass Kubernetes während der Predicates-Phase in der Lage sein muss, basierend auf den Volume-Eigenschaften des Pods zu schedulen.
Wenn der PVC des Pods noch nicht an ein bestimmtes PV gebunden ist, ist der Scheduler außerdem dafür verantwortlich, alle zu bindenden PVs zu überprüfen. Wenn ein verfügbares PV vorhanden ist und die nodeAffinity dieses PVs mit dem untersuchten Knoten übereinstimmt, gibt diese Regel „Erfolg“ zurück. Zum Beispiel:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-local-pv
spec:
  capacity:
    storage: 500Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-storage
  local:
    path: /mnt/disks/vol1
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - my-node

Man sieht, dass das persistente Verzeichnis, das diesem PV entspricht, nur auf dem Host namens my-node erscheint. Daher muss jeder Pod, der dieses PV über einen PVC verwendet, auf my-node eingeplant werden, um ordnungsgemäß zu funktionieren. Genau das entscheidet VolumeBindingPredicate im Scheduler.

Der dritte Typ sind hostbezogene Filterregeln.

Diese Gruppe von Regeln prüft hauptsächlich, ob der zu schedulende Pod bestimmte Bedingungen des Nodes selbst erfüllt.
PodToleratesNodeTaints prüft den oft verwendeten „Taint“-Mechanismus des Nodes. Nur wenn das Toleration-Feld des Pods mit dem Taint-Feld des Nodes übereinstimmt, kann dieser Pod auf den Node eingeplant werden.
NodeMemoryPressurePredicate prüft, ob der Arbeitsspeicher des aktuellen Nodes nicht mehr ausreicht. Wenn ja, kann der wartende Pod nicht auf diesen Node eingeplant werden.

Der vierte Typ sind podbezogene Filterregeln.

Diese Gruppe von Regeln überschneidet sich mit den meisten GeneralPredicates. Besonders ist hier PodAffinityPredicate. Die Aufgabe dieser Regel ist es, die Affinitäts- und Anti-Affinitäts-Beziehungen zwischen dem zu schedulenden Pod und den bestehenden Pods auf dem Node zu prüfen. Zum Beispiel:

apiVersion: v1
kind: Pod
metadata:
  name: with-pod-antiaffinity
spec:
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: security
              operator: In
              values:
              - S2
          topologyKey: kubernetes.io/hostname
  containers:
  - name: with-pod-affinity
    image: docker.io/ocpqe/hello-pod

In diesem Beispiel legt die podAntiAffinity-Regel fest, dass dieser Pod nicht auf demselben Node vorhanden sein möchte wie ein Pod mit dem Label security=S2. Es ist zu beachten, dass PodAffinityPredicate einen Gültigkeitsbereich hat – die obige Regel gilt beispielsweise nur für Nodes, die das Label mit dem Key kubernetes.io/hostname tragen. Dies ist die Aufgabe des Schlüsselworts topologyKey.

Diese vier Arten von Predicates bilden die grundlegende Strategie des Schedulers, um zu bestimmen, ob ein Node den zu schedulenden Pod ausführen kann.
Bei der Ausführung, wenn ein Pod eingeplant wird, startet der Kubernetes-Scheduler gleichzeitig 16 Goroutinen, um die Predicates für alle Nodes im Cluster parallel zu berechnen und schließlich die Liste der Hosts zurückzugeben, die diesen Pod ausführen können.
Es ist wichtig zu beachten, dass der Scheduler bei der Ausführung der Predicates für jeden Node in einer festgelegten Reihenfolge prüft. Diese Reihenfolge wird basierend auf der Bedeutung der Predicates selbst bestimmt. Zum Beispiel werden hostbezogene Predicates früher geprüft. Andernfalls wäre es sinnlos, PodAffinityPredicate auf einem Host mit stark unzureichenden Ressourcen zu berechnen.

Priorities

Nach dem „Filtern“ der Knoten in der Predicates-Phase besteht die Aufgabe der Priorities-Phase darin, diese Knoten zu bewerten. Der Bewertungsbereich liegt hier bei 0–10 Punkten, und der Knoten mit der höchsten Punktzahl ist der beste Knoten, an den der Pod schließlich gebunden wird.
Die am häufigsten verwendete Bewertungsregel in Priorities ist LeastRequestedPriority. Dieser Algorithmus wählt tatsächlich den Host mit den meisten freien Ressourcen (CPU und Arbeitsspeicher) aus.
Darüber hinaus gibt es drei weitere Priorities: NodeAffinityPriority, TaintTolerationPriority und InterPodAffinityPriority. Wie die Namen schon sagen, ähneln sie in Bedeutung und Berechnungsmethode den vorherigen drei Predicates: PodMatchNodeSelector, PodToleratesNodeTaints und PodAffinityPredicate. Allerdings gilt: Je mehr Felder eines Nodes die obigen Regeln erfüllen, desto höher ist seine Punktzahl als Priority.
In den Standard-Priorities gibt es auch eine Strategie namens ImageLocalityPriority. Es handelt sich um eine neue Scheduling-Regel, die in Kubernetes v1.12 aktiviert wurde: Wenn das vom zu schedulenden Pod benötigte Image groß ist und bereits auf einigen Nodes vorhanden ist, erhalten diese Nodes eine höhere Punktzahl.
Um zu vermeiden, dass der Algorithmus zu Scheduling-Staus führt, optimiert der Scheduler bei der Bewertung auch je nach Verteilung des Images: Wenn die Anzahl der Nodes, auf denen das große Image verteilt ist, sehr gering ist, wird das Gewicht dieser Nodes reduziert, wodurch das Risiko eines Scheduling-Staus „ausgeglichen“ wird.
Zusammenfassend ist dies das grundlegende Funktionsprinzip der Standard-Scheduling-Regeln im Kubernetes-Scheduler.
Im tatsächlichen Ausführungsprozess werden die Informationen über den Cluster und die Pods im Scheduler zwischengespeichert, sodass die Ausführung dieser Algorithmen relativ schnell ist.
Darüber hinaus achten komplexere Scheduling-Algorithmen wie PodAffinityPredicate bei der Berechnung nicht nur auf den zu schedulenden Pod und den untersuchten Node, sondern müssen auch die Informationen des gesamten Clusters berücksichtigen, z. B. das Durchlaufen aller Nodes und das Lesen ihrer Labels. In diesem Fall berechnet der Kubernetes-Scheduler die vom Algorithmus benötigten Clusterinformationen vorab, bevor er den Scheduling-Algorithmus für jeden zu schedulenden Pod ausführt, und speichert sie dann zwischen. Auf diese Weise muss der Scheduler bei der tatsächlichen Ausführung des Algorithmus nur die zwischengespeicherten Informationen für die Berechnung lesen, wodurch vermieden wird, für jeden Node bei der Berechnung der Predicates wiederholt die gesamten Clusterinformationen abrufen und berechnen zu müssen.

Zusammenfassung

Zusammenfassend hat dieser Artikel die wichtigsten Scheduling-Algorithmen im Standard-Scheduler von Kubernetes behandelt. Es ist wichtig zu beachten, dass es neben den in diesem Artikel behandelten Regeln tatsächlich einige Strategien im Kubernetes-Scheduler gibt, die standardmäßig nicht aktiviert sind. Sie können eine Konfigurationsdatei für kube-scheduler angeben oder einen ConfigMap erstellen, um zu konfigurieren, welche Regeln aktiviert oder deaktiviert werden sollen. Darüber hinaus können Sie das Verhalten des Schedulers steuern, indem Sie Gewichtungen für die Priorities festlegen.
Damit endet die Erklärung der Standard-Scheduling-Mechanismen im Kubernetes-Scheduler. Erwähnenswert ist, dass der Scheduler aufgrund des Cachings von Cluster- und Pod-Informationen zwar schnell arbeitet, aber bei komplexeren Algorithmen den gesamten Clusterzustand berücksichtigen muss, nicht nur den Pod und den betreffenden Node. Dies umfasst vorläufige Berechnungen und das Caching der erforderlichen Clusterinformationen vor der tatsächlichen Ausführung des Scheduling-Algorithmus, um Effizienz und Genauigkeit im Scheduling-Prozess zu gewährleisten.

Novita AI ist die All-in-One-Cloud-Plattform, die Ihre KI-Ambitionen unterstützt. Mit nahtlos integrierten APIs, Serverless Computing und GPU-Beschleunigung bieten wir die kosteneffektiven Tools, die Sie benötigen, um Ihr KI-gesteuertes Unternehmen schnell aufzubauen und zu skalieren. Vermeiden Sie Infrastruktur-Headaches und starten Sie kostenlos – Novita AI macht Ihre KI-Träume wahr.

Empfohlene Lektüre:

  1. What You Need to Know About Docker
  2. Why We Need Pods