Anwendungen skalieren und den Datenverkehr verteilen

KubernetesBeginner
Jetzt üben

Einführung

Services stellen Clients eine stabile Adresse für eine Gruppe von Pods bereit. Das ist besonders wertvoll, wenn sich die Nachfrage ändert: Ein Deployment kann Replikate hinzufügen oder entfernen, während die Identität des Services unverändert bleibt.

In diesem Lab gibt jedes Backend seinen eigenen Pod-Hostnamen zurück. Sie skalieren von zwei auf vier Replikate, senden unabhängige Anfragen über einen einzigen Service und sehen Antworten von mehreren Pods. Anschließend skalieren Sie zurück auf zwei Replikate und beobachten, wie sich sowohl das Deployment als auch der EndpointSlice an den neuen Sollzustand angleichen.

Dabei handelt es sich um manuelle horizontale Skalierung. Automatische Skalierung mit HPA hängt von Ressourcenanforderungen, Metriken und einer Steuerungsrichtlinie ab und sollte daher erst behandelt werden, wenn das Verhalten manueller Replikate vollständig verstanden wurde.

Eine beobachtbare replizierte Anwendung erstellen

Start der Umgebung: Dieses Lab startet einen vollständigen Kubernetes-Cluster für Sie. Die Konfiguration der Steuerungsebene, des Knotens und der Netzwerkkomponenten dauert normalerweise 2–3 Minuten. Bitte warten Sie geduldig, bis die Umgebung vollständig geladen ist, bevor Sie beginnen.

In diesem Schritt erstellen Sie zwei Backends, die zeigen, welcher Pod die jeweilige Anfrage verarbeitet hat. Dadurch wird die Verteilung des Service-Datenverkehrs sichtbar und bleibt nicht nur ein abstraktes Konzept.

Der erste Befehl verwendet cd, um in den vorbereiteten Arbeitsbereich zu wechseln. Der nächste Befehl nutzt ein Here-Dokument: cat <<'EOF' > hostname-web.yaml schreibt jede folgende Zeile bis zum abschließenden EOF in die YAML-Datei und ersetzt dabei deren bisherigen Inhalt.

cd /home/labex/project/scale-lab
cat <<'EOF' > hostname-web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hostname-web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hostname-web
  template:
    metadata:
      labels:
        app: hostname-web
    spec:
      containers:
        - name: web
          image: busybox:1.36
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c"]
          args:
            - mkdir -p /www; hostname > /www/index.html; exec httpd -f -p 8080 -h /www
          ports:
            - name: http
              containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: hostname-web
spec:
  selector:
    app: hostname-web
  ports:
    - name: http
      port: 80
      targetPort: http
EOF
kubectl apply -f hostname-web.yaml
kubectl rollout status deployment/hostname-web --timeout=60s
kubectl get pods -l app=hostname-web

Nach dem abschließenden EOF übergibt kubectl apply -f jedes Objekt aus der Datei an den API-Server. rollout status wartet bis zu 60 Sekunden, bis beide gewünschten Pods bereit sind, und -l app=hostname-web listet ausschließlich Pods mit diesem Label auf.

Innerhalb des Containerbefehls führen die Semikolons die Aktionen nacheinander aus: /www wird erstellt, der Hostname in index.html umgeleitet und anschließend mit exec der Webserver zum Hauptprozess des Containers gemacht. Die beiden Pod-Namen unterscheiden sich, daher gibt jedes Backend einen anderen Seitenwert zurück.

Die Ausgangs-Backends überprüfen

In diesem Schritt verknüpfen Sie die Replikatanzahl des Deployments mit der Anzahl der bereitstehenden Backends des Services.

Lesen Sie drei zusammengehörige Ansichten aus. get deployment zeigt gewünschte und bereite Replikate an; -l wählt die Anwendungspods aus und -o wide ergänzt Spalten für IP-Adresse und Node. Der abschließende Label-Selektor findet den für diesen Service erstellten EndpointSlice:

kubectl get deployment hostname-web
kubectl get pods -l app=hostname-web -o wide
kubectl get endpointslices -l kubernetes.io/service-name=hostname-web

Das Deployment zeigt 2/2, und der EndpointSlice enthält zwei Adressen. Senden Sie nun von einem einzigen, dauerhaft laufenden Client-Pod aus mehrere Anfragen an den Service.

kubectl run erstellt einen eigenständigen Pod, da --restart=Never gesetzt ist. Der Trenner -- beendet die Optionen von kubectl; sleep 3600 ist der Containerbefehl, der den Pod am Leben hält. Die for-Schleife verwendet seq 1 6, um sechs Durchläufe zu erzeugen, und kubectl exec POD -- COMMAND führt jedes Mal wget innerhalb des Clients aus:

