Kubernetes Scheduling: Wo Pods lernen, sich anzustellen (oder auch nicht)
Hast du dich schon mal gefragt, wie der Kubernetes Scheduler im Hintergrund entscheidet, auf welchem Node ein Pod landen soll? Stell dir vor, du hast ein Deployment erstellt, das 5 Replikate eines Pods enthält. Wie bestimmt der kube-scheduler, auf welchen Node jeder einzelne Pod platziert wird? Und was, wenn du willst, dass ein Pod auf einem bestimmten Node im Kubernetes-Cluster ausgeführt wird?
In diesem Blog schauen wir uns an, wie der kube-scheduler im Inneren funktioniert und welchem Algorithmus er grundsätzlich folgt – mit einem praktischen, hands-on Ansatz.
Bevor du weitermachst, ist es hilfreich, ein Grundverständnis der Kubernetes-Architektur zu haben. Falls du dich über die Kubernetes-Architektur informieren möchtest, kannst du meinen Blogeintrag dazu lesen – oder jede andere Quelle deiner Wahl verwenden.
Stell dir vor, du hast x Pods, die auf bestimmten Nodes im Kubernetes-Cluster ausgeführt werden sollen. Der Scheduler führt intern zwei Hauptschritte durch:
- Filtern (Filtering): Im ersten Schritt sucht der Scheduler eine Auswahl an Nodes, auf denen das Scheduling des Pods überhaupt möglich ist. In der Pod-Spezifikation definierst du den gewünschten Zustand des Pods. Basierend darauf filtert der Scheduler die Nodes, die diesen Zustand erfüllen können.
Beispiel: Dein Pod benötigt 64 MiB Arbeitsspeicher – der Scheduler durchsucht die verfügbaren Nodes und schließt alle aus, die diesen Arbeitsspeicher nicht bereitstellen können. Ein Node mit maximal 50 MiB Speicher würde also direkt rausfallen. - Bewerten (Scoring): Im zweiten Schritt bewertet der Scheduler die verbleibenden Nodes mithilfe eines Punktesystems, um den besten Node für die Platzierung zu finden. Jeder Node bekommt anhand aktiver Bewertungsregeln eine Punktzahl zugewiesen.
Schlussendlich weist der kube-scheduler den Pod dem Node mit der höchsten Punktzahl zu. Gibt es mehrere Nodes mit gleicher Punktzahl, wird einer davon zufällig ausgewählt.
Angenommen du hast folgende Nodes:- Node A:
- CPU: 4 Cores
- RAM: 8 GB
- Node B:
- CPU: 2 Cores
- RAM: 4 GB
- Node C:
- CPU: 8 Cores
- RAM: 16 GB
Und du hast einen Pod mit diesem Ressourcenbedarf:
- CPU: 3 Cores
- RAM: 6 GB
Vereinfachen wir den Scoring-Prozess durch eine einfache Rechnung:
- Ressourcen-Punkte berechnen:
- Node A: CPU-Score = 4 - 3 = 1, RAM-Score = 8 - 6 = 2
- Node B: CPU-Score = 2 - 3 = -1 (Negative Werte = Bestrafung), RAM-Score = 4 - 6 = -2
- Node C: CPU-Score = 8 - 3 = 5, RAM-Score = 16 - 6 = 10
- Gesamtpunkte berechnen:
- Node A: 1 + 2 = 3
- Node B: -1 + (-2) = -3
- Node C: 5 + 10 = 15
- Auswahl der Node mit der höchsten Punktzahl:
- Node C hat die höchste Punktzahl (15), daher wird der Pod auf diesem Node platziert.
Schauen wir nun auf verschiedene Möglichkeiten, wie du Pods gezielt auf bestimmte Nodes schedulen kannst, je nach Anforderung.
- Manuelles Scheduling
Beim manuellen Scheduling bestimmst du explizit, auf welchem Node ein bestimmter Pod laufen soll. Anstatt den Scheduler automatisch eine Entscheidung treffen zu lassen, gibst du ihm konkret vor, wo der Pod platziert wird. Dazu nutzt du das FeldnodeName
in der Pod-Definition (YAML-Datei) und gibst den Namen des gewünschten Nodes an.
apiVersion: v1 kind: Pod metadata: name: nginx-pod spec: containers: - name: nginx-container image: nginx:latest nodeName: node01
Du kannst dir mit folgendem Befehl alle verfügbaren Nodes in deinem Cluster anzeigen lassen:
kubectl get nodes
In dem obigen Bild siehst du, dass der nginx-Pod auf dem node01 geplant wurde (schau dir den Abschnitt NODE an, wenn du den Befehlk get po -o wide
ausführst).
Bevor wir weitermachen, werfen wir einen Blick auf eine andere Möglichkeit, Pods anhand von Labels und Selektoren zu gruppieren. Labels sind wie Haftnotizen oder Tags, die du Objekten wie Pods, Nodes oder Services anhängen kannst, um ihnen zusätzliche Informationen oder Eigenschaften zu geben. Sie bestehen einfach aus Schlüssel-Wert-Paaren, die du einem Pod zuweist, um ihn von anderen zu unterscheiden.
Zum Beispiel:language: Go
,specie: human
,favSport: Soccer
,color: blue
– das sind Beispiele für Labels. Selektoren sind einfach eine Möglichkeit, alle Pods mit bestimmten Labels auszuwählen. Ein Pod kann mehrere Labels zugewiesen bekommen.
- Taints und Tolerations: Das ist eine wirklich interessante Methode, um zu verstehen, wie Pods auf einem Node geplant werden. Ein Taint ist wie ein Spray, das du auf einen bestimmten Node auftragen kannst, sodass nur ein spezieller Pod, der dieses Spray tolerieren kann, auf diesem Node geplant werden darf. Denk dran: Taints werden auf Nodes angewendet, während Tolerations auf Pods angewendet werden.
In dem obigen Bild siehst du drei Nodes – Node01, Node02 und Node03 – zusammen mit 5 Pods. Auf Node01 wurde ein Taint angewendet, während Pod1 der einzige Pod ist, der diesen Taint tolerieren kann. Das bedeutet, dass Pod1 auf Node01 geplant werden kann, während die restlichen Pods niemals auf Node01 geplant werden können, weil sie den Taint nicht tolerieren, den Node01 hat. Wichtig ist, dass Taints und Tolerations keine Garantie dafür geben, dass Pod1 ausschließlich auf Node01 geplant wird. Es ist möglich, dass Pod1 auch auf einem anderen Node geplant wird – während die anderen Pods auf jedem Node außer Node01 geplant werden können.
Hast du dich jemals gefragt, warum ein Pod nie auf dem Master-Node geplant wird? Das liegt daran, dass der Master-Node bereits standardmäßig einen Taint gesetzt hat, der verhindert, dass andere Pods darauf geplant werden. Du kannst den Taint auf dem Master-Node mit folgendem Befehl anzeigen:
kubectl describe node <master-node-name> | grep Taints
Um einen Node mit einem Taint zu versehen, verwende diesen Befehl:
kubectl taint nodes node1 key1=value1:NoSchedule
Der obige Befehl verwendet deneffekt
NoSchedule
. Es gibt noch weitere Effekte, die du nutzen kannst – welche genau, erkläre ich dir im Folgenden:NoExecute:
Dieses Taint verhindert nicht nur, dass neue Pods auf einem betroffenen Node geplant werden, sondern es entfernt auch bestehende Pods auf diesem Node, die das Taint nicht tolerieren. Diese Entfernung ist das Unterscheidungsmerkmal vonNoExecute
-Taints im Vergleich zu normalen Taints, die lediglich neue Pod-Zuweisungen verhindern.NoSchedule:
Auf einem getainteten Node werden keine neuen Pods geplant, es sei denn, sie haben eine passende Tolerierung. Bereits laufende Pods auf dem Node werden nicht entfernt.PreferNoSchedule:
Dieses Taint ist eine „Präferenz“ oder eine „weiche“ Version vonNoSchedule
. Die Steuerungsebene (Control Plane) versucht, einen Pod, der das Taint nicht toleriert, nicht auf diesem Node zu platzieren – aber das ist nicht garantiert.
Um eine Tolerierung (toleration) für ein bestimmtes Taint in einem Pod anzugeben, kannst du folgenden Inhalt unter die Pod-Spec-Datei hinzufügen:
tolerations: - key: "example-key" operator: "Exists" effect: "NoSchedule"
Der Standardwert für den operator
ist Equal
. Eine Tolerierung „passt“ zu einem Taint, wenn die Schlüssel gleich sind und die Effekte gleich sind, und:
- der
operator
Exists
ist (in diesem Fall darf kein Wert angegeben werden), oder - der
operator
Equal
ist und die Werte übereinstimmen.
Lass uns eine praktische Demo durchführen, um das besser zu verstehen.
- Erstelle zwei Pod-Manifest-Dateien namens
pod-with-toleration.yaml
undpod-no-toleration.yaml
mit folgendem Inhalt.
pod-no-toleration.yaml
apiVersion: v1 kind: Pod metadata: name: pod-without-toleration spec: containers: - name: nginx-container image: nginx
pod-with-toleration.yaml
apiVersion: v1 kind: Pod metadata: name: pod-with-toleration spec: containers: - image: nginx name: nginx-container tolerations: - key: size operator: Equal value: small effect: NoSchedule
- Erstelle anschließend die Pods und prüfe, auf welchem Node die jeweiligen Pods geplant wurden.
Du wirst sehen, dass der pod-with-toleration Pod auf node01 geplant wurde, da er die Taint auf node01 tolerieren konnte. Der pod-no-toleration Pod hingegen wurde auf dem controlplane-Node geplant, da dort keine Taint vorhanden war.
Es ist wichtig zu beachten, dass der Pod mit einer Toleration sowohl auf dem controlplane als auch auf node01 geplant werden kann. Der Pod ohne Toleration kann jedoch nur auf dem controlplane geplant werden. Daher bieten Taints und Tolerations keine Garantie dafür, dass ein bestimmter Pod auf einem bestimmten Node geplant wird.
- Node Selector: Die einfachste empfohlene Form der Knotenauswahl-Beschränkung ist
nodeSelector
. Du kannst das FeldnodeSelector
in deiner Pod-Spezifikation hinzufügen und die Node-Labels angeben, die der Zielnode haben soll.
apiVersion: v1 kind: Pod metadata: name: nginx-pod spec: containers: - name: nginx-container image: nginx nodeSelector: env: prod # assign pod to a node with label 'env=prod'
- Node Affinity: Stell dir vor, du hast eine Reihe verschiedener Nodes in deinem Kubernetes-Cluster sowie verschiedene Pods. Die Nodes wurden mit Labels wie
size=small
,size=medium
undsize=large
versehen – diese zeigen an, zu welcher Größenkategorie ein bestimmter Node gehört. Was ist, wenn du deinen Pod nur auf Nodes vom Typsize=medium
odersize=large
ausführen möchtest? Beachte, dass die Komplexität bei der Planung eines Pods auf einem Node hier zunimmt. Du gibst nämlich mehrere Möglichkeiten an, auf denen der Pod laufen darf. Kannst du das mit Taints und Tolerations umsetzen? Überlege mal. Die Antwort lautet: Nein, das kannst du nicht. Und genau dafür kommt in solchen komplexeren Fällen Node Affinity zum Einsatz. Es bietet dir eine ausdrucksstärkere Möglichkeit, Auswahlkriterien für Nodes festzulegen. Du kannst Regeln mit verschiedenen Operatoren definieren (zum Beispiel:In
,NotIn
,Exists
,DoesNotExist
), um damit komplexere Bedingungen auszudrücken.
Node Affinity ist konzeptionell ähnlich wienodeSelector
. Es erlaubt dir, festzulegen, auf welchen Nodes dein Pod gestartet werden darf – basierend auf den Labels der Nodes. Es gibt zwei Typen von Node Affinity:- requiredDuringSchedulingIgnoredDuringExecution: Der Scheduler darf deinen Pod nur planen, wenn die angegebene Regel erfüllt wird. Das funktioniert ähnlich wie nodeSelector, aber mit einer ausdrucksstärkeren Syntax.
- preferredDuringSchedulingIgnoredDuringExecution: Der Scheduler versucht, einen Node zu finden, der die Bedingung erfüllt. Falls kein passender Node gefunden wird, wird der Pod trotzdem auf einem anderen Node geplant.
Schauen wir uns mal an, wie du das Ganze umsetzen kannst:
- Jetzt erstelle eine Pod-Definitionsdatei, die intern
nodeAffinity
verwendet und den Pod auf node01 scheduled.
apiVersion: v1 kind: Pod metadata: name: my-pod spec: containers: - name: nginx-container image: nginx affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: type operator: In values: - worker
Damit wird dermy-pod
auf dem Knoten ausgeführt, der das Labeltype=worker
enthält. Stell dir vor, du hast mehrere Knoten mit den Labelstype=worker
undtype=laser
. Wenn du deinen Pod auf einem dieser beiden Knoten planen möchtest, kannst du einfach weitere Werte für values in deiner Pod-Spezifikation hinzufügen. Du kannst außerdem verschiedene Operatoren verwenden. Um mehr über die unterschiedlichen Operatoren zu erfahren, lies bitte die Kubernetes-Dokumentation.
Nun, da du bis hierher gelesen hast, denke über folgende Frage nach:
Stell dir vor, es gibt 5 verschiedene Nodes und 5 verschiedene Pods. Wie würdest du sicherstellen, dass der erste Pod (Pod-01) auf node01 geplant wird, der zweite Pod (Pod-02) auf node02 – und so weiter?