Kennis Blogs Autoscaling op Kubernetes. Hoe werkt dat? - Deel 2

Autoscaling op Kubernetes. Hoe werkt dat? - Deel 2

In deel 1 van deze blog hebben wij autoscaling geconfigureerd voor een Kubernetes deployment via de metrics-API op basis van standaard CPU en memory metrics. In dit tweede deel kijken wij naar hoe je autoscaling kan configureren op basis van custom metrics. Dit doen wij door gebruik te maken van metrics vanuit Linkerd en ingress-nginx.

 

In het kort: waarom je custom metrics wil gebruiken voor autoscaling

Autoscaling kan ingezet worden om resources efficiënter in te zetten door gebruik te maken van minder VM's, zoals bijvoorbeeld tijdens piektijden. Als je handmatig op piektijden moet inspelen, dan ben je vaak te langzaam waardoor er niet optijd genoeg capaciteit is en applicaties langzamer zullen werken. Dit kan ervoor zorgen dat jouw applicaties onbeschikbaar of bruikbaar worden! Maar wanneer is autoscaling niet relevant? Autoscaling is niet relevant op het moment dat je een statisch, voorspelbaar applicatielandschap hebt, omdat het dan weinig toegevoegde waarde heeft. Je applicatie zal namelijk stabiel blijven. 

Het voordeel van custom metrics gebruiken is dat het een betrouwbaardere gedrag oplevert met autoscaling dan standaard metrics. Voor custom metrics kan je op basis van applicatiegedrag (bijv. latency en het aantal requests per second) je autoscaling inregelen. Dit is betrouwbaarder, omdat je hierop van tevoren kan testen. Zoals bijvoorbeeld bij een performance test, waar je erachter komt dat als je 120 rps tegelijkertijd uitvoert, je applicatie langzaam wordt.

 

Benodigdheden

  • Een Prometheus installatie binnen je cluster (Alleen bij ingress-nginx)
  • RBAC permissies om een nieuwe API Service te installeren
  • Linkerd of ingress-nginx

Zowel de voorbeelden van Linkerd als die van ingress-nginx zullen werken binnen nagenoeg elke Kubernetes distributie, zoals GKE, EKS, Kind, K3s of een AME Kubernetes-cluster

 

Autoscaling met Linkerd

Linkerd is een service mesh en CNCF-member-project. Linkerd kan je helpen om platform breed inzicht te krijgen in het gedrag van je applicaties via metrics zoals als succesratio en latentie zonder je code te hoeven wijzigen. Linkerd heeft nog vele andere use cases, zoals encryptie. Deze zullen wij voor het doeleinde van deze blog buiten scope laten.

Voor het eerste deel van autoscaling kijken we naar de metrics die uit de linker-proxy te halen zijn, om zo het aantal pods binnen onze deployment te schalen. Hiervoor gebruiken wij het voorbeeld deployment uit deel 1 van deze blog.

Om dit te laten werken, moet je Linkerd en de viz-plugin geïnstalleerd hebben.


linkerd install | kubectl apply -f - linkerd viz install | kubectl apply -f -


Zie de Linkerd Getting Started-documentatie voor gedetailleerde instructies over hoe Linkerd geïnstalleerd kan worden.

 

Installeren van de Prometheus Adapter

De Prometheus Adapter is een project dat de custom metrics API-Service van Kubernetes implementeert en verbindt aan een Prometheus-instantie.

 

Deze zullen wij via Helm installeren in ons cluster. Hiervoor zullen wij een nieuwe file aanmaken, waarmee wij enkele values van de Helm chart zullen overschrijven (deze noemen wij prometheus-Adapter.yaml):

prometheus:
url: http://prometheus.linkerd-viz.svc port: 9090 path: "" rules: custom: - seriesQuery: 'response_latency_ms_bucket{namespace!="",pod!=""}' resources: template: '<<.Resource>>' name: matches: '^(.*)_bucket$' as: "${1}_99th" metricsQuery: 'histogram_quantile(0.99, sum(irate(<<.Series>>{<<.LabelMatchers>>, direction="inbound", deployment!="", namespace!=""}[5m])) by (le, <<.GroupBy>>))'


Via deze instellingen configureren wij de Prometheus Adapter zodat die de Prometheus-installatie in je cluster kan vinden. Daarnaast worden er custom metric rules geconfigureerd. Dit is een mapping van een Prometheus query naar iets wat bruikbaar is door de Prometheus Adapter en custom metrics API service, zodat wij de metrics kunnen gebruiken in onze HorizontalPodAutoscaler (HPA) resource. Installeer de Prometheus Adapter via helm:

 

$ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
$ helm repo update
$ helm install -n monitoring  prometheus-adapter -f prometheus-adapter.yaml prometheus-community/prometheus-adapter
NAME: prometheus-adapter
LAST DEPLOYED: Sat Oct  9 16:37:20 2021
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

NOTES: prometheus-adapter has been deployed. In a few minutes you should be able to list metrics using the following command(s): kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1

 

Hierna valideer je of de Prometheus Adapter goed draait door de API-services op te vragen:

$ kubectl get apiservice v1beta1.custom.metrics.k8s.io
NAME                            SERVICE                      AVAILABLE                  AGE
v1beta1.custom.metrics.k8s.io   default/prometheus-adapter   False (MissingEndpoints)   24s


Tijdens de start-up zal de status MissingEndpoints blijven. Zodra de pod werkt, zal Available veranderen naar True. 

 

$ kubectl get apiservice v1beta1.custom.metrics.k8s.io
NAME                            SERVICE                      AVAILABLE   AGE
v1beta1.custom.metrics.k8s.io   default/prometheus-adapter   True        43s


Het is nu mogelijk om te valideren of je op maat gemaakte regels goed geïnstalleerd zijn door het volgende te gebruiken:

 

$ kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1
{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"custom.metrics.k8s.io/v1beta1","resources":[....]}


Voorbeeld

We kunnen nu custom metrics gebruiken in onze HPA-resources. Dit is onze voorbeeld-implementatie, genaamd scalingtest: 

 

apiVersion: v1
kind: Namespace
metadata:
  name: scalingtest
  annotations:
    linkerd.io/inject: enabled
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sampleapp
  namespace: scalingtest
spec:
  minReadySeconds: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: sampleapp
  template:
    metadata:
      labels:
        app: sampleapp
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: sampleapp
        image: nginx
        resources:
          requests:
            cpu: 100m
          limits:
            memory: "128Mi"
            cpu: "600m"
        ports:
        - containerPort: 80
          name: http
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 10"]
        startupProbe:
          httpGet:
            path: /
            port: http
          initialDelaySeconds: 5
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /
            port: http
          initialDelaySeconds: 5
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /
            port: http
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: sampleapp
  namespace: scalingtest
spec:
  selector:
    app: sampleapp
  ports:
  - port: 80
    targetPort: 80


We installeren deployment en service (example.yaml) in ons cluster:

$ kubectl apply -f example.yaml 
namespace/scalingtest created
deployment.apps/sampleapp created
service/sampleapp created

$ kubectl get pod
NAME                         READY   STATUS        RESTARTS   AGE
sampleapp-7db7fdcd9d-95czg   2/2     Running       0          4s

Belangrijk is dat de pod die aangemaakt is twee containers heeft (2/2). Dit is de sampleapp container met een linkerd proxy als sidecar.

Na het installeren van ons voorbeeld, voegen wij een load generator toe. Hiervoor gebruiken wij, net als in deel 1 van de blog, het slow_cooker project van Buoyant. Dit kunnen echter ook andere implementaties zijn:

 

kubectl -n scalingtest run load-generator --image=buoyantio/slow_cooker -- 
-qps 100 -concurrency 10 http://sampleapp

Ook hier genereren wij 100 requests per seconden tegen de voorbeeld deployment. Valideer via kubectl top pod dat het CPU-gebruik gestegen is:

 

$ kubectl top pod
NAME                        CPU(cores)   MEMORY(bytes)   
load-generator              128m         5Mi             
sampleapp-7db7fdcd9d-95czg  79m          2Mi         


Nu wij een deployment hebben geïnstalleerd en er requests uitkomen, kunnen wij beginnen met het configureren van een HPA policy:

 

kind: HorizontalPodAutoscaler
apiVersion: autoscaling/v2beta2
metadata:
  name: sampleapp
  namespace: scalingtest
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: sampleapp
  minReplicas: 1
  maxReplicas: 20
  metrics:
  # Scale based on request latency, Linkerd-proxy metric
  - type: Object
    object:
      metric:
        name: response_latency_ms_99th
      describedObject:
        apiVersion: apps/v1
        kind: Deployment
        name: sampleapp
      target:
        type: AverageValue
        averageValue: 1000000m # 1s