kubectl run load-client --image=busybox:1.36 --image-pull-policy=IfNotPresent --restart=Never -- sleep 3600
kubectl wait --for=condition=Ready pod/load-client --timeout=30s
for i in $(seq 1 6); do kubectl exec load-client -- wget -qO- http://hostname-web; done

Jede Antwort ist ein Pod-Name. In einer kurzen Stichprobe sehen Sie möglicherweise einen oder beide Namen. Die Verteilung durch den Service garantiert keine strikt abwechselnde Reihenfolge.

Deklarativ hochskalieren

In diesem Schritt ändern Sie den gespeicherten Sollzustand von zwei auf vier Replikate. Durch die Bearbeitung des Manifests bleiben Datei und laufendes Objekt konsistent.

sed -i 's/old/new/' file ersetzt passenden Text direkt in einer Datei. Anschließend sucht grep -n nach replicas: und zeigt die Zeilennummer an. So können Sie die Änderung vor dem Anwenden schnell überprüfen:

cd /home/labex/project/scale-lab
sed -i 's/replicas: 2/replicas: 4/' hostname-web.yaml
grep -n 'replicas:' hostname-web.yaml
kubectl apply -f hostname-web.yaml
kubectl rollout status deployment/hostname-web --timeout=60s
kubectl get deployment hostname-web

Das Deployment sollte 4/4 anzeigen. Der Controller hat zwei zusätzliche Pods erstellt, weil der tatsächliche Zustand unter dem neuen Sollzustand lag.

Das Anwachsen der Service-Backends beobachten

In diesem Schritt überprüfen Sie, dass der unveränderte Service die neuen Pods automatisch erkennt.

Der EndpointSlice-Befehl verwendet JSONPath, weil die normale Tabellendarstellung möglicherweise Details abkürzt. range wiederholt die Vorlage für jeden Endpoint. Jede Wiederholung gibt die erste Adresse, das wörtliche Wort ready, den Bereitschaftswert und einen Zeilenumbruch aus. Der umgekehrte Schrägstrich setzt einen Shell-Befehl über mehrere Anzeigezeilen hinweg fort:

kubectl get pods -l app=hostname-web -o wide
kubectl get endpointslices -l kubernetes.io/service-name=hostname-web \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'

Nun gibt es vier bereite Adressen. Sie mussten den Service nicht bearbeiten: Sein Selektor passte weiterhin auf jeden bereiten Pod mit app=hostname-web.

Vergleichen Sie die stabile Service-IP mit der erweiterten Backend-Menge:

kubectl get service hostname-web -o wide

Die ClusterIP bleibt stabil, während sich die Mitgliedschaft des EndpointSlice ändert.

Beobachten, wie Anfragen mehrere Pods erreichen

In diesem Schritt senden Sie unabhängige HTTP-Anfragen über einen einzigen Service und fassen zusammen, welche Pod-Hostnamen antworten.

Zunächst entfernt rm -f eine alte Ergebnisdatei, falls sie vorhanden ist; mit -f bleibt auch eine nicht vorhandene Datei problemlos. Die Schleife sendet 20 Anfragen. >> hängt jeden Hostnamen an, anstatt die bisherigen Ergebnisse zu ersetzen. Abschließend übergibt die Pipe die sortierten Zeilen an uniq -c, das aufeinanderfolgende Duplikate zusammenfasst und jedem Hostnamen seine Anzahl voranstellt:

rm -f /tmp/hostname-responses.txt
for i in $(seq 1 20); do
  kubectl exec load-client -- wget -qO- http://hostname-web >> /tmp/hostname-responses.txt
done
sort /tmp/hostname-responses.txt | uniq -c

Sie sollten mehr als einen Hostnamen sehen. Die Anzahlen können unterschiedlich ausfallen, und nicht jeder kurze Durchlauf muss alle vier Backends erreichen. Das Routing eines Kubernetes-Services verteilt Verbindungen, garantiert jedoch weder eine vollkommen gleichmäßige noch eine bestimmte Reihenfolge.

Überprüfen Sie, wie viele eindeutige Backends Ihre Stichprobe erreicht hat. sort -u behält von jedem Hostnamen eine Kopie. Die Pipe übergibt diese Zeilen an wc -l, und wc -l zählt die Zeilen:

sort -u /tmp/hostname-responses.txt | wc -l

Ein Wert größer als eins ist ein direkter Beleg dafür, dass die stabile Service-Adresse Anfragen an mehrere Pods weitergeleitet hat.

Imperativ herunterskalieren

