Kubernetes: Warum ist die Konfiguration des Ressourcenmanagements so wichtig?

In der Regel besteht immer die Notwendigkeit, einem bestimmten Anwendung einen dedizierten Pool von Ressourcen zur Verfügung zu stellen, um deren korrekte und stabile Funktion zu gewährleisten. Aber was passiert, wenn mehrere Anwendungen auf denselben Ressourcen laufen? Wie stellt man sicher, dass jede von ihnen mit den minimal notwendigen Ressourcen ausgestattet ist? Wie kann man den Ressourcenverbrauch einschränken? Wie verteilt man die Last sinnvoll zwischen den Knoten? Wie gewährleistet man die Funktionsfähigkeit des horizontalen Skalierungssystems bei steigender Last auf den Anwendungen?

Kubernetes: Warum ist die Konfiguration des Ressourcenmanagements so wichtig?

Beginnen wir mit den grundlegenden Ressourcentypen im System — hierzu zählen natürlich die CPU-Zeit und der Arbeitsspeicher. In den k8s-Mannifests werden diese Ressourcentypen in folgenden Einheiten gemessen:

  • CPU — in Kernen
  • RAM — in Bytes

Für jede Ressource gibt es zudem die Möglichkeit, zwei Arten von Anforderungen zu definieren — requests und Limits. Requests — beschreibt die minimalen Anforderungen an die verfügbaren Ressourcen des Knotens für den Start des Containers (und des Pods insgesamt), während Limits eine feste Obergrenze für die verfügbare Ressourcenmenge des Containers festlegt.

Es ist wichtig zu verstehen, dass im Manifest nicht unbedingt beide Typen eindeutig definiert werden müssen; das Verhalten wird wie folgt sein:

  • Wenn nur die Limits der Ressource explizit angegeben sind, nimmt die Requests für diese Ressource automatisch den Wert der Limits an (dies kann überprüft werden, indem die Entität beschreiben wird). Das heißt, die tatsächliche Nutzung des Containers wird durch die Menge der Ressourcen begrenzt, die er für seinen Start benötigt.
  • Wenn für die Ressource nur die Requests explizit angegeben sind, werden keine oberen Begrenzungen für diese Ressource festgelegt – das heißt, der Container ist nur durch die Ressourcen des Knotens begrenzt.

Es besteht auch die Möglichkeit, das Ressourcenmanagement nicht nur auf der Ebene eines bestimmten Containers, sondern auch auf der Ebene des Namespace mit Hilfe der folgenden Entitäten zu konfigurieren:

  • LimitRange – beschreibt die Begrenzungspolitik auf der Container-/Pod-Ebene im Namespace und ist notwendig, um die Standardgrenzen für Container/Pods sowie zur Verhinderung der Erstellung übermäßig großer Container/Pods (oder umgekehrt) zu beschreiben, die Anzahl zu begrenzen und mögliche Unterschiede in den Werten von Limits und Requests zu definieren.
  • ResourceQuotas — beschreibt die Einschränkungspolitik für alle Container im Namespace und dient in der Regel der Ressourcenzuweisung innerhalb von Umgebungen (nützlich, wenn die Umgebungen auf Knotenebene nicht strikt getrennt sind).

