Autoescalar tus pods con las métricas de Prometheus

Ya te adelanté en el artículo sobre autoescalado de aplicaciones en Kubernetes que, más allá de usar la CPU o la memoria consumida, también era posible utilizar métricas personalizadas, aunque todavía está en fase beta. En este artículo quiero contarte cómo usar las métricas de Prometheus para escalar tus pods.

Desplegar Prometheus Adapter

Sobre el ejemplo que utilicé para mostrarte cómo monitorizar tus aplicaciones con Prometheus, voy a desplegar la pieza necesaria para poder usar las métricas de este para escalar tus aplicaciones. Esta se llama Prometheus Adapter y lo primero que debes hacer es copiarte el archivo values.yml de su chart, donde necesitas hacer un par de modificaciones:

La primera de ellas es la URL del servidor de Prometheus:

# Url to access prometheus
prometheus:
  url: http://prometheus-server
  port: 80

Si vas a desplegar el adaptador en el mismo namespace que Prometheus, con poner el nombre del servicio es más que suficiente. Por otro lado, confirma que el puerto es el correcto. Si has seguido el artículo anterior, será el 80.

Lo segundo que tienes que adaptar es el apartado rules donde he añadido un par de ejemplos de las métricas que quiero utilizar:

rules:
  default: false
  rules:
  custom:
    - seriesQuery: 'http_requests_in_progress'
      resources:
        overrides:
          kubernetes_namespace:
            resource: namespace
          kubernetes_pod_name:
            resource: pod
      name:
        matches: "http_requests_in_progress"
        as: "http_requests_in_progress"
      metricsQuery: 'sum(<<.Series>>{<<.LabelMatchers>>}) by (<<.GroupBy>>)'  
    - seriesQuery: 'http_requests_received_total'
      resources:
        overrides:
          kubernetes_namespace:
            resource: namespace
          kubernetes_pod_name:
            resource: pod
      name:
        matches: "http_requests_received_total"        
        as: "http_requests_received_total"
      metricsQuery: 'sum(rate(<<.Series>>{<<.LabelMatchers>>}[3m])) by (<<.GroupBy>>)'    

Ambas provienen de la librería prometheus-net que expliqué en el artículo anterior. Es importante que tengas en cuenta que para que estas consultas funcionen correctamente debes indicar en el apartado resources > overrides de cada una qué propiedades de dicha métrica se están refiriendo a recursos de Kubernetes, ya que de lo contrario no funcionarán correctamente. En el caso de estas métricas, puedes ver en la UI de Prometheus que kubernetes_namespace se corresponde con el recurso Namespace de Kubernetes y que kubernetes_pod_name con el nombre del pod.

Por último, ejecuta el siguiente comando para desplegar el chart con los parámetros que has modificado:

helm install --namespace monitoring --name prom-adapter stable/prometheus-adapter -f prometheus-adapter/values.yml
kubectl rollout status -n monitoring deploy/prom-adapter-prometheus-adapter

Comprobar la configuración

Una vez desplegado el adaptador vamos a comprobar que las métricas personalizadas están funcionando correctamente. El objetivo es que Prometheus Adapter exponga dichas métricas a través del endpoint /apis/custom.metrics.k8s.io/v1beta1. Para comprobar que esto es así puedes utilizar esta llamada al endpoint:

kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1 | jq

Si la configuración es correcta deberías de ver algo como lo siguiente:

