Autoscaling des pods via des métriques personnalisées dans GKE

Découvrez comment rendre des métriques personnalisées disponibles dans le service Google Cloud Monitoring et faire en sorte que l'Horizontal Pod Autoscaler (HPA) utilise ces métriques pour la mise à l'échelle de nos pods.

Autoscaling des pods via des métriques personnalisées dans GKE

Aperçu du cas d'utilisation

L'exemple portera sur la mise à l'échelle des pods d'une application « Selenium Grid » en fonction de la taille de la « file d'attente des sessions ».

L'application « Selenium Grid » est essentiellement composée de deux composants :

  • hub : reçoit les demandes des clients et les transmet aux nœuds disponibles
  • nœuds : sont enregistrés auprès du Hub et traitent les demandes des clients transférées

Lorsque tous les pods « nœuds » sont occupés, les demandes des clients sont envoyées dans la « file d'attente des sessions ».

Notre objectif est de faire en sorte que l'HorizontalPodAutoscaler (HPA) ajoute plus de pods « nœuds » en fonction de la taille de la « file d'attente des sessions ».

Voici le manifeste Kubernetes utilisé pour déployer l'application « Selenium Grid », basé sur des exemples de manifestes disponibles ici : exemples-selenium-kubernetes.

# Selenium nodes

apiVersion: apps/v1
kind: Deployment
metadata:
  name: selenium-node-firefox
  namespace: default
  labels:
    app: selenium-node-firefox
spec:
  replicas: 1
  selector:
    matchLabels:
      app: selenium-node-firefox
  template:
    metadata:
      labels:
        app: selenium-node-firefox
    spec:
      volumes:
      - name: dshm
        emptyDir:
          medium: Memory
      containers:
      - name: selenium-node-firefox
        image: selenium/node-firefox:4.18.0-20240220
        volumeMounts:
          - mountPath: /dev/shm
            name: dshm
        env:
          - name: SE_EVENT_BUS_HOST
            value: "selenium-hub"
          - name: SE_EVENT_BUS_SUBSCRIBE_PORT
            value: "4443"
          - name: SE_EVENT_BUS_PUBLISH_PORT
            value: "4442"
          - name: NODE_MAX_INSTANCES
            value: "1"
          - name: NODE_MAX_SESSION
            value: "1"
        resources:
          limits:
            memory: "250Mi"
            cpu: "0.2"
---
# Selenium Hub

apiVersion: apps/v1
kind: Deployment
metadata:
  name: selenium-hub
  namespace: default
  labels:
    app: selenium-hub
spec:
  replicas: 1
  selector:
    matchLabels:
      app: selenium-hub
  template:
    metadata:
      labels:
        app: selenium-hub
    spec:
      containers:
      - name: selenium-hub
        image: selenium/hub:4.18.0-20240220
        ports:
          - containerPort: 4444
          - containerPort: 4443
          - containerPort: 4442
        resources:
          limits:
            memory: "200Mi"
            cpu: ".2"
        livenessProbe:
          httpGet:
            path: /wd/hub/status
            port: 4444
          initialDelaySeconds: 30
          timeoutSeconds: 5
        readinessProbe:
          httpGet:
            path: /wd/hub/status
            port: 4444
          initialDelaySeconds: 30
          timeoutSeconds: 5
---
# Selenium Hub service

apiVersion: v1
kind: Service
metadata:
  name: selenium-hub
  namespace: default
  labels:
    app: selenium-hub
spec:
  ports:
  - port: 4444
    targetPort: 4444
    name: port0
  - port: 4443
    targetPort: 4443
    name: port1
  - port: 4442
    targetPort: 4442
    name: port2
  selector:
    app: selenium-hub
  type: ClusterIP

Exposez les métriques de l'application via l'exporteur Prometheus

Nous devons d’abord nous assurer que les métriques dont nous avons besoin sont disponibles dans le format Prometheus via un point de terminaison HTTP.