Nachfolgend sind Beispiele für Manifestdateien aufgeführt, in denen Ressourcengrenzen festgelegt werden:

  • Auf der Ebene eines bestimmten Containers:

    containers:
    - name: app-nginx
      image: nginx
      resources:
        requests:
          memory: 1Gi
        limits:
          cpu: 200m

    Das bedeutet, dass beim Starten eines Containers mit nginx mindestens 1 GB RAM und 0,2 CPU-Kapazität auf dem Knoten verfügbar sein müssen; der Container kann maximal 0,2 CPU und den gesamten verfügbaren RAM auf dem Knoten nutzen.

  • Auf der Ebene des gesamten Namespace:

    apiVersion: v1
    kind: ResourceQuota
    metadata:
      name: nxs-test
    spec:
      hard:
        requests.cpu: 300m
        requests.memory: 1Gi
        limits.cpu: 700m
        limits.memory: 2Gi

    Das heißt, die Summe aller Anforderungen der Container im Standard-Namespace darf 300m für CPU und 1G für RAM nicht überschreiten, die Summe aller Limits beträgt 700m für CPU und 2G für RAM.

  • Standardbeschränkungen für Container im Namespace:

    apiVersion: v1
    kind: LimitRange
    metadata:
      name: nxs-limit-per-container
    spec:
     limits:
       - type: Container
         defaultRequest:
           cpu: 100m
           memory: 1Gi
         default:
           cpu: 1
           memory: 2Gi
         min:
           cpu: 50m
           memory: 500Mi
         max:
           cpu: 2
           memory: 4Gi

    Das bedeutet, dass im Standard-Namespace für alle Container standardmäßig eine Anforderung von 100m für die CPU und 1G für den RAM sowie ein Limit von 1 CPU und 2G festgelegt werden. Zudem gibt es Beschränkungen für mögliche Werte in den Anforderungs- und Grenzwerten für CPU (50m < x < 2) und RAM (500M < x < 4G).

  • Einschränkungen auf Pod-Ebene ns:

    apiVersion: v1
    kind: LimitRange
    metadata:
     name: nxs-limit-pod
    spec:
     limits:
     - type: Pod
       max:
         cpu: 4
         memory: 1Gi

    Das bedeutet, dass für jeden Pod im Standard-Namespace eine Einschränkung von 4 vCPU und 1G festgelegt wird.

Nun möchte ich erläutern, welche Vorteile die Festlegung dieser Grenzen für uns haben kann.

Der Mechanismus zur Lastverteilung zwischen den Knoten

Wie bekannt ist, ist für die Verteilung der Pods auf die Knoten ein K8s-Komponent zuständig, der Scheduler, der nach einem bestimmten Algorithmus arbeitet. Dieser Algorithmus durchläuft bei der Auswahl des optimalen Knotens zur Ausführung zwei Phasen:

  1. Filterung
  2. Rankings

Das bedeutet, dass gemäß der beschriebenen Politik zunächst Knoten ausgewählt werden, auf denen die Ausführung des Pods möglich ist, basierend auf einem Satz von Prädikaten (darunter wird auch geprüft, ob der Knoten genügend Ressourcen für die Ausführung des Pods hat — PodFitsResources), und dann werden für jeden dieser Knoten gemäß den Prioritäten Es werden Punkte vergeben (unter anderem, je mehr freie Ressourcen ein Knoten hat, desto mehr Punkte werden ihm zugewiesen – LeastResourceAllocation/LeastRequestedPriority/BalancedResourceAllocation) und erfolgt der Start auf dem Knoten mit der höchsten Punktzahl (sofern mehrere Knoten diese Bedingung erfüllen, wird einer von ihnen zufällig ausgewählt).

Es ist wichtig zu verstehen, dass der Scheduler bei der Bewertung der verfügbaren Ressourcen des Knotens auf die in etcd gespeicherten Daten zurückgreift – d.h. auf die Summe von requested/limit der Ressourcen jedes Pods, der auf diesem Knoten läuft, jedoch nicht auf den tatsächlichen Ressourcenverbrauch. Diese Informationen können durch den Befehl abgerufen werden: kubectl describe node $NODE, zum Beispiel:

# kubectl describe nodes nxs-k8s-s1
..
Non-terminated Pods:         (9 in total)
  Namespace                  Name                                         CPU Requests  CPU Limits  Memory Requests  Memory Limits  AGE
  ---------                  ----                                         ------------  ----------  ---------------  -------------  ---
  ingress-nginx              nginx-ingress-controller-754b85bf44-qkt2t    0 (0%)        0 (0%)      0 (0%)           0 (0%)         233d
  kube-system                kube-flannel-26bl4                           150m (0%)     300m (1%)   64M (0%)         500M (1%)      233d
  kube-system                kube-proxy-exporter-cb629                    0 (0%)        0 (0%)      0 (0%)           0 (0%)         233d
  kube-system                kube-proxy-x9fsc                             0 (0%)        0 (0%)      0 (0%)           0 (0%)         233d
  kube-system                nginx-proxy-k8s-worker-s1                    25m (0%)      300m (1%)   32M (0%)         512M (1%)      233d
  nxs-monitoring             alertmanager-main-1                          100m (0%)     100m (0%)   425Mi (1%)       25Mi (0%)      233d
  nxs-logging                filebeat-lmsmp                               100m (0%)     0 (0%)      100Mi (0%)       200Mi (0%)     233d
  nxs-monitoring             node-exporter-v4gdq                          112m (0%)     122m (0%)   200Mi (0%)       220Mi (0%)     233d
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests           Limits
  --------           --------           ------
  cpu                487m (3%)          822m (5%)
  memory             15856217600 (2%)  749976320 (3%)
  ephemeral-storage  0 (0%)             0 (0%)