In diesem Schritt verwenden Sie kubectl scale, um die vier Replikate schnell wieder auf zwei zu reduzieren.

kubectl scale ändert die gewünschte Replikatanzahl des laufenden Deployments sofort. --replicas=2 gibt die neue Anzahl vor; die YAML-Datei wird dadurch nicht bearbeitet:

kubectl scale deployment/hostname-web --replicas=2
kubectl rollout status deployment/hostname-web --timeout=60s
kubectl get pods -l app=hostname-web

Kubernetes beendet zwei Pods und behält zwei bei. Warten Sie, bis sich auch die Backend-Menge des Services angeglichen hat.

Diese begrenzte Abfrageschleife versucht es höchstens 30-mal. Bei jedem Durchlauf wird die Anzahl der bereiten Endpoints in count gespeichert. jq filtert das JSON des EndpointSlice nach bereiten Endpoints und gibt die Länge des Arrays zurück. [ "$count" -eq 2 ] ist ein numerischer Shell-Test, && break beendet die Schleife bei Erfolg, und sleep 1 wartet vor dem nächsten Versuch eine Sekunde:

for i in $(seq 1 30); do
  count=$(kubectl get endpointslices -l kubernetes.io/service-name=hostname-web -o json | jq '[.items[].endpoints[] | select(.conditions.ready == true)] | length')
  [ "$count" -eq 2 ] && break
  sleep 1
done
echo "Ready backends: $count"

Das laufende Deployment fordert nun zwei Replikate an, die Datei enthält jedoch weiterhin vier. Dieser Unterschied ist für den nächsten Schritt beabsichtigt.

Das Manifest abgleichen und die Spuren des Controllers auswerten

In diesem Schritt bringen Sie das gespeicherte Manifest mit dem laufenden Zustand aus zwei Replikaten in Einklang und verknüpfen die Skalierungsaktionen mit den Nachweisen des Controllers.

Vergleichen Sie zunächst die beiden Sollzustände. grep -n zeigt die gespeicherte Zeile an; JSONPath extrahiert ausschließlich das laufende Feld .spec.replicas und fügt einen Zeilenumbruch hinzu:

grep -n 'replicas:' /home/labex/project/scale-lab/hostname-web.yaml
kubectl get deployment hostname-web -o jsonpath='Live replicas: {.spec.replicas}{"\n"}'

Ändern Sie die Datei von vier zurück auf zwei Replikate und wenden Sie sie an:

sed -i 's/replicas: 4/replicas: 2/' /home/labex/project/scale-lab/hostname-web.yaml
kubectl apply -f /home/labex/project/scale-lab/hostname-web.yaml

Da der laufende Zustand bereits zwei Replikate enthält, sollte dieser Apply-Vorgang keine weiteren Pods erstellen. Lesen Sie die Ereignisse des Deployments aus. Die Pipe übergibt die vollständige Ausgabe von describe an sed -n; /Events:/,$p bedeutet „ab der Zeile mit Events: bis zum Ende ausgeben“:

kubectl describe deployment hostname-web | sed -n '/Events:/,$p'

Achten Sie auf Meldungen vom Typ ScalingReplicaSet, die Entscheidungen zum Hoch- und Herunterskalieren anzeigen. Entfernen Sie abschließend den temporären Client. --ignore-not-found sorgt dafür, dass die Bereinigung auch dann erfolgreich ist, wenn der Pod bereits verschwunden ist:

kubectl delete pod load-client --ignore-not-found

Manuelle Skalierung ändert die gewünschte Replikatanzahl. Der Deployment-Controller erstellt oder beendet Pods, und der Service verfolgt automatisch die bereiten Mitglieder. Wenn das Manifest synchron gehalten wird, kann ein späteres kubectl apply nicht unerwartet eine alte Anzahl wiederherstellen.

Zusammenfassung

Sie haben ein Deployment manuell von zwei auf vier und anschließend wieder auf zwei Replikate skaliert. Sie haben beobachtet, wie der Controller die Pods anpasste, wie die EndpointSlice-Mitgliedschaft den bereiten Backends folgte, ohne dass der Service geändert werden musste, und wie eine einzige Service-Adresse unabhängige Anfragen nachweislich an mehrere Pods weiterleitete.

Das wichtigste mentale Modell lautet: Die Replikatanzahl beschreibt den Sollzustand, der Deployment-Controller gleicht die tatsächlichen Pods daran an, und der Service folgt den bereiten Backends mit passenden Labels. Deklarative Dateien sollten nach bewussten Änderungen am laufenden Zustand wieder synchronisiert werden, damit zukünftige Apply-Vorgänge vorhersehbar bleiben.