Kubernetes: dlaczego tak ważne jest skonfigurowanie zarządzania zasobami systemu?

Zazwyczaj konieczne jest zapewnienie dedykowanej puli zasobów dla konkretnej aplikacji, aby działała poprawnie i stabilnie. Co jednak, jeśli na tych samych zasobach działa kilka aplikacji? Jak zapewnić każdemu z nich minimalnie potrzebne zasoby? W jaki sposób można ograniczyć zużycie zasobów? Jak prawidłowo rozdzielić obciążenie pomiędzy węzłami? Jak zapewnić funkcjonowanie mechanizmu poziomego skalowania w przypadku wzrostu obciążenia aplikacji?

Kubernetes: dlaczego tak ważne jest skonfigurowanie zarządzania zasobami systemu?

Najpierw warto określić, jakie podstawowe typy zasobów istnieją w systemie — są to oczywiście czas procesora i pamięć operacyjna. W manifestach k8s te typy zasobów mierzone są w następujących jednostkach:

  • CPU — w rdzeniach
  • RAM — w bajtach

Każdy zasób może mieć określone dwa typy wymagań — requests i limits. Requests — opisuje minimalne wymagania dotyczące dostępnych zasobów węzła do uruchomienia kontenera (a w przypadku poda w ogóle), podczas gdy limits ustala sztywną górną granicę zasobów, które są dostępne dla kontenera.

Ważne jest, aby zrozumieć, że nie ma obowiązku określania obu typów w manifeście, a zachowanie będzie następujące:

  • Jeśli wyraźnie zdefiniowany jest tylko limit zasobów, to requests dla tego zasobu automatycznie przyjmuje wartość równą limits (można to sprawdzić, wywołując opis jednostki). Oznacza to, że faktycznie praca kontenera będzie ograniczona do tej samej ilości zasobów, które są wymagane do jego uruchomienia.
  • Jeśli dla zasobu wyraźnie zdefiniowany jest tylko requests, to nie są ustalane żadne górne ograniczenia dla tego zasobu — to znaczy, że kontener jest ograniczony tylko zasobami samego węzła.

Istnieje również możliwość skonfigurowania zarządzania zasobami nie tylko na poziomie konkretnego kontenera, ale także na poziomie namespace przy użyciu następujących jednostek:

  • LimitRange — opisuje politykę ograniczeń na poziomie kontenera/poda w ns i jest potrzebna do opisania domyślnych ograniczeń dla kontenera/poda, a także do zapobiegania tworzeniu zbyt obfitych kontenerów/podów (lub odwrotnie), ograniczania ich liczby oraz określania możliwej różnicy wartości w limits i requests.
  • ResourceQuotas — opisuje politykę ograniczeń dla wszystkich kontenerów w ns i jest zazwyczaj stosowany do podziału zasobów między środowiskami (przydatne, gdy środowiska nie są ściśle oddzielone na poziomie węzłów)

Poniżej znajdują się przykłady manifestów, w których ustalane są ograniczenia zasobów:

  • Na poziomie konkretnego kontenera:

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

    Tzn. w tym przypadku do uruchomienia kontenera z nginx potrzebne jest co najmniej 1G pamięci RAM i 0.2 CPU na węźle, przy czym maksymalnie kontener może zjeść 0.2 CPU i całą dostępną pamięć RAM na węźle.

  • Na poziomie całego ns:

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

    Tzn. suma wszystkich requestów kontenerów w domyślnym ns nie może przekraczać 300m dla CPU i 1G dla RAM, a suma wszystkich limitów — 700m dla CPU i 2G dla RAM.

  • Domyślne ograniczenia dla kontenerów w ns:

    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

    Tzn. w domyślnym namespace dla wszystkich kontenerów domyślnie zostanie ustawiony request na 100m dla CPU i 1G dla RAM, limit — 1 CPU i 2G. Ponadto również ustalone jest ograniczenie na możliwe wartości w request/limit dla CPU (50m < x < 2) i RAM (500M < x < 4G).

  • Ograniczenia na poziomie podów ns:

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

    Tzn. dla każdego podu w domyślnym ns ustalone zostanie ograniczenie na 4 vCPU i 1G.