Hier sehen wir alle Pods, die auf einem bestimmten Knoten ausgeführt werden, sowie die Ressourcen, die jeder Pod anfordert. So sehen die Logs des Schedulers beim Starten des Pods cronjob-cron-events-1573793820-xt6q9 aus (diese Informationen erscheinen im Log des Schedulers, wenn das Logging-Niveau auf 10 in den Startparametern gesetzt wird –v=10):

Log

I1115 07:57:21.637791       1 scheduling_queue.go:908] Versuch, Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 zu planen                                                                                                                                           
I1115 07:57:21.637804       1 scheduler.go:453] Versuch, Pod zu planen: nxs-stage/cronjob-cron-events-1573793820-xt6q9                                                                                                                                                    
I1115 07:57:21.638285       1 predicates.go:829] Planung des Pods nxs-stage/cronjob-cron-events-1573793820-xt6q9 auf Node nxs-k8s-s5 ist erlaubt, Node läuft nur 16 von 110 Pods.                                                                               
I1115 07:57:21.638300       1 predicates.go:829] Planung des Pods nxs-stage/cronjob-cron-events-1573793820-xt6q9 auf Node nxs-k8s-s6 ist erlaubt, Node läuft nur 20 von 110 Pods.                                                                               
I1115 07:57:21.638322       1 predicates.go:829] Planung des Pods nxs-stage/cronjob-cron-events-1573793820-xt6q9 auf Node nxs-k8s-s3 ist erlaubt, Node läuft nur 20 von 110 Pods.                                                                               
I1115 07:57:21.638322       1 predicates.go:829] Planung des Pods nxs-stage/cronjob-cron-events-1573793820-xt6q9 auf Node nxs-k8s-s4 ist erlaubt, Node läuft nur 17 von 110 Pods.                                                                               
I1115 07:57:21.638334       1 predicates.go:829] Planung des Pods nxs-stage/cronjob-cron-events-1573793820-xt6q9 auf Node nxs-k8s-s10 ist erlaubt, Node läuft nur 16 von 110 Pods.                                                                              
I1115 07:57:21.638365       1 predicates.go:829] Planung des Pods nxs-stage/cronjob-cron-events-1573793820-xt6q9 auf Node nxs-k8s-s12 ist erlaubt, Node läuft nur 9 von 110 Pods.                                                                               
I1115 07:57:21.638334       1 predicates.go:829] Planung des Pods nxs-stage/cronjob-cron-events-1573793820-xt6q9 auf Node nxs-k8s-s11 ist erlaubt, Node läuft nur 11 von 110 Pods.                                                                              
I1115 07:57:21.638385       1 predicates.go:829] Planung des Pods nxs-stage/cronjob-cron-events-1573793820-xt6q9 auf Node nxs-k8s-s1 ist erlaubt, Node läuft nur 19 von 110 Pods.                                                                               
I1115 07:57:21.638402       1 predicates.go:829] Planung des Pods nxs-stage/cronjob-cron-events-1573793820-xt6q9 auf Node nxs-k8s-s2 ist erlaubt, Node läuft nur 21 von 110 Pods.                                                                               
I1115 07:57:21.638383       1 predicates.go:829] Planung des Pods nxs-stage/cronjob-cron-events-1573793820-xt6q9 auf Node nxs-k8s-s9 ist erlaubt, Node läuft nur 16 von 110 Pods.                                                                               
I1115 07:57:21.638335       1 predicates.go:829] Planung des Pods nxs-stage/cronjob-cron-events-1573793820-xt6q9 auf Node nxs-k8s-s8 ist erlaubt, Node läuft nur 18 von 110 Pods.                                                                               
I1115 07:57:21.638408       1 predicates.go:829] Planung des Pods nxs-stage/cronjob-cron-events-1573793820-xt6q9 auf Node nxs-k8s-s13 ist erlaubt, Node läuft nur 8 von 110 Pods.                                                                               
I1115 07:57:21.638478       1 predicates.go:1369] Planung des Pods nxs-stage/cronjob-cron-events-1573793820-xt6q9 auf Node nxs-k8s-s10 ist erlaubt, die Antipod-affinitätsbedingungen sind erfüllt.                                                                         
I1115 07:57:21.638505       1 predicates.go:1369] Planung des Pods nxs-stage/cronjob-cron-events-1573793820-xt6q9 auf Node nxs-k8s-s8 ist erlaubt, die Antipod-affinitätsbedingungen sind erfüllt.                                                                          
I1115 07:57:21.638577       1 predicates.go:1369] Planung des Pods nxs-stage/cronjob-cron-events-1573793820-xt6q9 auf Node nxs-k8s-s9 ist erlaubt, die Antipod-affinitätsbedingungen sind erfüllt.                                                                          
I1115 07:57:21.638583       1 predicates.go:829] Planung des Pods nxs-stage/cronjob-cron-events-1573793820-xt6q9 auf Node nxs-k8s-s7 ist erlaubt, Node läuft nur 25 von 110 Pods.                                                                               
I1115 07:57:21.638932       1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: BalancedResourceAllocation, Kapazität 39900 millicores 66620178432 Speicherbytes, Gesamtanforderung 2343 millicores 9640186880 Speicherbytes, Punktestand 9        
I1115 07:57:21.638946       1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: LeastResourceAllocation, Kapazität 39900 millicores 66620178432 Speicherbytes, Gesamtanforderung 2343 millicores 9640186880 Speicherbytes, Punktestand 8           
I1115 07:57:21.638961       1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: BalancedResourceAllocation, Kapazität 39900 millicores 66620170240 Speicherbytes, Gesamtanforderung 4107 millicores 11307422720 Speicherbytes, Punktestand 9        
I1115 07:57:21.638971       1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: BalancedResourceAllocation, Kapazität 39900 millicores 66620178432 Speicherbytes, Gesamtanforderung 5847 millicores 24333637120 Speicherbytes, Punktestand 7        
I1115 07:57:21.638975       1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: LeastResourceAllocation, Kapazität 39900 millicores 66620170240 Speicherbytes, Gesamtanforderung 4107 millicores 11307422720 Speicherbytes, Punktestand 8           
I1115 07:57:21.638990       1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: LeastResourceAllocation, Kapazität 39900 millicores 66620178432 Speicherbytes, Gesamtanforderung 5847 millicores 24333637120 Speicherbytes, Punktestand 7           
I1115 07:57:21.639022       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s10: TaintTolerationPriority, Punktestand: (10)                                                                                                        
I1115 07:57:21.639030       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s8: TaintTolerationPriority, Punktestand: (10)                                                                                                         
I1115 07:57:21.639034       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s9: TaintTolerationPriority, Punktestand: (10)                                                                                                         
I1115 07:57:21.639041       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s10: NodeAffinityPriority, Punktestand: (0)                                                                                                            
I1115 07:57:21.639053       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s8: NodeAffinityPriority, Punktestand: (0)                                                                                                             
I1115 07:57:21.639059       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s9: NodeAffinityPriority, Punktestand: (0)                                                                                                             
I1115 07:57:21.639061       1 interpod_affinity.go:237] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: InterPodAffinityPriority, Punktestand: (0)                                                                                                                   
I1115 07:57:21.639063       1 selector_spreading.go:146] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: SelectorSpreadPriority, Punktestand: (10)                                                                                                                   
I1115 07:57:21.639073       1 interpod_affinity.go:237] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: InterPodAffinityPriority, Punktestand: (0)                                                                                                                    
I1115 07:57:21.639077       1 selector_spreading.go:146] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: SelectorSpreadPriority, Punktestand: (10)                                                                                                                    
I1115 07:57:21.639085       1 interpod_affinity.go:237] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: InterPodAffinityPriority, Punktestand: (0)                                                                                                                    
I1115 07:57:21.639088       1 selector_spreading.go:146] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: SelectorSpreadPriority, Punktestand: (10)                                                                                                                    
I1115 07:57:21.639103       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s10: SelectorSpreadPriority, Punktestand: (10)                                                                                                         
I1115 07:57:21.639109       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s8: SelectorSpreadPriority, Punktestand: (10)                                                                                                          
I1115 07:57:21.639114       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s9: SelectorSpreadPriority, Punktestand: (10)                                                                                                          
I1115 07:57:21.639127       1 generic_scheduler.go:781] Host nxs-k8s-s10 => Punktestand 100037                                                                                                                                                                            
I1115 07:57:21.639150       1 generic_scheduler.go:781] Host nxs-k8s-s8 => Punktestand 100034                                                                                                                                                                             
I1115 07:57:21.639154       1 generic_scheduler.go:781] Host nxs-k8s-s9 => Punktestand 100037                                                                                                                                                                             
I1115 07:57:21.639267       1 scheduler_binder.go:269] AssumePodVolumes für Pod "nxs-stage/cronjob-cron-events-1573793820-xt6q9", Node "nxs-k8s-s10"                                                                                                               
I1115 07:57:21.639286       1 scheduler_binder.go:279] AssumePodVolumes für Pod "nxs-stage/cronjob-cron-events-1573793820-xt6q9", Node "nxs-k8s-s10": Alle PVCs gebunden und nichts zu tun                                                                             
I1115 07:57:21.639333       1 factory.go:733] Versuch, cronjob-cron-events-1573793820-xt6q9 an nxs-k8s-s10 zu binden