Malheureusement, ce n'est pas déjà le cas pour notre application « Selenium Grid ». La métrique dont nous avons besoin n'est pas disponible au format requis. Il nous reste donc une étape supplémentaire à effectuer avant de pouvoir la récupérer et l'intégrer au service « Google Cloud Monitoring » (anciennement « Stackdriver »).

Heureusement, quelqu'un a déjà créé un Exporteur de métriques Prometheus pour « Selenium Grid ». Cet exporteur interroge l'API « Selenium Grid », crée des métriques utiles à partir des réponses et les exposent dans le format Prometheus via un point de terminaison HTTP. Pour en savoir plus sur les exporteurs Prometheus, consultez Créer des exporteurs Prometheus.

Déployons l'exporteur Prometheus pour exposer les métriques « Selenium Grid » au format requis. Voici les manifestes Kubernetes correspondants :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: selenium-grid-metrics-exporter
  namespace: default
  labels:
    app: selenium-grid-metrics-exporter
spec:
  replicas: 1
  selector:
    matchLabels:
      app: selenium-grid-metrics-exporter
  template:
    metadata:
      labels:
        app: selenium-grid-metrics-exporter
    spec:
      containers:
      - name: selenium-grid-metrics-exporter
        image: gmichels/selenium-grid-exporter:v1.0.0
        ports:
          - containerPort: 8080
        command:
          - /bin/sh
          - -c
          - python ./exporter.py --grid-url http://selenium-hub:4444 --publish-interval 15 -p 8080
---
apiVersion: v1
kind: Service
metadata:
  name: selenium-grid-metrics-exporter
  namespace: default
  labels:
    app: selenium-grid-metrics-exporter
spec:
  ports:
  - port: 8080
    targetPort: 8080
    name: http
  selector:
    app: selenium-grid-metrics-exporter
  type: ClusterIP

Après avoir créé les objets Deployment et Service précédents pour l'exporteur Prometheus, interrogeons le service pour voir si la métrique concernant la taille de la file d'attente des sessions est disponible :

# Create a proxy from local machine to the exporter service
$ kubectl port-forward svc/selenium-grid-metrics-exporter --address 172.25.22.210 8080:8080 &

# Query the exporter service through the local proxy
$ curl http://172.25.22.210:8080/metrics

Handling connection for 8080
# HELP python_gc_objects_collected_total Objects collected during gc
# TYPE python_gc_objects_collected_total counter
python_gc_objects_collected_total{generation="0"} 112.0
python_gc_objects_collected_total{generation="1"} 266.0
python_gc_objects_collected_total{generation="2"} 0.0
(...)
# HELP selenium_grid_total_slots Total number of slots in the grid
# TYPE selenium_grid_total_slots gauge
selenium_grid_total_slots 1.0
# HELP selenium_grid_node_count Number of nodes in grid
# TYPE selenium_grid_node_count gauge
selenium_grid_node_count 1.0
# HELP selenium_grid_session_count Number of running sessions
# TYPE selenium_grid_session_count gauge
selenium_grid_session_count 0.0
# HELP selenium_grid_session_queue_size Number of queued sessions
# TYPE selenium_grid_session_queue_size gauge
selenium_grid_session_queue_size 0.0
# HELP selenium_node_slot_count Total number of node slots
# TYPE selenium_node_slot_count gauge
selenium_node_slot_count{deployment="selenium-node-chrome",node="chrome"} 0.0
selenium_node_slot_count{deployment="selenium-node-firefox",node="firefox"} 1.0
# HELP selenium_node_session_count Total number of node slots
# TYPE selenium_node_session_count gauge
selenium_node_session_count{deployment="selenium-node-chrome",node="chrome"} 0.0
selenium_node_session_count{deployment="selenium-node-firefox",node="firefox"} 0.0
# HELP selenium_node_usage_percent % of used node slots
# TYPE selenium_node_usage_percent gauge
selenium_node_usage_percent{deployment="selenium-node-chrome",node="chrome"} 0.0
selenium_node_usage_percent{deployment="selenium-node-firefox",node="firefox"} 0.0