Teraz chciałbym opowiedzieć o korzyściach, jakie mogą przynieść te ograniczenia.

Mechanizm równoważenia obciążenia między węzłami

Jak wiadomo, za rozmieszczanie podów na węzłach odpowiada komponent k8s, zwany scheduler, który działa według określonego algorytmu. Algorytm ten podczas wyboru optymalnego węzła do uruchomienia przechodzi przez dwa etapy:

  1. Filtrowanie
  2. Ranking

Tzn. zgodnie z opisaną polityką początkowo wybierane są węzły, na których możliwe jest uruchomienie poda na podstawie zestawu predicates (w tym sprawdzane jest, czy węzeł ma wystarczającą ilość zasobów do uruchomienia poda — PodFitsResources), a następnie dla każdego z tych węzłów zgodnie z priorities Punkty są przyznawane (w tym większa ilość wolnych zasobów w węźle — tym więcej punktów mu przypisywanych jest — LeastResourceAllocation/LeastRequestedPriority/BalancedResourceAllocation) i uruchamiane na węźle z największą liczbą punktów (jeśli kilka węzłów spełnia ten warunek, wybierany jest losowo z nich).

Jednak należy pamiętać, że scheduler, oceniając dostępne zasoby węzła, opiera się na danych, które są przechowywane w etcd — tzn. na sumie requested/limit zasobów każdego podu uruchomionego na tym węźle, a nie na faktycznym zużyciu zasobów. Te informacje można uzyskać z wyniku polecenia kubectl describe node $NODE, na przykład:

# 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%)

Tutaj widzimy wszystkie pody uruchomione na konkretnym węźle, a także zasoby, które są wymagane przez każdy z podów. Oto jak wyglądają logi scheduler'a podczas uruchamiania podu cronjob-cron-events-1573793820-xt6q9 (te informacje pojawią się w logu scheduler'a przy ustawieniu 10. poziomu logowania w argumentach polecenia uruchomienia —v=10):

log