Hier sehen wir, dass der Scheduler zunächst die Nodes filtert und eine Liste von drei Nodes erstellt, auf denen eine Ausführung möglich ist (nxs-k8s-s8, nxs-k8s-s9, nxs-k8s-s10). Anschließend werden die Punkte nach verschiedenen Parametern (einschließlich BalancedResourceAllocation, LeastResourceAllocation) für jede dieser Nodes gezählt, um den am besten geeigneten Knoten zu bestimmen. Letztendlich wird der Plan auf dem Knoten mit der höchsten Punktzahl erstellt (hier haben gleich zwei Nodes die gleiche Punktzahl von 100037, daher wird zufällig einer von ihnen ausgewählt — nxs-k8s-s10).

Fazit: Wenn auf einem Knoten Pods laufen, für die keine Einschränkungen festgelegt sind, entspricht das aus Sicht der Ressourcennutzung in K8s dem Zustand, als wären solche Pods auf diesem Knoten überhaupt nicht vorhanden. Daher kann es, wenn Sie zum Beispiel einen Pod mit einer ressourcenintensiven Anwendung (wie wowza) haben und für diesen keine Einschränkungen definiert sind, zu einer Situation kommen, in der dieser Pod tatsächlich alle Ressourcen des Knotens verbraucht hat, aber K8s diesen Knoten als nicht überlastet betrachtet. In der Folge erhält dieser Knoten die gleiche Punktzahl bei der Bewertung (insbesondere in Bezug auf die verfügbaren Ressourcen) wie ein Knoten, auf dem keine aktiven Pods laufen, was letztlich zu einer ungleichmäßigen Lastverteilung zwischen den Knoten führen kann.