Enkele verschillen tussen een HPA op basis van CPU en de custom metrics zijn:

  • Je gebruikt als type Object in plaats van Resource.
  • Je configureert het object met welke metric je wilt selecteren. Response_latency_ms_99th is de naam van de metric die wij bij de Prometheus Adapter hebben geconfigureerd. Deze kan je via kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1 terug vinden.
  • Bij target met averageValue wordt de value gedeeld door 1000. Dit is gedaan in de API-implementatie tegen rounding-errors. De ingevulde waarde (1000000m), staat voor 1 seconde. Door gebruik te maken van AverageValue als type, wordt er een scale event gedaan zodra de value hiervan met meer dan 10% afwijkt.

Zodra de HPA is geïnstalleerd in je cluster, zal de HPA na ongeveer een minuut in werking treden. Dit voorbeeld zal werken voor elke applicatie die draait binnen de linkerd mesh.

In dit voorbeeld hebben wij de custom metrics geconfigureerd op basis van response latency. Je kan echter elke metric die linkerd beschikbaar stelt gebruiken.

In de documentatie van linkerd is een lijst met metrics te vinden. Let hierbij op het type metric. De query voor een histogram is anders dan een counter. Hier zijn nog enkele voorbeelden voor de request_total metric:

 

prometheus:
  url: http://prometheus.linkerd-viz.svc
  port: 9090
  path: ""

rules:
  custom:
    - seriesQuery: 'request_total{namespace!="",pod!=""}'
      resources:
        template: '<<.Resource>>'
      name:
        matches: '^(.*)$'
        as: "linkerd_${1}"
      metricsQuery: 'sum(rate(<<.Series>>{<<.LabelMatchers>>, direction="inbound", deployment!="", namespace!=""}[5m])) by ( <<.GroupBy>>)'


Via deze query wordt het request per second berekend via een rate query, gedurende een slot van 5 minuten. Dit wordt gedaan over de gehele deployment. De metric is hierna beschikbaar als

 

linkerd_request_total. Hiermee kan je autoscaling zo configureren dat je een pod per elke 10 requests opstart:

kind: HorizontalPodAutoscaler
apiVersion: autoscaling/v2beta2
metadata:
  name: sampleapp
  namespace: scalingtest
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: sampleapp
  minReplicas: 1
  maxReplicas: 20
  metrics:
  # Scale based on request latency, Linkerd-proxy metric
  - type: Object
    object:
      metric:
        name: linkerd_request_total
      describedObject:
        apiVersion: apps/v1
        kind: Deployment
        name: sampleapp
      target:
        type: AverageValue
        averageValue: 10000m # 10

Autoscaling door ingress-nginx te gebruiken

In plaats van Linkerd, kan je ook ander custom metrics gebruiken voor autoscaling. Een voorbeeld hiervan is ingress-nginx, in combinatie met de latency metrics voor een ingress resource. De set-up is bijna identiek; ook hier heb je een Prometheus Adapter nodig en moet je dit zodanig configureren dat het verbindt met een Prometheus.

Hiervoor is het van belang dat je al een Prometheus-installatie beschikbaar hebt binnen je cluster. Waarbij dat met Linkerd bij de installatie van de viz plugin wordt meegenomen, dien je dat hier zelf te doen.

De installatie van Prometheus is buiten scope, maar kan bijvoorbeeld worden geinstalleerd via:

 

Er zijn vele verschillende manieren om Prometheus te installeren. Indien je gebruikmaakt van een AME-Kubernetes cluster, zal Prometheus al beschikbaar zijn binnen je cluster. Deze is bereikbaar op de URL http://prometheus.monitoring.svc:9090.

 

Instellen van de Prometheus Adapter

De volgende queries kunnen gebruikt worden om de autoscaling te configureren op requests per seconden of op de 99ste precentile voor latency op ingress resource.

 

prometheus:
  url: http://prometheus.monitoring.svc
  port: 9090
  path: ""

rules:
  custom:
    - seriesQuery: '{__name__=~"^nginx_ingress_.*",namespace!=""}'
      seriesFilters: []
      resources:
        template: <<.Resource>>
        overrides:
          exported_namespace:
            resource: "namespace"
      name:
        matches: ""
        as: ""
      metricsQuery: sum(<<.Series>>{<<.LabelMatchers>>}) by (<<.GroupBy>>)

    - seriesQuery: '{__name__=~"^nginx_ingress_controller_requests.*",namespace!=""}'
      seriesFilters: []
      resources:
        template: <<.Resource>>
        overrides:
          exported_namespace:
            resource: "namespace"
      name:
        matches: ""
        as: "nginx_ingress_controller_requests_rate"
      metricsQuery: round(sum(rate(<<.Series>>{<<.LabelMatchers>>}[1m])) by (<<.GroupBy>>), 0.001)

    - seriesQuery: '{__name__=~"^nginx_ingress_controller_request_duration_seconds_bucket",namespace!=""}'
      seriesFilters: []
      resources:
        template: <<.Resource>>
        overrides:
          exported_namespace:
            resource: "namespace"
      name:
        matches: "^(.*)_bucket$"
        as: "${1}_99th"
      metricsQuery: histogram_quantile(0.99, round(sum(rate(<<.Series>>{<<.LabelMatchers>>}[1m])) by (le, <<.GroupBy>>), 0.001))