Nous voyons un ensemble de métriques utiles pour l'application « Selenium Grid », exposées au format Prometheusrequis. Très bien.

La métrique relative au nombre de sessions en file d'attente dont nous avons besoin pour la mise à l'échelle automatique des pods de l'application est également disponible sous le nom « selenium_grid_session_queue_size ».

Ensuite, nous allons rendre la métrique « selenium_grid_session_queue_size » disponible dans le service « Google Cloud Monitoring », pour une utilisation ultérieure par le HPA.

Envoyer les métriques des exporteurs Prometheus à Cloud Monitoring

Déployer Prometheus-to-sd afin de collecter les métriques Prometheus exportées et les envoyer au service « Google Cloud Monitoring » (anciennement « Stackdriver »).

Voici le manifeste Kubernetes pour cela :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: selenium-grid-metrics-prometheus-to-sd
  namespace: default
  labels:
    app: selenium-grid-metrics-prometheus-to-sd
spec:
  replicas: 1
  selector:
    matchLabels:
      app: selenium-grid-metrics-prometheus-to-sd
  template:
    metadata:
      labels:
        app: selenium-grid-metrics-prometheus-to-sd
    spec:
      containers:
      - name: selenium-grid-metrics-prometheus-to-sd
        image: gcr.io/google-containers/prometheus-to-sd:v0.9.2
        ports:
          - name: profiler
            containerPort: 6060
        command:
          - /monitor
          # Scrape and export intervals are implemented in versions of prometheus-to-sd > v0.9.0
          - --scrape-interval=20s # how often in seconds metrics are pulled from Grid Hub service
          - --export-interval=30s # how often in seconds metrics are pushed to Stackdriver service
          - --stackdriver-prefix=custom.googleapis.com
          - --source=selenium-grid-metrics-exporter:http://selenium-grid-metrics-exporter:8080
          - --pod-id=$(POD_NAME)
          - --namespace-id=$(POD_NAMESPACE)
        env:
          - name: POD_NAME
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
          - name: POD_NAMESPACE
            valueFrom:
              fieldRef:
                fieldPath: metadata.namespace

Jetons un œil aux statuts des pods :

$ kubectl get pods
NAME                                                     READY   STATUS    RESTARTS   AGE
selenium-grid-metrics-exporter-57577d8869-kkfhz          1/1     Running   0          57m
selenium-grid-metrics-prometheus-to-sd-9d54fc8d7-8n4gs   1/1     Running   0          2m22s
selenium-hub-6787dc9b7d-hcsht                            1/1     Running   0          84m
selenium-node-firefox-85d5c7b75b-b7vmb                   1/1     Running   0          72m

$ kubectl logs -f pods/selenium-grid-metrics-prometheus-to-sd-9d54fc8d7-8n4gs
(...)
I0221 13:39:45.551781       1 main.go:182] Taking source configs from flags
I0221 13:39:45.551811       1 main.go:184] Taking source configs from kubernetes api server
I0221 13:39:45.551906       1 main.go:124] Built the following source configs: [0xc0002d4a90]
I0221 13:39:45.551987       1 main.go:193] Running prometheus-to-sd, monitored target is selenium-grid-metrics-exporter http://selenium-grid-metrics-exporter:8080

Tout semble fonctionner. Si vous obtenez une erreur d'autorisation 403 à cette étape, vérifiez d'abord deux points :

  • si workload identity est activé sur le pool de nœuds, assurez-vous que le compte de service Kubernetes associé aux pods exécutant le conteneur Prometheus-to-sd est lié à un compte de service IAM disposant au moins de l'autorisation « monitoring.timeSeries.create ». Pour savoir comment procéder, consultez Configuration de workload identity.

  • si workload identity n'est pas activé sur le pool de nœuds, assurez-vous que la fonctionnalité « Cloud Monitoring » est activée sur le cluster GKE. L'activation de cette fonctionnalité ajoutera la portée OAuth (OAuth scope) https://www.googleapis.com/auth/monitoring pour les pools de nœuds nouvellement créés, permettant un accès complet en lecture et en écriture à l'API « Cloud Monitoring ». Consultez Portées d'accès OAuth GKE et Toutes les portées d'accès OAuth de l'API Google pour plus de détails. Migrez vos charges de travail vers le pool de nœuds nouvellement créé après avoir activé la fonctionnalité « Cloud Monitoring ». La commande suivante :