I1115 07:57:21.637791       1 scheduling_queue.go:908] Zaczynam próby zaplanowania podu nxs-stage/cronjob-cron-events-1573793820-xt6q9                                                                                                                                           
I1115 07:57:21.637804       1 scheduler.go:453] Próbuję zaplanować pod: nxs-stage/cronjob-cron-events-1573793820-xt6q9                                                                                                                                                    
I1115 07:57:21.638285       1 predicates.go:829] Zaplanowanie podu nxs-stage/cronjob-cron-events-1573793820-xt6q9 na węźle nxs-k8s-s5 jest dozwolone, węzeł uruchamia tylko 16 z 110 podów.                                                                               
I1115 07:57:21.638300       1 predicates.go:829] Zaplanowanie podu nxs-stage/cronjob-cron-events-1573793820-xt6q9 na węźle nxs-k8s-s6 jest dozwolone, węzeł uruchamia tylko 20 z 110 podów.                                                                               
I1115 07:57:21.638322       1 predicates.go:829] Zaplanowanie podu nxs-stage/cronjob-cron-events-1573793820-xt6q9 na węźle nxs-k8s-s3 jest dozwolone, węzeł uruchamia tylko 20 z 110 podów.                                                                               
I1115 07:57:21.638322       1 predicates.go:829] Zaplanowanie podu nxs-stage/cronjob-cron-events-1573793820-xt6q9 na węźle nxs-k8s-s4 jest dozwolone, węzeł uruchamia tylko 17 z 110 podów.                                                                               
I1115 07:57:21.638334       1 predicates.go:829] Zaplanowanie podu nxs-stage/cronjob-cron-events-1573793820-xt6q9 na węźle nxs-k8s-s10 jest dozwolone, węzeł uruchamia tylko 16 z 110 podów.                                                                              
I1115 07:57:21.638365       1 predicates.go:829] Zaplanowanie podu nxs-stage/cronjob-cron-events-1573793820-xt6q9 na węźle nxs-k8s-s12 jest dozwolone, węzeł uruchamia tylko 9 z 110 podów.                                                                               
I1115 07:57:21.638334       1 predicates.go:829] Zaplanowanie podu nxs-stage/cronjob-cron-events-1573793820-xt6q9 na węźle nxs-k8s-s11 jest dozwolone, węzeł uruchamia tylko 11 z 110 podów.                                                                              
I1115 07:57:21.638385       1 predicates.go:829] Zaplanowanie podu nxs-stage/cronjob-cron-events-1573793820-xt6q9 na węźle nxs-k8s-s1 jest dozwolone, węzeł uruchamia tylko 19 z 110 podów.                                                                               
I1115 07:57:21.638402       1 predicates.go:829] Zaplanowanie podu nxs-stage/cronjob-cron-events-1573793820-xt6q9 na węźle nxs-k8s-s2 jest dozwolone, węzeł uruchamia tylko 21 z 110 podów.                                                                               
I1115 07:57:21.638383       1 predicates.go:829] Zaplanowanie podu nxs-stage/cronjob-cron-events-1573793820-xt6q9 na węźle nxs-k8s-s9 jest dozwolone, węzeł uruchamia tylko 16 z 110 podów.                                                                               
I1115 07:57:21.638335       1 predicates.go:829] Zaplanowanie podu nxs-stage/cronjob-cron-events-1573793820-xt6q9 na węźle nxs-k8s-s8 jest dozwolone, węzeł uruchamia tylko 18 z 110 podów.                                                                               
I1115 07:57:21.638408       1 predicates.go:829] Zaplanowanie podu nxs-stage/cronjob-cron-events-1573793820-xt6q9 na węźle nxs-k8s-s13 jest dozwolone, węzeł uruchamia tylko 8 z 110 podów.                                                                               
I1115 07:57:21.638478       1 predicates.go:1369] Zaplanowanie podu nxs-stage/cronjob-cron-events-1573793820-xt6q9 na węźle nxs-k8s-s10 jest dozwolone, spełnione warunki anty-afinity.                                                                         
I1115 07:57:21.638505       1 predicates.go:1369] Zaplanowanie podu nxs-stage/cronjob-cron-events-1573793820-xt6q9 na węźle nxs-k8s-s8 jest dozwolone, spełnione warunki anty-afinity.                                                                          
I1115 07:57:21.638577       1 predicates.go:1369] Zaplanowanie podu nxs-stage/cronjob-cron-events-1573793820-xt6q9 na węźle nxs-k8s-s9 jest dozwolone, spełnione warunki anty-afinity.                                                                          
I1115 07:57:21.638583       1 predicates.go:829] Zaplanowanie podu nxs-stage/cronjob-cron-events-1573793820-xt6q9 na węźle nxs-k8s-s7 jest dozwolone, węzeł uruchamia tylko 25 z 110 podów.                                                                               
I1115 07:57:21.638932       1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: BalancedResourceAllocation, pojemność 39900 milikerneli 66620178432 bajtów pamięci, całkowite żądanie 2343 milikerneli 9640186880 bajtów pamięci, wynik 9        
I1115 07:57:21.638946       1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: LeastResourceAllocation, pojemność 39900 milikerneli 66620178432 bajtów pamięci, całkowite żądanie 2343 milikerneli 9640186880 bajtów pamięci, wynik 8           
I1115 07:57:21.638961       1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: BalancedResourceAllocation, pojemność 39900 milikerneli 66620170240 bajtów pamięci, całkowite żądanie 4107 milikerneli 11307422720 bajtów pamięci, wynik 9        
I1115 07:57:21.638971       1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: BalancedResourceAllocation, pojemność 39900 milikerneli 66620178432 bajtów pamięci, całkowite żądanie 5847 milikerneli 24333637120 bajtów pamięci, wynik 7        
I1115 07:57:21.638975       1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: LeastResourceAllocation, pojemność 39900 milikerneli 66620170240 bajtów pamięci, całkowite żądanie 4107 milikerneli 11307422720 bajtów pamięci, wynik 8           
I1115 07:57:21.638990       1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: LeastResourceAllocation, pojemność 39900 milikerneli 66620178432 bajtów pamięci, całkowite żądanie 5847 milikerneli 24333637120 bajtów pamięci, wynik 7           
I1115 07:57:21.639022       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-nxs-stage -> nxs-k8s-s10: TaintTolerationPriority, Wynik: (10)                                                                                                        
I1115 07:57:21.639030       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-nxs-stage -> nxs-k8s-s8: TaintTolerationPriority, Wynik: (10)                                                                                                         
I1115 07:57:21.639034       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-nxs-stage -> nxs-k8s-s9: TaintTolerationPriority, Wynik: (10)                                                                                                         
I1115 07:57:21.639041       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-nxs-stage -> nxs-k8s-s10: NodeAffinityPriority, Wynik: (0)                                                                                                            
I1115 07:57:21.639053       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-nxs-stage -> nxs-k8s-s8: NodeAffinityPriority, Wynik: (0)                                                                                                             
I1115 07:57:21.639059       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-nxs-stage -> nxs-k8s-s9: NodeAffinityPriority, Wynik: (0)                                                                                                             
I1115 07:57:21.639061       1 interpod_affinity.go:237] cronjob-cron-events-1573793820 -> nxs-k8s-s10: InterPodAffinityPriority, Wynik: (0)                                                                                                                   
I1115 07:57:21.639063       1 selector_spreading.go:146] cronjob-cron-events-1573793820 -> nxs-k8s-s10: SelectorSpreadPriority, Wynik: (10)                                                                                                                   
I1115 07:57:21.639073       1 interpod_affinity.go:237] cronjob-cron-events-1573793820 -> nxs-k8s-s8: InterPodAffinityPriority, Wynik: (0)                                                                                                                    
I1115 07:57:21.639077       1 selector_spreading.go:146] cronjob-cron-events-1573793820 -> nxs-k8s-s8: SelectorSpreadPriority, Wynik: (10)                                                                                                                    
I1115 07:57:21.639085       1 interpod_affinity.go:237] cronjob-cron-events-1573793820 -> nxs-k8s-s9: InterPodAffinityPriority, Wynik: (0)                                                                                                                    
I1115 07:57:21.639088       1 selector_spreading.go:146] cronjob-cron-events-1573793820 -> nxs-k8s-s9: SelectorSpreadPriority, Wynik: (10)                                                                                                                    
I1115 07:57:21.639103       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-nxs-stage -> nxs-k8s-s10: SelectorSpreadPriority, Wynik: (10)                                                                                                         
I1115 07:57:21.639109       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-nxs-stage -> nxs-k8s-s8: SelectorSpreadPriority, Wynik: (10)                                                                                                          
I1115 07:57:21.639114       1 generic_scheduler.go:726] cronjob-cron-events-1573793820-nxs-stage -> nxs-k8s-s9: SelectorSpreadPriority, Wynik: (10)                                                                                                          
I1115 07:57:21.639127       1 generic_scheduler.go:781] Host nxs-k8s-s10 -> Wynik 100037                                                                                                                                                                            
I1115 07:57:21.639150       1 generic_scheduler.go:781] Host nxs-k8s-s8 -> Wynik 100034                                                                                                                                                                             
I1115 07:57:21.639154       1 generic_scheduler.go:781] Host nxs-k8s-s9 -> Wynik 100037                                                                                                                                                                             
I1115 07:57:21.639267       1 scheduler_binder.go:269] AssumePodVolumes dla podu "nxs-stage/cronjob-cron-events-1573793820-xt6q9", węzeł "nxs-k8s-s10"                                                                                                               
I1115 07:57:21.639286       1 scheduler_binder.go:279] AssumePodVolumes dla podu "nxs-stage/cronjob-cron-events-1573793820-xt6q9", węzeł "nxs-k8s-s10": wszystkie PVC są powiązane i nic do zrobienia                                                                             
I1115 07:57:21.639333       1 factory.go:733] Próbuję powiązać cronjob-cron-events-1573793820-xt6q9 z nxs-k8s-s10