Pod kündigen

Wie bekannt ist, wird jedem Pod eine der 3 QoS-Klassen zugewiesen:

  1. guaranteed — wird zugewiesen, wenn für jeden Container im Pod sowohl Memory- als auch CPU-Requests und -Limits festgelegt sind, wobei diese Werte übereinstimmen müssen
  2. burstable — wenn mindestens ein Container im Pod Requests und Limits hat, wobei der Request < Limit ist
  3. best effort — wenn kein Container im Pod hinsichtlich der Ressourcen eingeschränkt ist

Wenn auf einem Node Ressourcenmangel (Speicherplatz, RAM) festgestellt wird, beginnt der Kubelet, Pods nach einem bestimmten Algorithmus zu priorisieren und auszusiedeln, der die Priorität des Pods und seine QoS-Klasse berücksichtigt. Zum Beispiel, wenn es um RAM geht, werden basierend auf der QoS-Klasse Punkte nach folgendem Prinzip vergeben:

  • Guaranteed: -998
  • BestEffort: 1000
  • Burstable: min(max(2, 1000 - (1000 * memoryRequestBytes) / machineMemoryCapacityBytes), 999)

Das bedeutet, dass der Kubelet bei gleichrangiger Priorität zuerst die Pods mit der QoS-Klasse "Best Effort" aus dem Node ausgliedert.

Fazit: Wenn Sie die Wahrscheinlichkeit verringern möchten, dass ein benötigter Pod bei Ressourcenmangel vom Node entfernt wird, sollten Sie neben der Priorität auch dafür sorgen, dass request/limit gesetzt werden.

Der Mechanismus zur horizontalen Skalierung von Anwendungs-Pods (HPA)

Wenn die Aufgabe darin besteht, die Anzahl der Pods automatisch zu erhöhen oder zu verringern, abhängig von der Ressourcennutzung (systemisch - CPU/RAM oder benutzerspezifisch - rps), kann eine Entität wie HPA (Horizontal Pod Autoscaler) bei der Lösung helfen. Sein Algorithmus funktioniert wie folgt:

  1. Die aktuellen Werte der beobachteten Ressource (currentMetricValue) werden ermittelt.
  2. Die gewünschten Werte für die Ressource (desiredMetricValue) werden für Systemressourcen über eine Anfrage festgelegt.
  3. Es wird die aktuelle Anzahl von Replikaten (currentReplicas) bestimmt.
  4. Die gewünschte Anzahl der Replikate (desiredReplicas) wird nach folgender Formel berechnet:
    desiredReplicas = [ currentReplicas * ( currentMetricValue / desiredMetricValue )]

Es findet keine Skalierung statt, wenn der Faktor (currentMetricValue / desiredMetricValue) nahe bei 1 liegt (die zulässige Abweichung kann von uns selbst festgelegt werden, sie beträgt standardmäßig 0,1).

Betrachten wir die Funktionsweise von hpa am Beispiel der Anwendung app-test (beschrieben als Deployment), bei der die Anzahl der Replikate je nach CPU-Verbrauch geändert werden muss:

  • Manifest der Anwendung

    kind: Deployment
    apiVersion: apps/v1beta2
    metadata:
     name: app-test
    spec:
     selector:
       matchLabels:
         app: app-test
     replicas: 2
     template:
       metadata:
         labels:
           app: app-test
       spec:
         containers:
         - name: nginx
           image: registry.nixys.ru/generic-images/nginx
           imagePullPolicy: Always
           resources:
             requests:
               cpu: 60m
           ports:
           - name: http
             containerPort: 80
           - name: nginx-exporter
             image: nginx/nginx-prometheus-exporter
             resources:
               requests:
                 cpu: 30m
             ports:
             - name: nginx-exporter
               containerPort: 9113
             args:
             - -nginx.scrape-uri
             - http://127.0.0.1:80/nginx-status

    Das bedeutet, wir sehen, dass die Anwendung ursprünglich mit zwei Instanzen gestartet wird, von denen jede zwei Container für nginx und nginx-exporter enthält, für die jeweils festgelegt ist, requests für die CPU.

  • HPA-Manifest

    apiVersion: autoscaling/v2beta2
    kind: HorizontalPodAutoscaler
    metadata:
     name: app-test-hpa
    spec:
     maxReplicas: 10
     minReplicas: 2
     scaleTargetRef:
       apiVersion: extensions/v1beta1
       kind: Deployment
       name: app-test
     metrics:
     - type: Resource
       resource:
         name: cpu
         target:
           type: Utilization
           averageUtilization: 30

    Das bedeutet, wir haben eine HPA erstellt, die den Deployment app-test überwacht und die Anzahl der Pods mit der Anwendung basierend auf dem CPU-Wert reguliert (wir erwarten, dass der Pod 30 % der ihm zugewiesenen CPU benötigt), wobei die Anzahl der Replikate zwischen 2 und 10 liegt.

    Nun betrachten wir den Mechanismus der HPA, wenn wir eine Last auf einen der Pods anwenden:

     # kubectl top pod
     NAME                                                   CPU(cores)   MEMORY(bytes)
     app-test-78559f8f44-pgs58            101m         243Mi
     app-test-78559f8f44-cj4jz            4m           240Mi

Insgesamt haben wir Folgendes:

  • Wünschenswerter Wert (desiredMetricValue) — gemäß den HPA-Einstellungen beträgt dieser 30 %
  • Aktueller Wert (currentMetricValue) — zur Berechnung ermittelt der controller-manager den Durchschnittswert des Ressourcenverbrauchs in %, d.h., vereinfacht gesagt, er macht Folgendes:
    1. Er erhält die absoluten Werte der Metriken der Pods vom Metric-Server, d.h. 101m und 4m.
    2. Er berechnet den durchschnittlichen absoluten Wert, d.h. (101m + 4m) / 2 = 53m.
    3. Er erhält den Absolutwert für den gewünschten Ressourcenverbrauch (dabei werden die Requests aller Container summiert) 60m + 30m = 90m.
    4. Berechnet den durchschnittlichen Prozentsatz der CPU-Nutzung im Verhältnis zu den Pod-Requests, d.h. 53m / 90m * 100% = 59%.

Jetzt haben wir alles Notwendige, um zu bestimmen, ob die Anzahl der Replikate geändert werden muss; dazu berechnen wir den Faktor:

ratio = 59% / 30% = 1,96.

Das heißt, die Anzahl der Replikate sollte ungefähr verdoppelt werden und beträgt [2 * 1,96] = 4.

Fazit: Wie man sehen kann, ist eine Voraussetzung für das Funktionieren dieses Mechanismus, dass Anfragen für alle Container im beobachteten Pod vorhanden sind.

Mechanismus der horizontalen automatischen Skalierung von Knoten (Cluster Autoscaler).

Um die negativen Auswirkungen auf das System bei Lastspitzen zu minimieren, reicht die Einrichtung eines HPA möglicherweise nicht aus. Beispielsweise entscheidet der HPA-Controller-Manager gemäß den Einstellungen, dass die Anzahl der Replikate verdoppelt werden muss, jedoch sind auf den Knoten keine freien Ressourcen verfügbar, um so viele Pods zu starten (d.h. der Knoten kann die angeforderten Ressourcen der Pod-Requests nicht bereitstellen), und diese Pods wechseln in den Status Pending.

In diesem Fall, wenn der Anbieter über eine entsprechende IaaS/PaaS (z.B. GKE/GCE, AKS, EKS usw.) verfügt, kann uns ein solches Werkzeug wie Node Autoscalerhelfen. Er ermöglicht es, die maximale und minimale Anzahl von Knoten im Cluster festzulegen und die aktuelle Anzahl der Knoten automatisch zu regulieren (indem er über die API des Cloud-Anbieters Knoten anfordert oder entfernt), wenn ein Ressourcenmangel im Cluster festgestellt wird und die Pods nicht geplant werden können (im Status Pending sind).

Fazit: Für die Möglichkeit der automatischen Skalierung der Knoten müssen in den Containern der Pods Requests definiert werden, damit k8s die Auslastung der Knoten korrekt bewerten und entsprechend melden kann, dass für den Start eines weiteren Pods im Cluster nicht genügend Ressourcen vorhanden sind.

Fazit

Es ist zu beachten, dass die Festlegung von Ressourcenbeschränkungen für den Container keine zwingende Voraussetzung für den erfolgreichen Start der Anwendung ist, jedoch aus folgenden Gründen dennoch ratsam ist:

  1. Um eine genauere Arbeitsweise des Schedulers hinsichtlich der Lastverteilung zwischen den Knoten k8s zu ermöglichen
  2. Um die Wahrscheinlichkeit eines „Pod-Eviction“-Ereignisses zu reduzieren
  3. Um das horizontale Autoscaling der Anwendungs-Pods (HPA) zu ermöglichen
  4. Für das horizontale automatisierte Skalieren von Knoten (Cluster-Autoscaling) bei Cloud-Anbietern

Lesen Sie auch andere Artikel in unserem Blog:

Quelle: habr.com

Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen 🔥 Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen | ProHoster