gcloud container clusters describe <cluster-name> --zone <cluster-zone>

pourrait être utilisée pour voir les portées OAuth des pools de nœuds du cluster en recherchant « oauthScopes » dans le résultat.

La métrique « selenium_grid_session_queue_size » devrait désormais être présente dans le service « Google Cloud Monitoring ». Vérifions en accédant à l'Explorateur de métriques Google Cloud :

Métriques personnalisées dans le service Google Cloud Monitoring
Métrique concernant la taille de la file d'attente des sessions Selenium Grid

Notez que le conteneur gcr.io/google-containers/prometheus-to-sd pourrait également être déployé en tant que conteneur sidecar à l'intérieur des pods de l'exporteur Prometheus exposant les métriques personnalisées qui nous intéressent. Pour en savoir plus sur l'export de métriques vers le service Google Cloud Monitoring, jetez un œil à Exemples-Prometheus-vers-stackdriver.

S'assurer que le HPA peut interroger des métriques personnalisées/externes

Maintenant que la métrique dont nous avons besoin est disponible dans le service « Google Cloud Monitoring », nous devons nous assurer que les objets HorizontalPodAutoscaler (HPA) sont capables de la lire.

Pour cela, nous devons étendre le service d'API Kubernetes, afin de créer des API qui seront utilisées par le HPA, et ensuite pouvoir obtenir les métriques personnalisées/externes disponibles dans le service « Google Cloud Monitoring ».

Pour y parvenir, nous devons déployer un adaptateur de métriques personnalisées/externes au sein du cluster GKE. Voir adaptateur de métriques personnalisées stackdriver pour en savoir plus.

Voici la commande utilisée pour déployer l'adaptateur de métriques personnalisées/externes :

# If the resource type of the metrics we want to get from the new metrics APIs
# uses the new resource model: k8s_pod, k8s_node

$ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/k8s-stackdriver/master/custom-metrics-stackdriver-adapter/deploy/production/adapter_new_resource_model.yaml

# If the resource type of the metrics we want to get from the new metrics APIs
# uses the legacy resource model: gke_container

$ kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/k8s-stackdriver/master/custom-metrics-stackdriver-adapter/deploy/production/adapter.yaml

# Result

namespace/custom-metrics created
serviceaccount/custom-metrics-stackdriver-adapter created
clusterrolebinding.rbac.authorization.k8s.io/custom-metrics:system:auth-delegator created
rolebinding.rbac.authorization.k8s.io/custom-metrics-auth-reader created
clusterrolebinding.rbac.authorization.k8s.io/custom-metrics-resource-reader created
deployment.apps/custom-metrics-stackdriver-adapter created
service/custom-metrics-stackdriver-adapter created
apiservice.apiregistration.k8s.io/v1beta1.custom.metrics.k8s.io created
apiservice.apiregistration.k8s.io/v1beta2.custom.metrics.k8s.io created
apiservice.apiregistration.k8s.io/v1beta1.external.metrics.k8s.io created
Warning: resource clusterroles/external-metrics-reader is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
clusterrole.rbac.authorization.k8s.io/external-metrics-reader configured
clusterrolebinding.rbac.authorization.k8s.io/external-metrics-reader created

Après cela, deux nouvelles API de métriques seront disponibles avec les groupes d'API suivants : « external.metrics.k8s.io » et « custom.metrics.k8s.io ».

Jetons un œil aux journaux de l’adaptateur de métriques personnalisées/externes :

$ kubectl get all -n custom-metrics

NAME                                                     READY   STATUS    RESTARTS   AGE
pod/custom-metrics-stackdriver-adapter-df7d6cfcd-tkp45   1/1     Running   0          3m21s