➜  Prometheus-By-Me kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1 | jq
{
  "kind": "APIResourceList",
  "apiVersion": "v1",
  "groupVersion": "custom.metrics.k8s.io/v1beta1",
  "resources": [
    {
      "name": "namespaces/http_requests_in_progress",
      "singularName": "",
      "namespaced": false,
      "kind": "MetricValueList",
      "verbs": [
        "get"
      ]
    },
    {
      "name": "pods/http_requests_in_progress",
      "singularName": "",
      "namespaced": true,
      "kind": "MetricValueList",
      "verbs": [
        "get"
      ]
    },
    {
      "name": "namespaces/http_requests_received_total",
      "singularName": "",
      "namespaced": false,
      "kind": "MetricValueList",
      "verbs": [
        "get"
      ]
    },
    {
      "name": "pods/http_requests_received_total",
      "singularName": "",
      "namespaced": true,
      "kind": "MetricValueList",
      "verbs": [
        "get"
      ]
    }
  ]
}

Nota: Para esta llamada estoy usando jq, que me permite ver de una forma más legible los resultados en JSON, además de poder filtrar y hacer consultas sobre ellos.

Lo siguiente que necesitas comprobar es que los pods que tienen valores para estas métricas los están devolviendo de manera correcta:

kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/bookstore/pods/*/http_requests_in_progress" | jq
kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/bookstore/pods/*/http_requests_received_total" | jq

Crear un Horizontal Pod Autoscaler con métricas personalizas

Ahora que estás exponiendo métricas personalizadas, con la ayuda de Prometheus Adapter, vamos a comprobar que estas funcionan correctamente con la API que configuré con prometheus-net:

apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: bookstore-hpa
  namespace: bookstore
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: booksapi
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Pods
      pods:
        metricName: http_requests_received_total
        targetAverageValue: "100m"

Como puedes ver, he utilizado la métrica http_requests_received_total y he indicado que cómo mínimo vamos a tener 2 réplicas y cómo máximo hasta 10. Esto dependerá de si el valor de la métrica http_requests_received_total se mantiene por encima o por debajo de 100m.
Ahora, al igual que hice en el artículo donde configuré la monitorización de la API, voy a ejecutar dentro de un busybox una llamada en bucle a mi API para generar tráfico y obligar a mi HPA a que aumente el número de réplicas, ya que el valor de la métrica http_requests_received_total subirá:

kubectl run -i --tty load-test$RANDOM --image=busybox /bin/sh
while true; do wget -q -O- http://booksapi-svc.bookstore.svc.cluster.local/api/books; done

Si controlas en dos ventanas diferentes tanto el HPA como el deployment confirmarás que la métrica va aumentando el valor:

➜  Prometheus-By-Me kubectl get hpa -n bookstore -w
NAME            REFERENCE             TARGETS          MINPODS   MAXPODS   REPLICAS   AGE
bookstore-hpa   Deployment/booksapi   <unknown>/100m   1         10        0          10s
bookstore-hpa   Deployment/booksapi   66m/100m         1         10        2          15s
bookstore-hpa   Deployment/booksapi   5278m/100m       1         10        2          5m3s
bookstore-hpa   Deployment/booksapi   14699m/100m      1         10        4          5m18s
bookstore-hpa   Deployment/booksapi   14323m/100m      1         10        8          5m33s
bookstore-hpa   Deployment/booksapi   7968m/100m       1         10        10         5m48s
bookstore-hpa   Deployment/booksapi   8931m/100m       1         10        10         6m4s
bookstore-hpa   Deployment/booksapi   10021m/100m      1         10        10         6m19s

al igual que las réplicas:

➜  Prometheus-By-Me kubectl get deploy -n bookstore -w
NAME       READY   UP-TO-DATE   AVAILABLE   AGE
booksapi   2/2     2            2           4h42m
booksapi   2/4     2            2           4h46m
booksapi   2/4     2            2           4h46m
booksapi   2/4     2            2           4h46m
booksapi   2/4     4            2           4h46m
booksapi   3/4     4            3           4h46m
booksapi   4/4     4            4           4h46m
booksapi   4/8     4            4           4h47m
booksapi   4/8     4            4           4h47m
booksapi   4/8     4            4           4h47m
booksapi   4/8     8            4           4h47m
booksapi   5/8     8            5           4h47m
booksapi   6/8     8            6           4h47m
booksapi   7/8     8            7           4h47m
booksapi   8/8     8            8           4h47m
booksapi   8/10    8            8           4h47m
booksapi   8/10    8            8           4h47m
booksapi   8/10    8            8           4h47m
booksapi   8/10    10           8           4h47m
booksapi   9/10    10           9           4h47m
booksapi   10/10   10           10          4h47m