Tutaj widzimy, że scheduler początkowo filtruje i formuje listę z 3 węzłów, na których możliwe jest uruchomienie (nxs-k8s-s8, nxs-k8s-s9, nxs-k8s-s10). Następnie dokonuje obliczenia punktów według kilku parametrów (w tym BalancedResourceAllocation, LeastResourceAllocation) dla każdego z tych węzłów w celu określenia najbardziej odpowiedniego węzła. Ostatecznie plan jest przypisany do węzła z największą liczbą punktów (tutaj od razu dwa węzły mają tę samą liczbę punktów 100037, dlatego wybierany jest losowo jeden z nich — nxs-k8s-s10).

Wnioski: Jeśli na węźle działają pody, dla których nie określono ograniczeń, to dla k8s (z punktu widzenia zużycia zasobów) będzie to równoznaczne z tym, jakby na tym węźle takie pody w ogóle nie istniały. Dlatego, jeśli macie, powiedzmy, pod z wymagającym procesem (na przykład, wowza) i dla niego nie określono ograniczeń, może wystąpić sytuacja, w której dany pod zjada wszystkie zasoby węzła, a mimo to dla k8s ten węzeł będzie uważany za niezapracowany i otrzyma taką samą liczbę punktów w rankingu (w punktach z oceną dostępnych zasobów), jak węzeł, na którym nie ma aktywnych podów, co ostatecznie może prowadzić do nierównomiernego rozkładu obciążenia między węzłami.