NAME                                         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
service/custom-metrics-stackdriver-adapter   ClusterIP   10.1.0.208   <none>        443/TCP   3m20s

NAME                                                 READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/custom-metrics-stackdriver-adapter   1/1     1            1           3m22s

NAME                                                           DESIRED   CURRENT   READY   AGE
replicaset.apps/custom-metrics-stackdriver-adapter-df7d6cfcd   1         1         1       3m22s

$ kubectl logs -f pods/custom-metrics-stackdriver-adapter-df7d6cfcd-tkp45 -n custom-metrics

I0222 09:38:55.812226       1 adapter.go:200] serverOptions: {false true true false false   false false}
I0222 09:38:55.812362       1 adapter.go:210] ListFullCustomMetrics is disabled, which would only list 1 metric resource to reduce memory usage. Add --list-full-custom-metrics to list full metric resources for debugging.
I0222 09:38:59.132069       1 serving.go:341] Generated self-signed cert (apiserver.local.config/certificates/apiserver.crt, apiserver.local.config/certificates/apiserver.key)
I0222 09:39:01.415941       1 secure_serving.go:256] Serving securely on [::]:443
I0222 09:39:01.416012       1 requestheader_controller.go:169] Starting RequestHeaderAuthRequestController
I0222 09:39:01.416025       1 shared_informer.go:240] Waiting for caches to sync for RequestHeaderAuthRequestController
I0222 09:39:01.416068       1 dynamic_serving_content.go:129] "Starting controller" name="serving-cert::apiserver.local.config/certificates/apiserver.crt::apiserver.local.config/certificates/apiserver.key"
I0222 09:39:01.416127       1 tlsconfig.go:240] "Starting DynamicServingCertificateController"
I0222 09:39:01.416253       1 configmap_cafile_content.go:201] "Starting controller" name="client-ca::kube-system::extension-apiserver-authentication::client-ca-file"
I0222 09:39:01.416259       1 shared_informer.go:240] Waiting for caches to sync for client-ca::kube-system::extension-apiserver-authentication::client-ca-file
I0222 09:39:01.416273       1 configmap_cafile_content.go:201] "Starting controller" name="client-ca::kube-system::extension-apiserver-authentication::requestheader-client-ca-file"
I0222 09:39:01.416278       1 shared_informer.go:240] Waiting for caches to sync for client-ca::kube-system::extension-apiserver-authentication::requestheader-client-ca-file
I0222 09:39:01.516235       1 shared_informer.go:247] Caches are synced for RequestHeaderAuthRequestController
I0222 09:39:01.516354       1 shared_informer.go:247] Caches are synced for client-ca::kube-system::extension-apiserver-authentication::requestheader-client-ca-file
I0222 09:39:01.536396       1 shared_informer.go:247] Caches are synced for client-ca::kube-system::extension-apiserver-authentication::client-ca-file

Il semble que tout fonctionne correctement. Nous pouvons voir que la métrique dont nous avons besoin est désormais disponible via les nouvelles API de métriques en faisant :

$ kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1/namespaces/default/custom.googleapis.com|selenium-grid-metrics-exporter|selenium_grid_session_queue_size" | jq

{
  "kind": "ExternalMetricValueList",
  "apiVersion": "external.metrics.k8s.io/v1beta1",
  "metadata": {},
  "items": [
    {
      "metricName": "custom.googleapis.com|selenium-grid-metrics-exporter|selenium_grid_session_queue_size",
      "metricLabels": {
        "resource.labels.cluster_name": "mygkecluster",
        "resource.labels.container_name": "",
        "resource.labels.instance_id": "gke-mygkecluster-main-486d49bd-w6gb",
        "resource.labels.namespace_id": "default",
        "resource.labels.pod_id": "selenium-grid-metrics-prometheus-to-sd-9d54fc8d7-twctc",
        "resource.labels.project_id": "wise-bulk-410910",
        "resource.labels.zone": "us-central1-c",
        "resource.type": "gke_container"
      },
      "timestamp": "2024-02-22T10:45:16Z",
      "value": "5"
    }
  ]
}