Bovenstaand stelt 2 metrics beschikbaar die bruikbaar zijn binnen de HorizontalPodAutoscaler resource;

 

  • nginx_ingress_controller_requests_rate - de requests per seconden voor een ingress resource
  • nginx_ingress_controller_request_duration_seconds_99th - de latency voor requests die binnen de 99ste percentile vallen voor een ingress resource

 

Dit kan net zoals het vorige voorbeeld (linkerd) worden geconfigureerd voor de Prometheus Adapter via Helm:

 

$ helm install -n monitoring prometheus-adapter -f prometheus-adapter.yaml prometheus-community/prometheus-adapter


Mocht je al een Prometheus Adapter installatie hebben draaien, kan je deze upgraden via:

 

$ helm upgrade -n monitoring  -i prometheus-adapter -f prometheus-adapter.yaml prometheus-community/prometheus-adapter


Wij kunnen nu een HPA resource configureren voor een deployment op basis van de latency van een ingress resource:

 

kind: HorizontalPodAutoscaler
apiVersion: autoscaling/v2beta2
metadata:
  name: sampleapp
  namespace: scalingtest
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: sampleapp
  minReplicas: 1
  maxReplicas: 20
  metrics:
  - type: Object
    object:
      metric:
        name: nginx_ingress_controller_request_duration_seconds_99th
      describedObject:
        apiVersion: networking.k8s.io/v1
        kind: ingress
        name: sampleapp
      target:
        type: AverageValue
        averageValue: 10m # 10ms


Custom Autoscaling behaviour

Het standaard gedrag voor autoscaling is goed genoeg voor de meeste standaard applicaties. Zodra je wat meer specifieke requirements hebt kan je dit overschrijven.

Hiervoor kan je behaviour gebruiken. Hierin zitten opties voor het scale down gedrag en het scale up gedrag.

 

kind: HorizontalPodAutoscaler
apiVersion: autoscaling/v2beta2
metadata:
  name: sampleapp
  namespace: scalingtest
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: sampleapp
  minReplicas: 1
  maxReplicas: 20
  metrics:
  - type: Object
    object:
      metric:
        name: nginx_ingress_controller_request_duration_seconds_99th
      describedObject:
        apiVersion: networking.k8s.io/v1
        kind: ingress
        name: sampleapp
      target:
        type: AverageValue
        averageValue: 10m # 10ms

  # configure scale up/down abehaviour
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 120
      policies:
      - type: Percent
        value: 25
        periodSeconds: 10
      - type: Pods
        value: 1
        periodSeconds: 5
      selectPolicy: Max
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 25
        periodSeconds: 30
      - type: Pods
        value: 1
        periodSeconds: 5
      selectPolicy: Max

Conclusie

In het tweede gedeelte van deze blog hebben we een voorbeeld voor autoscaling gegeven op basis van custom metrics vanuit Linkerd en ingress-nginx, door gebruik te maken van de Prometheus Adapter. Door gebruik te maken van custom metrics, is het mogelijk om veel efficienter extra capaciteit toe te voegen zodra er gebruikers-impact is.

Vanuit performance testen zou je bijvoorbeeld kunnen bepalen wat de ideale hoeveelheid requests per seconden per applicatie-instantie is. Je kan dit getal gebruiken als je target voor autoscaling.

Met custom metrics kan je ook verder gaan door de queries zo aan te passen dat je op basis van historische data kan voorspellen wanneer er peak traffic zal plaatsvinden, zodat je kan beginnen met het vergroten van capaciteit voordat je gebruikers hinder ondervinden van een capaciteit tekort.

Door de State-mindset van Cloud Provider Hosts kost het developers veel tijd en handwerk om softwarereleases te doen. Wij van Avisi Cloud vinden dat het uitrollen van software geautomatiseerd moet worden en dat vanuit de Change-mindset gehandeld moet worden. Daarom hebben wij Avisi Managed Environments gecreëerd, waarmee jij 20% extra ontwikkelcapaciteit kan behalen. Wil jij ook 20% extra ontwikkelcapaciteit? Neem dan contact met ons op.