Wydalenie poda

Jak wiadomo — każdemu podowi przypisywana jest jedna z 3 klas QoS:

  1. guaranuted — jest przypisywana wtedy, gdy dla każdego kontenera w podzie dla pamięci i CPU określono request i limit, przy czym te wartości muszą być równe
  2. burstable — przynajmniej jeden kontener w podzie ma request i limit, przy tym request < limit
  3. best effort — gdy żaden kontener w podzie nie jest ograniczony pod względem zasobów

Przy tym, gdy na węźle występuje niedobór zasobów (dysku, pamięci), kubelet zaczyna oceniać i wydalać pody według określonego algorytmu, który uwzględnia priorytet poda i jego klasę QoS. Na przykład, jeśli chodzi o RAM, to na podstawie klasy QoS przyznawane są punkty według następującej zasady:

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

T.j. przy takim samym priorytecie kubelet w pierwszej kolejności wydali z węzła pody z klasą QoS best effort.

Wnioski: Jeśli chcesz zmniejszyć prawdopodobieństwo wydalenia potrzebnego poda z węzła w przypadku niedoboru zasobów na nim, to oprócz priorytetu należy również zadbać o określenie request/limit dla niego.

Mechanizm poziomego automatycznego skalowania aplikacji (HPA)

Kiedy pojawia się potrzeba automatycznego zwiększania i zmniejszania liczby podów w zależności od wykorzystania zasobów (systemowych – CPU/RAM lub użytkowych – rps), w jej rozwiązaniu może pomóc taka jednostka k8s jak HPA (Horizontal Pod Autoscaler). Algorytm działa następująco:

  1. Określane są aktualne wartości obserwowanego zasobu (currentMetricValue)
  2. Określane są pożądane wartości dla zasobu (desiredMetricValue), które dla zasobów systemowych ustawia się za pomocą request
  3. Określa się bieżącą liczbę replik (currentReplicas)
  4. Na podstawie następującego wzoru oblicza się pożądaną liczbę replik (desiredReplicas)
    desiredReplicas = [ currentReplicas * ( currentMetricValue / desiredMetricValue )]

Przy tym skalowanie nie nastąpi, gdy współczynnik (currentMetricValue / desiredMetricValue) jest bliski 1 (przy czym dopuszczalny błąd możemy ustawić sami, domyślnie wynosi 0,1).