# or 

$ kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq | grep -i "selenium_grid_session_queue_size"

Mise à l'échelle automatique à l'aide de mesures personnalisées/externes

Nous pouvons maintenant configurer le HPA pour utiliser la métrique personnalisée/externe pour la mise à l'échelle des pods « Nœuds Selenium Grid ». Voici le manifeste pour la création de la ressource HPA .

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: selenium-grid-nodes
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: selenium-node-firefox
  minReplicas: 1
  maxReplicas: 3
  metrics:
  - type: External
    external:
      metric:
        name: custom.googleapis.com|selenium-grid-metrics-exporter|selenium_grid_session_queue_size
      target:
        type: Value
        value: 

Après avoir déployé le HPA, nous devrions voir la taille de la file d'attente des sessions dans la colonne « TARGET » comme indiqué ci-dessous, ce qui signifie que le HPA est désormais capable d'utiliser la métrique personnalisée « selenium_grid_session_queue_size » pour la mise à l'échelle automatique des pods « nœuds » de l'application « Selenium Grid ».

$ kubectl get hpa
NAME                  REFERENCE                          TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
selenium-grid-nodes   Deployment/selenium-node-firefox   10/3      1         3         3          58m

Voici ce que nous voyons lorsque le HPA n'est pas en mesure d'obtenir la valeur de la métrique concernant la taille de la file d'attente des sessions :

$ kubectl get hpa
NAME                  REFERENCE                          TARGETS       MINPODS   MAXPODS   REPLICAS   AGE
selenium-grid-nodes   Deployment/selenium-node-firefox   <unknown>/3   1         3         0          16s

$ kubectl describe hpa
Name: selenium-grid-nodes
Namespace: default
(...)
Reference: Deployment/selenium-node-firefox
Metrics: ( current / target )
  "custom.googleapis.com|selenium-grid-metrics-exporter|selenium_grid_session_queue_size" (target value):  <unknown> / 3
Min replicas: 1
Max replicas: 3
Deployment pods: 1 current / 1 desired
Conditions:
  Type            Status  Reason                   Message
  ----            ------  ------                   -------
  AbleToScale     True    ReadyForNewScale         recommended size matches current size
  ScalingActive   False   FailedGetExternalMetric  the HPA was unable to compute the replica count: unable to get external metric default/custom.googleapis.com|selenium-grid-metrics-exporter|selenium_grid_session_queue_size/nil: unable to fetch metrics from external metrics API: the server could not find the requested resource (get custom.googleapis.com|selenium-grid-metrics-exporter|selenium_grid_session_queue_size.external.metrics.k8s.io)
  ScalingLimited  False   DesiredWithinRange       the desired count is within the acceptable range
Events:
  Type     Reason                   Age               From                       Message
  ----     ------                   ----              ----                       -------
  Warning  FailedGetExternalMetric  6s (x2 over 21s)  horizontal-pod-autoscaler  unable to get external metric default/custom.googleapis.com|selenium-grid-metrics-exporter|selenium_grid_session_queue_size/nil: unable to fetch metrics from external metrics API: the server could not find the requested resource (get custom.googleapis.com|selenium-grid-metrics-exporter|selenium_grid_session_queue_size.external.metrics.k8s.io)

Réglage du HPA

Pour personnaliser les comportements de « mise à l'échelle verticale » (scale in) et de « mise à l'échelle horizontale » (scale out) pour l' HPA, voici la section de la référence de l'API hpa-v2 à ce sujet.

La fréquence à laquelle le HPA extrait les métriques des serveurs de métriques est un paramètre de kube-controller-manager ('--horizontal-pod-autoscaler-sync-period') et est défini par défaut sur « 15 s ».

Pour changer l'intervalle de scraping et d'export du composant Prometheus-to-sd, les paramètres « --scrape-interval » et « --export-interval » peuvent être utilisés avec la commande « monitor ». Voir Intervalle-de-scraping-vs-intervalle d-export-prometheus-vers-sd et fichier-go-principal-prometheus-vers-sd pour en savoir plus.