Evidentemente, como tiene un tope de 10 réplicas, aunque el valor esté muy por encima de 100m nunca creará más del máximo.

Una vez que pares el pod load-test podrás comprobar que las réplicas vuelven a disminuir, de manera muy progresiva, conforme se estabiliza la métrica personalizada de Prometheus.:

➜  Prometheus-By-Me kubectl get hpa -n bookstore -w
NAME            REFERENCE             TARGETS          MINPODS   MAXPODS   REPLICAS   AGE
bookstore-hpa   Deployment/booksapi   <unknown>/100m   1         10        0          10s
bookstore-hpa   Deployment/booksapi   66m/100m         1         10        2          15s
bookstore-hpa   Deployment/booksapi   5278m/100m       1         10        2          5m3s
bookstore-hpa   Deployment/booksapi   14699m/100m      1         10        4          5m18s
bookstore-hpa   Deployment/booksapi   14323m/100m      1         10        8          5m33s
bookstore-hpa   Deployment/booksapi   7968m/100m       1         10        10         5m48s
bookstore-hpa   Deployment/booksapi   8931m/100m       1         10        10         6m4s
bookstore-hpa   Deployment/booksapi   10021m/100m      1         10        10         6m19s
bookstore-hpa   Deployment/booksapi   11798m/100m      1         10        10         6m34s
bookstore-hpa   Deployment/booksapi   13569m/100m      1         10        10         6m49s
bookstore-hpa   Deployment/booksapi   15529m/100m      1         10        10         7m5s
bookstore-hpa   Deployment/booksapi   16856m/100m      1         10        10         7m20s
bookstore-hpa   Deployment/booksapi   16618m/100m      1         10        10         7m35s
bookstore-hpa   Deployment/booksapi   14649m/100m      1         10        10         7m50s
bookstore-hpa   Deployment/booksapi   13233m/100m      1         10        10         8m6s
bookstore-hpa   Deployment/booksapi   12181m/100m      1         10        10         8m21s
bookstore-hpa   Deployment/booksapi   10665m/100m      1         10        10         8m36s
bookstore-hpa   Deployment/booksapi   8642m/100m       1         10        10         8m51s
bookstore-hpa   Deployment/booksapi   6804m/100m       1         10        10         9m7s
bookstore-hpa   Deployment/booksapi   4943m/100m       1         10        10         9m22s
bookstore-hpa   Deployment/booksapi   3078m/100m       1         10        10         9m37s
bookstore-hpa   Deployment/booksapi   1238m/100m       1         10        10         9m52s
bookstore-hpa   Deployment/booksapi   69m/100m         1         10        10         10m
bookstore-hpa   Deployment/booksapi   66m/100m         1         10        10         10m
bookstore-hpa   Deployment/booksapi   66m/100m         1         10        10         14m
bookstore-hpa   Deployment/booksapi   66m/100m         1         10        7          15m
bookstore-hpa   Deployment/booksapi   66m/100m         1         10        7          20m
bookstore-hpa   Deployment/booksapi   66m/100m         1         10        5          20m
bookstore-hpa   Deployment/booksapi   66m/100m         1         10        5          25m
bookstore-hpa   Deployment/booksapi   66m/100m         1         10        4          25m
bookstore-hpa   Deployment/booksapi   66m/100m         1         10        4          30m
bookstore-hpa   Deployment/booksapi   66m/100m         1         10        3          30m
bookstore-hpa   Deployment/booksapi   66m/100m         1         10        2          35m

¡Saludos!