Rozważmy działanie hpa na przykładzie aplikacji app-test (opisanej jako Deployment), w której należy zmieniać liczbę replik w zależności od zużycia CPU:

  • Manifest aplikacji

    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

    To znaczy, widzimy, że pod z aplikacją uruchamiany jest początkowo w dwóch egzemplarzach, z których każdy zawiera dwa kontenery nginx i nginx-exporter, dla których ustawiono requests dla CPU.

  • Manifest HPA

    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

    To znaczy, że stworzyliśmy hpa, który będzie monitorować Deployment app-test i regulować liczbę podów z aplikacją na podstawie wskaźnika CPU (oczekujemy, że pod powinien zużywać 30% z zarezerwowanego mu CPU), przy czym liczba replik znajduje się w przedziale 2-10.

    Teraz rozważmy mechanizm działania hpa, jeśli na jeden z podów zostanie nałożony ładunek:

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

W związku z tym mamy następujące:

  • Wartość docelowa (desiredMetricValue) — zgodnie z ustawieniami hpa wynosi 30%
  • Bieżąca wartość (currentMetricValue) — w celu obliczeń controller-manager oblicza średnie zużycie zasobów w %, tzn. w uproszczeniu robi to w następujący sposób:
    1. Pobiera absolutne wartości metryk poda z serwera metryk, tzn. 101m i 4m
    2. Oblicza średnią absolutną wartość, tzn. (101m + 4m) / 2 = 53m
    3. Pobiera absolutną wartość dla żądanego zużycia zasobów (w tym celu sumuje requesty wszystkich kontenerów) 60m + 30m = 90m
    4. Oblicza średni procent zużycia CPU w odniesieniu do requestu poda, tzn. 53m / 90m * 100% = 59%

Teraz mamy wszystko, co potrzebne, aby określić, czy należy zmienić liczbę replik, w tym celu obliczamy współczynnik:

ratio = 59% / 30% = 1.96

Tzn. liczba replik powinna zostać zwiększona o około 2 razy i wynosić [2 * 1.96] = 4.

Wynik: Jak można zauważyć, aby ten mechanizm działał, konieczne jest również posiadanie requestów dla wszystkich kontenerów w obserwowanym podzie.

Mechanizm poziomego automatycznego skalowania węzłów (Cluster Autoscaler)

Aby zniwelować negatywne skutki dla systemu w przypadku wzrostu obciążenia, samo posiadanie skonfigurowanego hpa może być niewystarczające. Na przykład, zgodnie z ustawieniami w hpa, manager kontrolera podejmuje decyzję, że liczba replik musi zostać podwojona, jednak na węzłach brakuje dostępnych zasobów do uruchomienia takiej liczby podów (tzn. węzeł nie może dostarczyć żądanych zasobów dla poda requests) i te pody przechodzą w stan Pending.

W takim przypadku, jeśli dostawca posiada odpowiedni IaaS/PaaS (np. GKE/GCE, AKS, EKS itd.), może nam pomóc takie narzędzie jak Node Autoscaler. Pozwala on ustawić maksymalną i minimalną liczbę węzłów w klastrze oraz automatycznie regulować bieżącą liczbę węzłów (poprzez wywołanie API dostawcy chmury w celu zamówienia/usunięcia węzła), gdy zauważa się brak zasobów w klastrze i pody nie mogą być zaplanowane (znajdują się w stanie Pending).

Wynik: Aby móc skorzystać z automatycznego skalowania węzłów, należy określić requesty w kontenerach podów, aby k8s mógł prawidłowo ocenić obciążenie węzłów i odpowiednio informować, że do uruchomienia kolejnego poda brakuje zasobów w klastrze.

Podsumowanie

Warto zauważyć, że ustawienie ograniczeń zasobów kontenera nie jest warunkiem koniecznym do pomyślnego uruchomienia aplikacji, jednak nadal lepiej to zrobić z następujących powodów:

  1. Aby zwiększyć dokładność działania harmonogramu w zakresie równoważenia obciążenia między węzłami k8s
  2. Aby zmniejszyć prawdopodobieństwo wystąpienia zdarzenia „wypierania poda”
  3. Aby zrealizować poziome autoskalowanie podów aplikacji (HPA)
  4. Aby zrealizować poziome autoskalowanie węzłów (Cluster Autoscaling) u dostawców chmurowych

Przeczytaj również inne artykuły na naszym blogu:

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster