Kubernetes: HorizontalPodAutoscaler — обзор и примеры

Автор: | 08/11/2020
 

Kubernetes HorizontalPodAutoscaler, как видно из названия, предназначен для автоматического скейлинга Kubernetes Pods в кластере, которые управляются ReplicationController, Deployment или ReplicaSet контроллерами, основываясь на их метриках потребления ресурсов — CPU, память и т.д.

Кратко его рассматривали в посте Kubernetes: запуск metrics-server в AWS EKS для Kubernetes Pod AutoScaler, теперь разберёмся с доступными метриками.

Для HPA доступны три типа метрик:

  • metrics.k8s.io: метрики использования ресурсов, как правило предоставляется metrics-server
  • custom.metrics.k8s.io: метрики, предоставляемые адаптерами (контроллерами), работающими в самом кластере, такими как Microsoft Azure Adapter, Google Stackdriver, Prometheus Adapter (Prometheus Adapter используем в этом посте чуть позже), см. полный список тут>>>
  • external.metrics.k8s.io: по сути тот же Custom Metircs API, но метрики поставляются внешней системой, например от AWS CloudWatch

См. Support for metrics APIs и Custom and external metrics for autoscaling workloads.

Кроме HPA существует Vertical Pod Autoscaling (VPA), и их можно комбинировать, хотя и с ограничениями, см. Horizontal Pod Autoscaling Limitations.

Создание HorizontalPodAutoscaler

Создадим простой НРА, который будет выполнять скейлинг по потреблению CPU:

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: hpa-example
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: deployment-example
  minReplicas: 1
  maxReplicas: 5
  targetCPUUtilizationPercentage: 10

Тут:

  • apiVersion: autoscaling/v1 — API-группа autoscaling, обращайте внимание на версию, т.к. в API v1 доступен скейлинг только по CPU, а memory и custom metrics могут быть использованы в API `v2beta2` (в v1 доступны через аннотации), см. API Object.
  • spec.scaleTargetRef: указываем НРА, какой контроллер он будет скейлить (ReplicationController, Deployment или ReplicaSet), в данном случае будет выполнен поиск объекта типа Deployment с именем deployment-example
  • spec.minReplicas, spec.maxReplicas: минимальное и максимальное количество подов в контроллере, которое будет запущено этим HPA
  • targetCPUUtilizationPercentage: % от requests использования CPU, при достижении которого НРА начнёт добавлять или удалять поды

Создаём его:

kubectl apply -f hpa-example.yaml
horizontalpodautoscaler.autoscaling/hpa-example created

Проверяем:

kubectl get hpa hpa-example
NAME          REFERENCE                       TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
hpa-example   Deployment/deployment-example   <unknown>/10%   1         5         0          89s

Сейчас у нас TARGETS со значением <unknown>, т.к. нет подов, с которых НРА мог бы собрать метрики, хотя сами метрики в кластере доступны — проверяем:

kubectl get --raw "/apis/metrics.k8s.io/" | jq
{
"kind": "APIGroup",
"apiVersion": "v1",
"name": "metrics.k8s.io",
"versions": [
{
"groupVersion": "metrics.k8s.io/v1beta1",
"version": "v1beta1"
}
],
"preferredVersion": {
"groupVersion": "metrics.k8s.io/v1beta1",
"version": "v1beta1"
}
}

Добавляем Deployment с именем deployment-example, файл hpa-deployment-example.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-example
spec:
  replicas: 1
  strategy:
    type: RollingUpdate
  selector:
    matchLabels:
      application: deployment-example
  template:
    metadata:
      labels:
        application: deployment-example
    spec: 
      containers:
      - name: deployment-example-pod
        image: nginx
        ports:
          - containerPort: 80
        resources:
          requests:
            cpu: 100m
            memory: 100Mi

Тут мы описываем Deployment, который запустит один под с NINGX, которому задаст requests в 100 millicores и 100 mebibyte (мегабайт) памяти, см. Kubernetes best practices: Resource requests and limits.

Создаём его:

kubectl apply -f hpa-deployment-example.yaml
deployment.apps/deployment-example created

Проверяем HPA теперь:

kubectl get hpa hpa-example
NAME          REFERENCE                       TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
hpa-example   Deployment/deployment-example   0%/10%    1         5         1          14m

Наш НРА увидел деплоймент, и начал собирать с него метрики.

Проверим метрики с конкретного пода — находим его имя:

kubectl get pod | grep example | cut -d " " -f 1
deployment-example-86c47f5897-2mzjd

И выполняем API-запрос:

kubectl get --raw /apis/metrics.k8s.io/v1beta1/namespaces/default/pods/deployment-example-86c47f5897-2mzjd | jq
{
"kind": "PodMetrics",
"apiVersion": "metrics.k8s.io/v1beta1",
"metadata": {
"name": "deployment-example-86c47f5897-2mzjd",
"namespace": "default",
"selfLink": "/apis/metrics.k8s.io/v1beta1/namespaces/default/pods/deployment-example-86c47f5897-2mzjd",
"creationTimestamp": "2020-08-07T10:41:21Z"
},
"timestamp": "2020-08-07T10:40:39Z",
"window": "30s",
"containers": [
{
"name": "deployment-example-pod",
"usage": {
"cpu": "0",
"memory": "2496Ki"
}
}
]
}

Потребление CPU — ноль, памяти — 2 мегабайта — глянем в top:

kubectl top pod deployment-example-86c47f5897-2mzjd
NAME                                  CPU(cores)   MEMORY(bytes)
deployment-example-86c47f5897-2mzjd   0m           2Mi

«Ага, вот эти ребята!» (с)

Хорошо — метрики видим, НРА есть, деплоймент есть — попробуем посмотреть, как работает скейлинг.

Load testing HorizontalPodAutoscaler scaling

Используем loadimpact/loadgentest-wrk.

Запустим переадресацию портов с локальной машины к поду, что бы получить доступ к нашему NGINX, т.к. LoadBalancer в мир мы не создавали (см. Kubernetes: ClusterIP vs NodePort vs LoadBalancer, Services и Ingress — обзор, примеры):

kubectl port-forward deployment-example-86c47f5897-2mzjd 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

Проверяем ресурсы ещё раз:

kubectl get hpa hpa-example
NAME          REFERENCE                       TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
hpa-example   Deployment/deployment-example   0%/10%    1         5         1          33m

0% CPU использовано, запущен 1 под (REPLICAS 1).

Запускаем тест:

docker run --rm --net=host loadimpact/loadgentest-wrk -c 100 -t 100 -d 5m http://127.0.0.1:8080
Running 5m test @ http://127.0.0.1:8080

Тут:

  • открываем 100 подключений, используя 600 потоков
  • выполняем тест 5 минут

Проверяем под:

kubectl top pod deployment-example-86c47f5897-2mzjd
NAME                                  CPU(cores)   MEMORY(bytes)
deployment-example-86c47f5897-2mzjd   49m          2Mi

CPU уже 49mi, в реквестах мы задавали лимит в 10 milicpu — проверяем HPA:

kubectl get hpa hpa-example
NAME          REFERENCE                       TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
hpa-example   Deployment/deployment-example   49%/10%   1         5         4          42m

TARGETS 49% из лимита в 10% — и НРА запустил новые поды — REPLICAS 4:

kubectl get pod | grep example
deployment-example-86c47f5897-2mzjd    1/1     Running   0          31m
deployment-example-86c47f5897-4ntd4    1/1     Running   0          24s
deployment-example-86c47f5897-p7tc7    1/1     Running   0          8s
deployment-example-86c47f5897-q49gk    1/1     Running   0          24s
deployment-example-86c47f5897-zvdvz    1/1     Running   0          24s

Multi-metrics scaling

Сейчас наш Deployment скейлится только на основании метрики потребления CPU.

Обновим НРА — добавим возможность скейлится по памяти:

apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: hpa-example
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: deployment-example
  minReplicas: 1
  maxReplicas: 5
  metrics:
  - type: Resource
    resource:
      name: cpu
      targetAverageUtilization: 10
  - type: Resource
    resource:
      name: memory 
      targetAverageUtilization: 10

autoscaling API group versions

Давайте ещё раз вернёмся к версиям API.

В первом варианте мы использовали autoscaling/v1, в котором есть поддержка только targetCPUUtilizationPercentage.

Проверяем autoscaling/v2beta1 — тут уже добавлено поле metrics, которое явлется массивом MetricSpec, у которого есть уже четыре поля — external, object, pods, resource.

В свою очередь для resource имеется объект ResourceMetricSource, у которого имеются поля targetAverageUtilization и targetAverageValue, которые мы и используем в mertics вместо указания targetCPUUtilizationPercentage.

Применяем изменения в НРА:

kubectl apply -f hpa-example.yaml
horizontalpodautoscaler.autoscaling/hpa-example configured

Проверяем:

kubectl get hpa hpa-example
NAME          REFERENCE                       TARGETS          MINPODS   MAXPODS   REPLICAS   AGE
hpa-example   Deployment/deployment-example   3%/10%, 0%/10%   1         5         2          126m

Теперь в TARGETS мы видим две метрики — и память, и CPU.

Нагрузить NGINX по памяти будет сложно, потому проверим, сколько он сейчас потребляет памяти:

kubectl get --raw /apis/metrics.k8s.io/v1beta1/namespaces/default/pods/deployment-example-c4d6f96db-jv6nm | jq '.containers[].usage.memory'
"2824Ki"

2 мегабайта.

Изменим наш HPA, и зададим не % от реквеста, а явное ограничение в 1024Ki — 1 мегабайт.

Вместо targetAverageUtilization используем targetAverageUtilization:

...
  metrics:
  - type: Resource
    resource:
      name: cpu
      targetAverageUtilization: 10
  - type: Resource
    resource:
      name: memory
      targetAverageValue: 1024Ki

Обновляем, проверяем:

kubectl get hpa hpa-example
NAME          REFERENCE                       TARGETS               MINPODS   MAXPODS   REPLICAS   AGE
hpa-example   Deployment/deployment-example   2551808/1Mi, 0%/10%   1         5         3          2m8s

Проверим значение из TARGETS — переведём в килобайты:

echo 2551808/1024 | bc
2492

И реальное потребление в поде:

kubectl get --raw /apis/metrics.k8s.io/v1beta1/namespaces/default/pods/deployment-example-c4d6f96db-fldl2 | jq '.containers[].usage.memory'
"2496Ki"

2492 ~= 2496Ki, окей, с этим разобрались, скейлинг тоже работает.

Custom Metrics

Memory metrics scaling

Помимо использование метрик, которые нам дают API-сервер и cAdvisor, мы можем использовать любые другие метрики, например — собираемые Prometheus.

Это могут быть метрики из Cloudwatch-експортёра, node_exporter, или метрики непосредственно из приложения.

Документация тут>>>.

Так как у нас используется Prometehus (см. Kubernetes: мониторинг с Prometheus и Kubernetes: мониторинг кластера с Prometheus Operator) — то используем его адаптер, вместе всё будет выглядеть примерно так:

Если попробовать обратиться к external и custom API-ендпоинтам сейчас — то получим ошибку:

kubectl get --raw /apis/custom.metrics.k8s.io/
Error from server (NotFound): the server could not find the requested resource
kubectl get --raw /apis/external.metrics.k8s.io/
Error from server (NotFound): the server could not find the requested resource

Устанавливаем адаптер из Helm-чарта:

helm install prometheus-adapter stable/prometheus-adapter
NAME: prometheus-adapter
LAST DEPLOYED: Sat Aug  8 13:27:36 2020
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/v1beta

Ждём пару минут, и проверяем API ещё раз:

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

Но почему в "resources":[] пусто?

Посмотрим логи:

kubectl logs -f prometheus-adapter-b8945f4d8-q5t6x
I0808 10:45:47.706771       1 adapter.go:94] successfully using in-cluster auth
E0808 10:45:47.752737       1 provider.go:209] unable to update list of all metrics: unable to fetch metrics for query "{__name__=~\"^container_.*\",container!=\"POD\",namespace!=\"\",pod!=\"\"}": Get http://prometheus.default.svc:9090/api/v1/series?match%5B%5D=%7B__name__%3D~%22%5Econtainer_.%2A%22%2Ccontainer%21%3D%22POD%22%2Cnamespace%21%3D%22%22%2Cpod%21%3D%22%22%7D&start=1596882347.736: dial tcp: lookup prometheus.default.svc on 172.20.0.10:53: no such host
I0808 10:45:48.032873       1 serving.go:306] Generated self-signed cert (/tmp/cert/apiserver.crt, /tmp/cert/apiserver.key)
...

Вот наша ошибка:

dial tcp: lookup prometheus.default.svc on 172.20.0.10:53: no such host

Проверим адрес нашего Prometheus:

kubectl exec -ti deployment-example-c4d6f96db-fldl2 curl prometheus-prometheus-oper-prometheus.monitoring.svc.cluster.local:9090/metrics | head -5
HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 2.0078e-05
go_gc_duration_seconds{quantile="0.25"} 3.8669e-05
go_gc_duration_seconds{quantile="0.5"} 6.956e-05

Окей — Prometheus доступен по адресу prometheus-prometheus-oper-prometheus.monitoring.svc.cluster.local:9090.

Открываем на редактирование деплоймент:

kubectl edit deploy prometheus-adapter

Обновляем prometheus-url:

...
    spec:
      affinity: {}
      containers:
      - args:
        - /adapter
        - --secure-port=6443
        - --cert-dir=/tmp/cert
        - --logtostderr=true
        - --prometheus-url=http://prometheus-prometheus-oper-prometheus.monitoring.svc.cluster.local:9090
...

Обновляем, и через минуту проверяем:

kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq .  |grep "pods/" | head -5
"name": "pods/node_load15",
"name": "pods/go_memstats_next_gc_bytes",
"name": "pods/coredns_forward_request_duration_seconds_count",
"name": "pods/rest_client_requests",
"name": "pods/node_ipvs_incoming_bytes",

Хорошо — метрики пошли, попробуем их применить в НРА.

Проверим наличие и значение memory_usage_bytes:

kubectl get --raw="/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/*/memory_usage_bytes" | jq .
{
"kind": "MetricValueList",
"apiVersion": "custom.metrics.k8s.io/v1beta1",
"metadata": {
"selfLink": "/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/%2A/memory_usage_bytes"
},
"items": [
{
"describedObject": {
"kind": "Pod",
"namespace": "default",
"name": "deployment-example-c4d6f96db-8tfnw",
"apiVersion": "/v1"
},
"metricName": "memory_usage_bytes",
"timestamp": "2020-08-08T11:18:53Z",
"value": "11886592",
"selector": null
},
...

Обновляем НРА:

apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: hpa-example
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: deployment-example
  minReplicas: 1
  maxReplicas: 5
  metrics:
  - type: Pods
    pods:
      metricName: memory_usage_bytes
      targetAverageValue: 1024000

Проверим значения в HPA сейчас:

kubectl get hpa hpa-example
NAME          REFERENCE                       TARGETS               MINPODS   MAXPODS   REPLICAS   AGE
hpa-example   Deployment/deployment-example   4694016/1Mi, 0%/10%   1         5         5          69m

Применяем:

kubectl apply -f hpa-example.yaml
horizontalpodautoscaler.autoscaling/hpa-example configured

Проверяем ещё раз:

kubectl get hpa hpa-example
NAME          REFERENCE                       TARGETS          MINPODS   MAXPODS   REPLICAS   AGE
hpa-example   Deployment/deployment-example   11853824/1024k   1         5         1          16s

Реплика всё ещё 1, смотрим логи:

...
43s         Normal    ScalingReplicaSet   Deployment                Scaled up replica set deployment-example-c4d6f96db to 1
16s         Normal    ScalingReplicaSet   Deployment                Scaled up replica set deployment-example-c4d6f96db to 4
1s          Normal    ScalingReplicaSet   Deployment                Scaled up replica set deployment-example-c4d6f96db to 5
16s         Normal    SuccessfulRescale   HorizontalPodAutoscaler   New size: 4; reason: pods metric memory_usage_bytes above target
1s          Normal    SuccessfulRescale   HorizontalPodAutoscaler   New size: 5; reason: pods metric memory_usage_bytes above target
...

И реплики заскейлились:

kubectl get hpa hpa-example
NAME          REFERENCE                       TARGETS             MINPODS   MAXPODS   REPLICAS   AGE
hpa-example   Deployment/deployment-example   6996787200m/1024k   1         5         5          104s

И всё хорошо, пока мы используем готовые метрики, которые уже есть в кластере, например memory_usage_bytes, собираемая cAdvisor со всех контейнеров.

Попробуем использовать более кастомную метрику, например — скейлить Gorush-сервер на основе его метрик, см. Kubernetes: запуск push-сервера Gorush в EKS за AWS LoadBalancer.

Application-based metrics scaling

У нас имеется Gorush-сервер, который выполняет отправку пуш-уведомлений на мобильные, у которого есть встроенный ендпонт /metrics, по которому доступны стандартные time-series метрики, которые можно использовать в Prometheus.

Используем Service, ConfigMap и Deployment:

apiVersion: v1
kind: Service
metadata:
  name: gorush
  labels:
    app: gorush
    tier: frontend
spec:
  selector:
    app: gorush
    tier: frontend
  type: ClusterIP
  ports:
  - name: gorush
    protocol: TCP
    port: 80
    targetPort: 8088
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: gorush-config
data:
  stat.engine: memory
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gorush
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gorush
      tier: frontend
  template:
    metadata:
      labels:
        app: gorush
        tier: frontend
    spec:
      containers:
      - image: appleboy/gorush
        name: gorush
        imagePullPolicy: Always
        ports:
        - containerPort: 8088
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8088
          initialDelaySeconds: 3
          periodSeconds: 3
        env:
        - name: GORUSH_STAT_ENGINE
          valueFrom:
            configMapKeyRef:
              name: gorush-config
              key: stat.engine

Создаём отдельный неймспейс:

kubectl create ns eks-dev-1-gorush
namespace/eks-dev-1-gorush created

Создаём сервисы:

kubectl -n eks-dev-1-gorush apply -f my-gorush.yaml
service/gorush created
configmap/gorush-config created
deployment.apps/gorush created

Проверяем поды:

kubectl -n eks-dev-1-gorush get pod
NAME                      READY   STATUS        RESTARTS   AGE
gorush-5c6775748b-6r54h   1/1     Running       0          83s

Его сервис:

kubectl -n eks-dev-1-gorush get svc
NAME     TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
gorush   ClusterIP   172.20.186.251   <none>        80/TCP    103s

Пробросим порт к поду:

kubectl -n eks-dev-1-gorush port-forward gorush-5c6775748b-6r54h 8088:8088
Forwarding from 127.0.0.1:8088 -> 8088
Forwarding from [::1]:8088 -> 8088

Проверяем метрики:

curl -s localhost:8088/metrics | grep gorush | head
HELP gorush_android_fail Number of android fail count
TYPE gorush_android_fail gauge
gorush_android_fail 0
HELP gorush_android_success Number of android success count
TYPE gorush_android_success gauge
gorush_android_success 0
HELP gorush_ios_error Number of iOS fail count
TYPE gorush_ios_error gauge
gorush_ios_error 0
HELP gorush_ios_success Number of iOS success count

Либо другим образом: зная имя Service, мы можем обратиться к нему напрямую.

Находим сервис:

kubectl -n eks-dev-1-gorush get svc
NAME     TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
gorush   ClusterIP   172.20.186.251   <none>        80/TCP    26m

Открываем прокси к API-серверу:

kubectl proxy --port=8080
Starting to serve on 127.0.0.1:8080

Обращаемся напрямую к сервису:

curl -sL localhost:8080/api/v1/namespaces/eks-dev-1-gorush/services/gorush:gorush/proxy/metrics | head
HELP go_gc_duration_seconds A summary of the GC invocation durations.
TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 9.194e-06
go_gc_duration_seconds{quantile="0.25"} 1.2092e-05
go_gc_duration_seconds{quantile="0.5"} 2.1812e-05
go_gc_duration_seconds{quantile="0.75"} 5.1794e-05
go_gc_duration_seconds{quantile="1"} 0.000145631
go_gc_duration_seconds_sum 0.001080551
go_gc_duration_seconds_count 32
HELP go_goroutines Number of goroutines that currently exist.

Kubernetes ServiceMonitor

Далее, нам надо добавить ServiceMonitor в Kubernetes, см. Создание Kubernetes ServiceMonitor.

Проверим метрики сейчас — пробросим порт к нашему Prometheus:

kk -n monitoring port-forward prometheus-prometheus-prometheus-oper-prometheus-0 9090:9090
Forwarding from [::1]:9090 -> 9090
Forwarding from 127.0.0.1:9090 -> 9090

Пробуем получить их:

curl "localhost:9090/api/v1/series?match[]=gorush_total_push_count&start=1597141864"
{"status":"success","data":[]}

"data":[] пустая — метрики в Prometheus не собираются.

Описываем ServiceMonitor:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    serviceapp: gorush-servicemonitor
    release: prometheus
  name: gorush-servicemonitor
  namespace: monitoring
spec:     
  endpoints:
  - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
    interval: 15s
    port: gorush
  namespaceSelector:
    matchNames: 
    - eks-dev-1-gorush
  selector: 
    matchLabels:
      app: gorush

Прим: The Prometheus resource includes a field called serviceMonitorSelector, which defines a selection of ServiceMonitors to be used. By default and before the version v0.19.0, ServiceMonitors must be installed in the same namespace as the Prometheus instance. With the Prometheus Operator v0.19.0 and above, ServiceMonitors can be selected outside the Prometheus namespace via the serviceMonitorNamespaceSelector field of the Prometheus resource.
См. Prometheus Operator

Создаём его:

kubectl apply -f ../../gorush-service-monitor.yaml
servicemonitor.monitoring.coreos.com/gorush-servicemonitor created

Проверяем его в таргетах:

UP, отлично.

И через пару минут ещё раз метрики:

curl "localhost:9090/api/v1/series?match[]=gorush_total_push_count&start=1597141864"
{"status":"success","data":[{"__name__":"gorush_total_push_count","endpoint":"gorush","instance":"10.3.35.14:8088","job":"gorush","namespace":"eks-dev-1-gorush","pod":"gorush-5c6775748b-6r54h","service":"gorush"}]}

Либо так:

curl -s localhost:9090/api/v1/label/__name__/values | jq | grep gorush
"gorush_android_fail",
"gorush_android_success",
"gorush_ios_error",
"gorush_ios_success",
"gorush_queue_usage",
"gorush_total_push_count",

Хорошо — метрики пошли, добавим HorizontalPodAutoscaler для этого деплоймента.

Проверяем, какие группы метрик доступны через Custom Metrics API:

kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq .  | grep "gorush"
"name": "services/gorush_android_success",
"name": "pods/gorush_android_fail",
"name": "namespaces/gorush_total_push_count",
"name": "namespaces/gorush_queue_usage",
"name": "pods/gorush_ios_success",
"name": "namespaces/gorush_ios_success",
"name": "jobs.batch/gorush_ios_error",
"name": "services/gorush_total_push_count",
"name": "jobs.batch/gorush_queue_usage",
"name": "pods/gorush_queue_usage",
"name": "jobs.batch/gorush_android_fail",
"name": "services/gorush_queue_usage",
"name": "services/gorush_ios_success",
"name": "jobs.batch/gorush_android_success",
"name": "jobs.batch/gorush_total_push_count",
"name": "pods/gorush_ios_error",
"name": "pods/gorush_total_push_count",
"name": "pods/gorush_android_success",
"name": "namespaces/gorush_android_success",
"name": "namespaces/gorush_android_fail",
"name": "namespaces/gorush_ios_error",
"name": "jobs.batch/gorush_ios_success",
"name": "services/gorush_ios_error",
"name": "services/gorush_android_fail",

Описываем HPA, который использует gorush_queue_usage из группы Pods:

apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: gorush-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: gorush
  minReplicas: 1
  maxReplicas: 5
  metrics:
  - type: Pods
    pods:
      metricName: gorush_total_push_count
      targetAverageValue: 2

С таким НРА поды должны заскейлится, как только gorush_total_push_count станет больше 2.

Создаём его:

kubectl -n eks-dev-1-gorush apply -f my-gorush.yaml
service/gorush unchanged
configmap/gorush-config unchanged
deployment.apps/gorush unchanged
horizontalpodautoscaler.autoscaling/gorush-hpa created

Проверим значение метрики сейчас:

kubectl get --raw="/apis/custom.metrics.k8s.io/v1beta1/namespaces/eks-dev-1-gorush/pods/*/gorush_total_push_count" | jq '.items[].value'
"0"

Проверяем НРА:

kubectl -n eks-dev-1-gorush get hpa
NAME         REFERENCE           TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
gorush-hpa   Deployment/gorush   0/1       1         5         1          17s

TARGETS 0/1, хорошо.

Шлём пуш:

curl -X POST a6095d18859c849889531cf08baa6bcf-531932299.us-east-2.elb.amazonaws.com/api/push -d '{"notifications":[{"tokens":["990004543798742"],"platform":2,"message":"Hello Android"}]}'
{"counts":1,"logs":[],"success":"ok"}

Ещё раз проверяем метрику:

kubectl get --raw="/apis/custom.metrics.k8s.io/v1beta1/namespaces/eks-dev-1-gorush/pods/*/gorush_total_push_count" | jq '.items[].value'
"1"

Один пуш отправлен.

Глянем НРА:

kubectl -n eks-dev-1-gorush get hpa
NAME         REFERENCE           TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
gorush-hpa   Deployment/gorush   1/2       1         5         1          9m42s

TARGETS 1/2, REPLICAS всё ещё одна — шлём второй пуш, и проверяем события:

kubectl -n eks-dev-1-gorush get events --watch
LAST SEEN   TYPE     REASON                 KIND                      MESSAGE
18s         Normal   Scheduled              Pod                       Successfully assigned eks-dev-1-gorush/gorush-5c6775748b-x8fjs to ip-10-3-49-200.us-east-2.compute.internal
17s         Normal   Pulling                Pod                       Pulling image "appleboy/gorush"
17s         Normal   Pulled                 Pod                       Successfully pulled image "appleboy/gorush"
17s         Normal   Created                Pod                       Created container gorush
17s         Normal   Started                Pod                       Started container gorush
18s         Normal   SuccessfulCreate       ReplicaSet                Created pod: gorush-5c6775748b-x8fjs
18s         Normal   SuccessfulRescale      HorizontalPodAutoscaler   New size: 2; reason: pods metric gorush_total_push_count above target
18s         Normal   ScalingReplicaSet      Deployment                Scaled up replica set gorush-5c6775748b to 2

И сам НРА теперь:

kubectl -n eks-dev-1-gorush get hpa
NAME         REFERENCE           TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
gorush-hpa   Deployment/gorush   3/2       1         5         2          10m

Отлично — скейлинг по метрике gorush_total_push_count работает.

Но есть нюанс: gorush_total_push_count — метрика куммулятивная, т.е. на графике Production-окружения будет выглядеть так:

В таком случае — наш НРА будет скейлить вечно.

Prometheus Adapter ConfigMapseriesQuery и metricsQuery

Что бы изменить это — создадим свою метрику.

Prometheus Adapter дня настроек использует ConfigMap:

kubectl get cm prometheus-adapter
NAME                 DATA   AGE
prometheus-adapter   1      46h

Который содержит файл config.yaml, пример файла тут>>>.

Сформируем PromQL-запрос, который вернёт нам значение пушей в секунду:

rate(gorush_total_push_count{instance="push.server.com:80",job="push-server"}[5m])

Обновляем ConfigMap, и добавляем свой запрос:

apiVersion: v1
data:
  config.yaml: |
    rules:
    - seriesQuery: '{__name__=~"gorush_total_push_count"}'
      seriesFilters: []
      resources:
        overrides:
          namespace:
            resource: namespace
          pod:
            resource: pod
      name:
        matches: ""
        as: "gorush_push_per_second"
      metricsQuery: rate(<<.Series>>{<<.LabelMatchers>>}[5m])

Проверяем:

kubectl get --raw="/apis/custom.metrics.k8s.io/v1beta1/namespaces/eks-dev-1-gorush/pods/*/gorush_push_per_second" | jq
Error from server (NotFound): the server could not find the metric gorush_push_per_second for pods

Пересоздаём под, что бы он обновил у себя ConfigMap (см. Kubernetes: ConfigMap и Secrets — auto-reload данных в подах):

kubectl delete pod prometheus-adapter-7c56787c5c-kllq6
pod "prometheus-adapter-7c56787c5c-kllq6" deleted

Проверяем:

kubectl get --raw="/apis/custom.metrics.k8s.io/v1beta1/namespaces/eks-dev-1-gorush/pods/*/gorush_push_per_second" | jq
{
"kind": "MetricValueList",
"apiVersion": "custom.metrics.k8s.io/v1beta1",
"metadata": {
"selfLink": "/apis/custom.metrics.k8s.io/v1beta1/namespaces/eks-dev-1-gorush/pods/%2A/gorush_push_per_second"
},
"items": [
{
"describedObject": {
"kind": "Pod",
"namespace": "eks-dev-1-gorush",
"name": "gorush-5c6775748b-6r54h",
"apiVersion": "/v1"
},
"metricName": "gorush_push_per_second",
"timestamp": "2020-08-11T12:28:03Z",
"value": "0",
"selector": null
},
...

Обновим НРА — используем gorush_push_per_second:

apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: gorush-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: gorush
  minReplicas: 1
  maxReplicas: 5
  metrics:
  - type: Pods
    pods:
      metricName: gorush_push_per_second
      targetAverageValue: 1m

Проверяем HPA:

kubectl -n eks-dev-1-gorush get hpa
NAME         REFERENCE           TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
gorush-hpa   Deployment/gorush   0/1m      1         5         1          68m

Проверяем события:

...
0s    Normal   SuccessfulRescale   HorizontalPodAutoscaler   New size: 4; reason: pods metric gorush_push_per_second above target
0s    Normal   ScalingReplicaSet   Deployment   Scaled up replica set gorush-5c6775748b to 4
...

И НРА теперь:

kubectl -n eks-dev-1-gorush get hpa
NAME         REFERENCE           TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
gorush-hpa   Deployment/gorush   11m/1m    1         5         5          70m

Готово.

Ссылки по теме