Kubernetes для самых маленьких. Часть 32: Как работают метрики и автомасштабирование?

Все части серии «Kubernetes для самых маленьких»

В прошлой части мы разбирали сбои по событиям и логам — по тому, что осталось в кластере на момент вопроса. Сегодня — о числах, собранных заранее: сколько CPU и памяти потребляют поды на самом деле, как Kubernetes по этим числам сам меняет число реплик и где хранить историю метрик, чтобы через неделю ответить, что было ночью. Начнём с встроенного контура — kubelet, metrics-server, kubectl top и HorizontalPodAutoscaler, — затем поставим Prometheus с Grafana и Alertmanager одним Helm-чартом (часть 27), научим его снимать метрики с hello и напишем первые правила алертов. Requests и limits из части 10 здесь понадобятся постоянно: автомасштабирование считает проценты именно от них.

Неймспейс demo остаётся таким, каким его оставила часть 30: под уровнем restricted, hello работает от UID 10001 без capabilities. Это повлияет на то, откуда мы будем давать нагрузку.


Два контура метрик

В кластере есть два независимых пути, по которым идут метрики, и путать их — частая ошибка:

 Контур ресурсов (встроенный)                    Полный контур (мониторинг)
 ────────────────────────────                    ──────────────────────────

 kubelet на каждом узле                          экспортёры: kubelet, node-exporter,
   /metrics/resource                             kube-state-metrics, приложения /metrics
        │  каждые 15 с                                   │  каждые 30 с (pull)
        ▼                                                ▼
 metrics-server                                  Prometheus
   две последние точки в памяти                    TSDB на диске, недели истории
        │                                                │
        ▼                                                ├──► Grafana: графики
 kube-apiserver                                          ├──► правила → Alertmanager
   /apis/metrics.k8s.io (aggregation)                    └──► адаптер → custom/external
        │                                                     metrics API (по желанию)
        ├──► kubectl top
        ├──► HorizontalPodAutoscaler
        └──► VerticalPodAutoscaler

Контур ресурсов — минимальный: только CPU и память подов и узлов, только «сейчас», без истории. Он нужен самому Kubernetes для автомасштабирования и человеку для kubectl top. Полный контур — система мониторинга: тысячи метрик, хранение на диске, запросы, графики и алерты. Один не заменяет другой: Prometheus не нужен HPA для масштабирования по CPU, а metrics-server бесполезен для вопроса «что было вчера».


Откуда kubelet берёт цифры

Источник обоих контуров — kubelet. Он получает статистику контейнеров от containerd по CRI и из cgroups, которые сам создал для подов (часть 10): cpu.stat (usage_usec — накопленное время CPU) и memory.current, memory.stat (из них считается working set — используемая память минус неактивный файловый кэш, который ядро может вытеснить). Внутри kubelet встроен cAdvisor — библиотека Google, которая обходит cgroups и собирает по ним подробную статистику.

Наружу kubelet отдаёт это на своём порту 10250 по нескольким адресам:

Адрес Что там Кто читает
/metrics/resource минимальный набор в формате Prometheus: CPU и working set по узлу, подам и контейнерам metrics-server
/stats/summary Summary API — то же и больше (сеть, файловые системы, тома) в JSON старые версии metrics-server, вспомогательные утилиты
/metrics/cadvisor всё, что собрал cAdvisor: сотни метрик по контейнерам Prometheus
/metrics/probes результаты проб liveness, readiness и startup Prometheus
/metrics метрики самого kubelet: время операций, PLEG, тома (kubelet_volume_stats_*) Prometheus

Заглянуть можно через прокси API-сервера, не заходя на узел:

$ kubectl get --raw /api/v1/nodes/k8s-node-2/proxy/metrics/resource | grep -E 'hello-74d65597f7-5lfft|^node_'
container_cpu_usage_seconds_total{container="hello",namespace="demo",pod="hello-74d65597f7-5lfft"} 0.396541 1793735197827
container_memory_working_set_bytes{container="hello",namespace="demo",pod="hello-74d65597f7-5lfft"} 6.623232e+06 1793735197827
container_start_time_seconds{container="hello",namespace="demo",pod="hello-74d65597f7-5lfft"} 1.7937347742225347e+09
container_swap_limit_bytes{container="hello",namespace="demo",pod="hello-74d65597f7-5lfft"} 0 1793735197827
container_swap_usage_bytes{container="hello",namespace="demo",pod="hello-74d65597f7-5lfft"} 0 1793735197827
node_cpu_usage_seconds_total 13472.803 1793735195256
node_memory_working_set_bytes 1.55904e+09 1793735195256
node_swap_usage_bytes 0 1793735195256
pod_cpu_usage_seconds_total{namespace="demo",pod="hello-74d65597f7-5lfft"} 0.394523 1793735193027
pod_memory_working_set_bytes{namespace="demo",pod="hello-74d65597f7-5lfft"} 6.606848e+06 1793735193027
pod_swap_usage_bytes{namespace="demo",pod="hello-74d65597f7-5lfft"} 0 1793735193027

Последнее число в строке — метка времени измерения в миллисекундах. CPU здесь — счётчик: секунды процессорного времени с момента старта контейнера. Потребление «в ядрах» получается только как разность двух измерений, поделённая на интервал между ними. Следующий запрос показал 0.444832 с меткой 1793735209621, то есть за 11,8 секунды: (0,444832 − 0,396541) / 11,8 ≈ 0,004 ядра, или 4m. Память — мгновенное значение в байтах: здесь 6,6 МБ.


metrics-server

metrics-server — небольшой компонент из kubernetes-sigs. Каждые 15 секунд он опрашивает /metrics/resource у всех kubelet'ов, держит в памяти две последние точки на каждый объект, вычисляет из них CPU и отдаёт результат как API metrics.k8s.io/v1beta1. Отдельный объект APIService регистрирует этот API в слое агрегации kube-apiserver: запросы к /apis/metrics.k8s.io/... API-сервер не обрабатывает сам, а проксирует в сервис metrics-server.

Чего metrics-server не делает, по словам его же документации:

  • не хранит историю — только последнее значение; перезапуск пода metrics-server ничего не теряет, потому что терять нечего;
  • не подходит для мониторинга — не надо подключать к нему Prometheus или графики, для этого есть /metrics/resource и /metrics/cadvisor у kubelet;
  • не точен в деталях — значение CPU усреднено за окно, короткие пики в нём сглажены;
  • не знает других метрик, кроме CPU и памяти.

Версии 0.9.x поддерживают Kubernetes 1.34 и новее. Ставится официальным манифестом с явной версией:

$ kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.9.1/components.yaml
serviceaccount/metrics-server created
clusterrole.rbac.authorization.k8s.io/system:aggregated-metrics-reader created
clusterrole.rbac.authorization.k8s.io/system:metrics-server created
rolebinding.rbac.authorization.k8s.io/metrics-server-auth-reader created
clusterrolebinding.rbac.authorization.k8s.io/metrics-server:system:auth-delegator created
clusterrolebinding.rbac.authorization.k8s.io/system:metrics-server created
service/metrics-server created
deployment.apps/metrics-server created
apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io created

В манифесте у Deployment аргументы --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname (ходить к kubelet по IP узла), --kubelet-use-node-status-port (порт брать из статуса узла) и --metric-resolution=15s. Флага --kubelet-insecure-tls там нет: metrics-server проверяет сертификат, которым kubelet отвечает на порту 10250, по CA кластера. На нашем кластере проверка проходит сразу — ещё в части 4 мы включили serverTLSBootstrap: true и вручную одобрили CSR kubelet-serving, так что каждый kubelet отвечает сертификатом, подписанным CA кластера. Через полминуты metrics-server готов:

$ kubectl get apiservice v1beta1.metrics.k8s.io
NAME                     SERVICE                      AVAILABLE   AGE
v1beta1.metrics.k8s.io   kube-system/metrics-server   True        32s

Почему не --kubelet-insecure-tls

На кластере kubeadm, поставленном без serverTLSBootstrap, kubelet сам генерирует для порта 10250 самоподписанный сертификат: издатель — он сам (CN=k8s-node-2-ca@1793733646), в SAN только имя узла, IP-адреса нет. Проверку по CA кластера такой сертификат не проходит. Увидеть это можно на одном узле, убрав на время поле из /var/lib/kubelet/config.yaml: в журнале metrics-server появляется ошибка, а узел в kubectl top теряет цифры.

$ kubectl logs -n kube-system deploy/metrics-server | grep -m1 'Failed to scrape'
E1103 19:20:50.298744       1 scraper.go:149] "Failed to scrape node" err="Get \"https://192.168.10.21:10250/metrics/resource\": tls: failed to verify certificate: x509: cannot validate certificate for 192.168.10.21 because it doesn't contain any IP SANs" node="k8s-node-2"

$ kubectl top nodes
NAME           CPU(cores)   CPU(%)      MEMORY(bytes)   MEMORY(%)
k8s-master-1   104m         5%          1927Mi          58%
k8s-node-1     40m          2%          735Mi           22%
k8s-node-2     <unknown>    <unknown>   <unknown>       <unknown>

APIService при этом остаётся True: другие узлы отвечают. Первый ответ в поиске — добавить metrics-server флаг --kubelet-insecure-tls. Это выключает проверку сертификата kubelet совсем. Чем это плохо: при каждом опросе metrics-server предъявляет kubelet'у токен своего ServiceAccount, и без проверки сертификата он предъявит его кому угодно, кто сумеет перехватить трафик в сети узлов. Документация metrics-server прямо называет этот флаг вариантом для тестов. Правильно — дать kubelet'ам сертификаты, подписанные CA кластера, как это сделано на нашем стенде.

Подписанные сертификаты kubelet

С serverTLSBootstrap: true kubelet запрашивает серверный сертификат через API certificates.k8s.io — так же, как при присоединении к кластеру он запросил клиентский: создаёт объект CertificateSigningRequest (CSR) с подписантом kubernetes.io/kubelet-serving и перечисляет в нём свои имена и адреса. Клиентские CSR узлов kube-controller-manager одобряет автоматически, а серверные — нет: Kubernetes не может сам убедиться, что узел вправе получить сертификат на запрошенные имена и адреса. Поэтому в части 4 мы перед одобрением смотрели, что именно просит каждый узел. После одобрения kube-controller-manager подписывает сертификат CA кластера, kubelet забирает его в /var/lib/kubelet/pki/kubelet-server-current.pem и отвечает им.

Если кластер уже работает без этого поля, его включают в двух местах. ConfigMap kubelet-config — источник, из которого kubeadm берёт конфигурацию для новых узлов и при обновлениях:

$ kubectl edit cm kubelet-config -n kube-system
data:
  kubelet: |
    apiVersion: kubelet.config.k8s.io/v1beta1
    kind: KubeletConfiguration
    serverTLSBootstrap: true
    ...

На каждом существующем узле то же поле дописывают в /var/lib/kubelet/config.yaml, перезапускают kubelet и одобряют появившиеся CSR так же, как при установке кластера:

user@k8s-node-1:~$ echo 'serverTLSBootstrap: true' | sudo tee -a /var/lib/kubelet/config.yaml
user@k8s-node-1:~$ sudo systemctl restart kubelet

Сертификат живёт год и продлевается по тому же пути: kubelet заранее создаст новый CSR, и его снова нужно одобрить — иначе, когда старый сертификат истечёт, metrics-server перестанет снимать метрики с этого узла. Чтобы не забыть, одобрение автоматизируют контроллером вроде kubelet-csr-approver — он одобряет только запросы, где имена и IP соответствуют узлу. Контроллер, который одобряет всё подряд, хуже ручного одобрения: любой, у кого есть клиентский сертификат узла, получит серверный сертификат на любое имя.

Те же подписанные сертификаты позволяют закрыть и вторую дыру того же рода: по умолчанию kube-apiserver в kubeadm не проверяет сертификат kubelet, когда проксирует kubectl logs и exec. Это видно и на опыте выше: kubectl get --raw /api/v1/nodes/k8s-node-2/proxy/... с самоподписанным сертификатом продолжает работать. Раз kubelet'ы отвечают сертификатами от CA кластера, API-серверу можно задать --kubelet-certificate-authority с этим CA. Обратное направление — API-сервер к metrics-server — в манифесте помечено insecureSkipTLSVerify: true у APIService; аккуратная замена — выпустить metrics-server сертификат через cert-manager из части 19 и прописать caBundle.

kubectl top

$ kubectl top nodes
NAME           CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)
k8s-master-1   145m         7%       1853Mi          56%
k8s-node-1     146m         7%       728Mi           22%
k8s-node-2     65m          3%       1492Mi          45%

$ kubectl top pods -n demo --containers
POD                      NAME    CPU(cores)   MEMORY(bytes)
hello-74d65597f7-5lfft   hello   5m           6Mi
hello-74d65597f7-8vkl6   hello   4m           6Mi
hello-74d65597f7-qqdxp   hello   4m           6Mi

Проценты у узлов считаются от allocatable — того, что kubelet готов отдать подам, а не от всей памяти машины. Память — working set, поэтому цифры не совпадут с free на узле: free считает по /proc/meminfo, а working set — по cgroup, и в него входит активная часть файлового кэша. Именно working set сравнивается с лимитом при OOM и с порогами выселения kubelet, поэтому для решений о requests и limits смотреть нужно на него.


HorizontalPodAutoscaler

HorizontalPodAutoscaler (HPA) — объект autoscaling/v2, который меняет replicas у Deployment, StatefulSet или любого ресурса с подресурсом scale. Его контроллер живёт в kube-controller-manager и раз в 15 секунд (--horizontal-pod-autoscaler-sync-period) для каждого HPA делает одно и то же: получает метрики, считает желаемое число реплик и записывает его в spec.replicas цели. Дальше работает обычный контроллер Deployment из части 7.

Алгоритм

desiredReplicas = ceil( currentReplicas × currentMetricValue / desiredMetricValue )

Три пода при цели 70 % CPU и фактических 140 % дают ceil(3 × 140 / 70) = 6. При 75 % — ceil(3 × 75 / 70) = ceil(3,21) = 4, но масштабирования не будет: если отношение текущего к целевому отличается от 1 меньше чем на допуск (10 % по умолчанию, --horizontal-pod-autoscaler-tolerance), HPA ничего не делает. Допуск можно задать и в самом HPA — полем tolerance в behavior.scaleUp и behavior.scaleDown: возможность появилась в 1.33, с 1.35 она в статусе бета и включена по умолчанию, в 1.37 стала стабильной.

Детали, которые объясняют «странное» поведение:

  • Проценты — от requests. Утилизация 70 % означает 70 % от resources.requests.cpu, а не от ядра и не от лимита. Если хотя бы у одного контейнера пода нет request по этому ресурсу, HPA не может посчитать утилизацию и бездействует, сообщая об этом в conditions. Это ещё одна причина всегда задавать requests.
  • Неготовые поды при масштабировании вверх по CPU считаются потребляющими 0 %, а поды без метрик — 0 % при росте и 100 % при снижении. В первые 5 минут после старта пода (--horizontal-pod-autoscaler-cpu-initialization-period) его CPU учитывается, только если замер сделан, когда под уже был готов: всплеск CPU при старте Go- или Java-приложения не должен вызывать новый рост.
  • Несколько метрик — HPA считает желаемое число по каждой и берёт максимум.
  • replicas в манифесте. Если HPA управляет Deployment, поле replicas из манифеста Deployment убирают: иначе каждый kubectl apply будет возвращать число реплик к записанному в файле, споря с HPA.

Типы метрик

Тип Откуда Пример
Resource metrics.k8s.io (metrics-server) CPU или память в среднем по подам: Utilization в процентах от requests или AverageValue в абсолютных единицах
ContainerResource metrics.k8s.io то же, но по одному контейнеру пода — чтобы sidecar не размывал среднее
Pods custom.metrics.k8s.io метрика, у которой есть значение на каждый под: запросов в секунду на под
Object custom.metrics.k8s.io метрика одного объекта кластера: запросов в секунду на Ingress
External external.metrics.k8s.io метрика вне кластера: длина очереди в брокере сообщений

API custom.metrics.k8s.io и external.metrics.k8s.io в кластере сами по себе не существуют: их, как metrics-server, регистрирует в слое агрегации отдельный адаптер — компонент, который переводит запросы HPA в запросы к системе мониторинга. Для Prometheus это prometheus-adapter (развивается медленно) или KEDA, о которой ниже.

behavior

Поле behavior задаёт, как быстро HPA может менять число реплик, отдельно вверх и вниз. Значения по умолчанию:

behavior:
  scaleDown:
    stabilizationWindowSeconds: 300
    policies:
    - type: Percent
      value: 100
      periodSeconds: 15
  scaleUp:
    stabilizationWindowSeconds: 0
    policies:
    - type: Percent
      value: 100
      periodSeconds: 15
    - type: Pods
      value: 4
      periodSeconds: 15
    selectPolicy: Max

Окно стабилизации — HPA помнит рекомендации за последние N секунд и при снижении берёт из них наибольшую. Нагрузка упала на минуту и вернулась — реплики не тронуты. По умолчанию вниз окно 5 минут, вверх — ноль: расти можно сразу. Политики ограничивают шаг: вверх — не больше чем удвоение или +4 пода за 15 секунд (из двух берётся больший, selectPolicy: Max; с трёх реплик — сразу до семи), вниз — до всего лишнего сразу. Для сервиса, который долго прогревается или держит долгие соединения, резкое снижение вредно, и политику вниз делают мягче — например, не больше одного пода в минуту.

Пример на hello

В манифесте hello из части 30 ресурсов нет, а без requests.cpu HPA считать не из чего. Зададим их и начнём с трёх реплик:

$ kubectl set resources deployment hello -n demo --requests=cpu=100m,memory=32Mi --limits=memory=64Mi
deployment.apps/hello resource requirements updated
$ kubectl scale deployment hello -n demo --replicas=3
deployment.apps/hello scaled

HPA держит среднюю загрузку CPU на 70 % от request, от трёх до десяти реплик, и убирает поды не быстрее одного в минуту:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: hello
  namespace: demo
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: hello
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Pods
        value: 1
        periodSeconds: 60

Нагрузку дадут поды с бесконечным циклом запросов к сервису. В demo их не запустить: busybox без securityContext уровень restricted отклонит (pods "..." is forbidden: violates PodSecurity "restricted:latest"). Поэтому генератор живёт в неймспейсе default и обращается к сервису по имени hello.demo (часть 16). Сначала два пода:

$ kubectl apply -f hello-hpa.yaml
horizontalpodautoscaler.autoscaling/hello created
$ kubectl create deployment load -n default --image=busybox:1.37 --replicas=2 -- \
    /bin/sh -c 'while true; do wget -q -O /dev/null http://hello.demo; done'
deployment.apps/load created

$ kubectl get hpa hello -n demo --watch
NAME    REFERENCE          TARGETS       MINPODS   MAXPODS   REPLICAS   AGE
hello   Deployment/hello   cpu: 0%/70%   3         10        3          75s
hello   Deployment/hello   cpu: 15%/70%   3         10        3          105s
hello   Deployment/hello   cpu: 59%/70%   3         10        3          2m1s
hello   Deployment/hello   cpu: 74%/70%   3         10        3          2m16s
hello   Deployment/hello   cpu: 75%/70%   3         10        3          2m31s
hello   Deployment/hello   cpu: 74%/70%   3         10        3          3m1s
hello   Deployment/hello   cpu: 76%/70%   3         10        3          6m16s

Целых семь минут цель превышена, а реплик по-прежнему три. Это и есть допуск в деле: 75 / 70 = 1,07, отличие от единицы меньше 10 %, поэтому HPA ничего не делает, хотя формула дала бы четыре. Дадим нагрузку посильнее — шесть подов:

$ kubectl delete deployment load -n default
$ kubectl create deployment load -n default --image=busybox:1.37 --replicas=6 -- \
    /bin/sh -c 'while true; do wget -q -O /dev/null http://hello.demo; done'

$ kubectl get hpa hello -n demo --watch
NAME    REFERENCE          TARGETS       MINPODS   MAXPODS   REPLICAS   AGE
hello   Deployment/hello   cpu: 0%/70%   3         10        3          22m
hello   Deployment/hello   cpu: 22%/70%   3         10        3          22m
hello   Deployment/hello   cpu: 134%/70%   3         10        3          22m
hello   Deployment/hello   cpu: 136%/70%   3         10        6          23m
hello   Deployment/hello   cpu: 126%/70%   3         10        6          23m
hello   Deployment/hello   cpu: 75%/70%    3         10        6          23m
hello   Deployment/hello   cpu: 48%/70%    3         10        6          23m
hello   Deployment/hello   cpu: 28%/70%    3         10        6          24m

ceil(3 × 136 / 70) = ceil(5,83) = 6: в рамках политик по умолчанию, поэтому одним шагом. Строка 126 % при шести репликах — запаздывание: новые поды ещё не вошли в замер. Дальше средняя загрузка ходит между 20 и 60 % и ниже цели, но реплики не снижаются: окно стабилизации держит наибольшую рекомендацию за пять минут, а нагрузка всё ещё идёт. Убираем её:

$ kubectl delete deployment load -n default
$ kubectl events -n demo --for hpa/hello
LAST SEEN   TYPE     REASON              OBJECT                          MESSAGE
16m         Normal   SuccessfulRescale   HorizontalPodAutoscaler/hello   New size: 6; reason: cpu resource utilization (percentage of request) above target
6m13s       Normal   SuccessfulRescale   HorizontalPodAutoscaler/hello   New size: 5; reason: All metrics below target
5m13s       Normal   SuccessfulRescale   HorizontalPodAutoscaler/hello   New size: 4; reason: All metrics below target
4m12s       Normal   SuccessfulRescale   HorizontalPodAutoscaler/hello   New size: 3; reason: All metrics below target

Первое снижение — через 5 минут 47 секунд после снятия нагрузки (окно стабилизации плюс последний высокий замер), затем по одному поду в минуту, как задано в политике. Пока окно не прошло, в conditions видно AbleToScale с причиной ScaleDownStabilized и сообщением recent recommendations were higher than current one, applying the highest recent recommendation. Полное состояние HPA — в status:

$ kubectl get hpa hello -n demo -o yaml | sed -n '/^status:/,$p'
status:
  conditions:
  - lastTransitionTime: "2026-11-03T18:14:13Z"
    message: recommended size matches current size
    reason: ReadyForNewScale
    status: "True"
    type: AbleToScale
  - lastTransitionTime: "2026-11-03T18:14:13Z"
    message: the HPA was able to successfully calculate a replica count from cpu resource
      utilization (percentage of request)
    reason: ValidMetricFound
    status: "True"
    type: ScalingActive
  - lastTransitionTime: "2026-11-03T18:46:50Z"
    message: the desired replica count is less than the minimum replica count
    reason: TooFewReplicas
    status: "True"
    type: ScalingLimited
  currentMetrics:
  - resource:
      current:
        averageUtilization: 1
        averageValue: 1m
      name: cpu
    type: Resource
  currentReplicas: 3
  desiredReplicas: 3
  lastScaleTime: "2026-11-03T18:48:51Z"

ScalingActive=False — HPA не может получить метрики: нет metrics-server, у контейнера нет request, адаптер не отвечает. ScalingLimited=True — упёрлись в minReplicas или maxReplicas.

Другие автомасштабировщики

VerticalPodAutoscaler (VPA) — отдельный проект из репозитория kubernetes/autoscaler: смотрит на фактическое потребление подов и рекомендует или сам выставляет им requests и limits. С тех пор как изменение ресурсов пода на месте (in-place resize) стало стабильным в Kubernetes 1.35, VPA умеет в режиме InPlaceOrRecreate менять ресурсы без пересоздания пода там, где это возможно. По CPU одновременно с HPA его не используют: они начнут спорить. Полезный безопасный режим — updateMode: "Off", когда VPA только пишет рекомендации, а человек решает.

KEDA (Kubernetes Event-Driven Autoscaling) — оператор, который масштабирует по внешним событиям: длине очереди RabbitMQ или Kafka, результату запроса к Prometheus, расписанию. Объект ScaledObject описывает источник, KEDA реализует для него external.metrics.k8s.io и создаёт обычный HPA; сверх HPA он умеет масштабировать до нуля реплик и обратно.

Cluster Autoscaler — меняет не число подов, а число узлов: добавляет машину, когда поды висят в Pending из-за нехватки ресурсов, и убирает недогруженную. Он работает через API облачного провайдера или платформы виртуализации, которая умеет создавать машины по запросу. В кластере на собственных серверах, как наш, ему нечего вызывать, поэтому здесь его нет; ёмкость планируют по метрикам заранее.


Prometheus

Prometheus — система мониторинга и база временных рядов, проект CNCF. Устройство в четырёх пунктах.

Модель данных. Временной ряд — это имя метрики плюс набор меток (пар ключ — значение), и последовательность точек «время — число» для этого сочетания. container_cpu_usage_seconds_total{namespace="demo", pod="hello-…-5lfft", container="hello"} — один ряд; другой под — другой ряд. Каждое уникальное сочетание меток — новый ряд в базе, поэтому в метки не кладут то, у чего неограниченное число значений (идентификаторы пользователей, URL с параметрами): это называется кардинальностью, и она главный враг памяти Prometheus.

Pull. Prometheus сам ходит к целям по HTTP за /metrics с заданным интервалом и забирает текущие значения в текстовом формате. Список целей он строит service discovery: для Kubernetes — по API кластера, следя за подами, сервисами, эндпоинтами и узлами. Если цель не ответила, ряд up{...} для неё становится 0 — по нему отличают «приложение молчит» от «приложение говорит, что всё плохо».

Экспортёры. Не каждая программа умеет отдавать /metrics. Экспортёр — отдельный процесс, который снимает состояние с чего-то другого и переводит в формат Prometheus: node-exporter читает /proc и /sys узла (CPU, память, диски, сеть, файловые системы), kube-state-metrics смотрит на объекты API и превращает их состояние в метрики (сколько реплик желаемо и готово, причина последнего завершения контейнера, фаза пода, ёмкость PVC). Приложения, как hello, отдают свои метрики сами.

Хранилище. Локальная база TSDB: свежие точки — в памяти и в журнале предзаписи (WAL) на диске, каждые два часа они сбрасываются в неизменяемый блок (каталог с чанками, индексом и файлом удалений), блоки потом укрупняются. Точка занимает на диске 1–2 байта благодаря сжатию.

PromQL за пять минут

Типов метрик четыре: counter (только растёт, сбрасывается при рестарте: запросы, секунды CPU), gauge (любое текущее значение: память, длина очереди), histogram (распределение по корзинам _bucket{le="..."} плюс _sum и _count: время ответа) и summary (квантили, посчитанные в самом приложении).

container_memory_working_set_bytes{namespace="demo", container="hello"}

Это мгновенный вектор — по одному последнему значению для каждого подходящего ряда. Счётчики в таком виде бесполезны, их смотрят через скорость изменения за окно — вектор диапазона [5m]:

rate(container_cpu_usage_seconds_total{namespace="demo", container="hello"}[5m])

rate — средняя скорость роста в секунду за 5 минут, с поправкой на сбросы счётчика; для секунд CPU это и есть ядра. increase(...[1h]) — прирост за час. Агрегация убирает лишние метки:

sum by (pod) (rate(container_cpu_usage_seconds_total{namespace="demo"}[5m]))

histogram_quantile(0.95, sum by (le) (rate(apiserver_request_duration_seconds_bucket{verb="GET"}[5m])))

Первое — CPU по подам. Второе — 95-й перцентиль времени ответа kube-apiserver на запросы GET по его гистограмме; на нашем стенде это около 0,07 секунды. Окно в rate должно вмещать хотя бы четыре интервала опроса: при 30 секундах — от двух минут.


kube-prometheus-stack

Собрать всё это руками можно, но долго. kube-prometheus-stack — Helm-чарт сообщества Prometheus, который ставит готовый набор:

  • Prometheus Operator — оператор (часть 28) с набором CRD: Prometheus и Alertmanager описывают сами серверы (оператор превращает их в StatefulSet), ServiceMonitor и PodMonitor — что опрашивать, PrometheusRule — правила записи и алертов, AlertmanagerConfig — маршруты уведомлений. Оператор следит за этими объектами и генерирует из них конфигурацию Prometheus, так что цели и правила добавляются манифестами рядом с приложением;
  • Prometheus с готовыми ServiceMonitor'ами для kubelet и cAdvisor, API-сервера, CoreDNS, kube-controller-manager, kube-scheduler, etcd и kube-proxy;
  • kube-state-metrics и node-exporter (DaemonSet на каждом узле);
  • Alertmanager — сервис, который получает сработавшие алерты от Prometheus, группирует их, подавляет зависимые (узел недоступен — не нужно отдельно о каждом его поде), выдерживает паузы повторов и рассылает по каналам: почта, мессенджеры, webhook;
  • Grafana — веб-интерфейс для графиков и дашбордов поверх Prometheus и других источников, с готовыми дашбордами по кластеру (в этой версии чарта их 29);
  • 155 правил алертов в 35 объектах PrometheusRule из проекта kubernetes-mixin и других: KubePodCrashLooping, KubePersistentVolumeFillingUp, CPUThrottlingHigh, KubeNodeNotReady и так далее.

Хранилище: локальный том, не NFS

Prometheus пишет в TSDB постоянно, держит файлы открытыми через mmap и рассчитывает на POSIX-семантику файловой системы. Его документация прямо говорит: файловые системы, не соответствующие POSIX, не поддерживаются, возможна неисправимая порча данных, и NFS не поддерживается. Поэтому тома из csi-driver-nfs (часть 23) для Prometheus не годятся — ровно по той же причине, по которой на NFS не кладут базы данных. Нужен локальный диск узла: StorageClass local-path из части 25 (local-path-provisioner — контроллер, который создаёт том как каталог на узле, где запустился под) или статический локальный PV на отдельном диске. Если вы убрали его после той части, он ставится снова одним манифестом:

$ kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.37/deploy/local-path-storage.yaml

Локальный том привязывает под к узлу: если k8s-node-2 выключится, Prometheus будет ждать его возвращения. Для мониторинга небольшого кластера это приемлемо; где нет — ставят две реплики (replicas: 2 в ресурсе Prometheus), каждая со своим локальным томом на своём узле и независимым опросом тех же целей.

Сколько места. Формула из документации: объём = срок хранения в секундах × точек в секунду × байт на точку. На нашем стенде из трёх узлов этот чарт через час работы даёт 76 тысяч рядов и 2 100 точек в секунду. За 15 дней: 1 296 000 × 2 100 × 2 ≈ 5,4 ГБ, плюс WAL и запас на укрупнение блоков и рост кластера — тома на 20 ГБ хватит. Реальные цифры ваш кластер покажет сам: prometheus_tsdb_head_series и sum(rate(prometheus_tsdb_head_samples_appended_total[5m])). Срок хранения задают двумя ограничениями сразу: retention (время) и retentionSize (объём, процентов на 10–15 меньше тома), — что наступит раньше. Памяти Prometheus на стенде занял около 370 МиБ; это и есть ориентир для requests.

Установка

node-exporter монтирует каталоги узла и работает в его сетевом пространстве имён, поэтому неймспейс мониторинга сразу помечаем уровнем privileged (часть 30). Пароль администратора Grafana — в Secret, созданном заранее, а не в values (секрет в values окажется в истории релиза Helm и в Git):

$ kubectl create namespace monitoring
$ kubectl label namespace monitoring pod-security.kubernetes.io/enforce=privileged
$ kubectl create secret generic grafana-admin -n monitoring \
    --from-literal=admin-user=admin --from-literal=admin-password="$(openssl rand -base64 24)"

Минимальные values:

# kps-values.yaml
fullnameOverride: kps

prometheus:
  prometheusSpec:
    retention: 15d
    retentionSize: 17GB
    # брать ServiceMonitor, PodMonitor и PrometheusRule из всех неймспейсов,
    # а не только с меткой release: kps
    serviceMonitorSelectorNilUsesHelmValues: false
    podMonitorSelectorNilUsesHelmValues: false
    ruleSelectorNilUsesHelmValues: false
    # искать цели через EndpointSlice, а не через устаревший API Endpoints
    serviceDiscoveryRole: EndpointSlice
    resources:
      requests: { cpu: 200m, memory: 1Gi }
      limits: { memory: 2Gi }
    storageSpec:
      volumeClaimTemplate:
        spec:
          storageClassName: local-path
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 20Gi

alertmanager:
  alertmanagerSpec:
    storage:
      volumeClaimTemplate:
        spec:
          storageClassName: local-path
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 1Gi

grafana:
  admin:
    existingSecret: grafana-admin

# наши kubelet'ы отвечают сертификатами от CA кластера, проверку можно включить
kubelet:
  serviceMonitor:
    tlsConfig:
      insecureSkipVerify: false

Последний блок — та же тема, что и с metrics-server. По умолчанию чарт опрашивает kubelet'ы с insecureSkipVerify: true, потому что на обычном кластере kubeadm их сертификаты самоподписанные. У нас они подписаны CA кластера, и с проверкой все девять целей kubelet (по три адреса на узел) остаются up: CA чарт берёт из ConfigMap kube-root-ca.crt, который есть в каждом неймспейсе.

Чарт ставится из OCI-реестра одной командой с явной версией (подробно Helm — в части 27):

$ helm install kps oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack \
    --version 92.17.1 -n monitoring -f kps-values.yaml
...
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
$ kubectl get pods -n monitoring
NAME                                      READY   STATUS    RESTARTS        AGE
alertmanager-kps-alertmanager-0           2/2     Running   0               8m34s
kps-grafana-6d697c9dd6-x947c              3/3     Running   2 (2m14s ago)   10m
kps-kube-state-metrics-67c8fc75bb-bn5cl   1/1     Running   0               10m
kps-operator-5f77976978-76cft             1/1     Running   0               10m
kps-prometheus-node-exporter-6b98q        1/1     Running   0               10m
kps-prometheus-node-exporter-7wzp5        1/1     Running   0               10m
kps-prometheus-node-exporter-t5rkg        1/1     Running   0               10m
prometheus-kps-prometheus-0               2/2     Running   0               8m32s

Это Prometheus 3.15, Alertmanager 0.34, оператор 0.94 и Grafana 13.3. Два перезапуска Grafana на стенде — не авария: на узлах с двумя vCPU первый старт занимает несколько минут, и контейнер дважды не укладывается в проверку живучести (Container grafana failed liveness probe, will be restarted). В поде Prometheus два контейнера: сам Prometheus и config-reloader, который следит за сгенерированной оператором конфигурацией и просит Prometheus перечитать её без перезапуска.

Особенность kubeadm. kube-controller-manager, kube-scheduler и метрики etcd в kubeadm по умолчанию слушают только 127.0.0.1, а kube-proxy отдаёт метрики на 127.0.0.1:10249. Prometheus в поде до них не дотянется: сразу после установки цели этих четырёх компонентов — down, а среди алертов etcdInsufficientMembers (critical, горит сразу), TargetDown, KubeControllerManagerInstanceUnreachable, KubeSchedulerInstanceUnreachable и KubeProxyInstanceUnreachable. Варианта два.

Первый — перевести их на адрес узла: bind-address: 0.0.0.0 в extraArgs kubeadm для controller-manager и scheduler, адрес узла в listen-metrics-urls у etcd и metricsBindAddress: 0.0.0.0:10249 в ConfigMap kube-proxy с последующим kubectl rollout restart ds/kube-proxy -n kube-system. На стенде после этого все четыре становятся up. Разница в защите: порты 10257 и 10259 controller-manager и scheduler требуют аутентификации (без токена — 403), а etcd и kube-proxy отдают метрики кому угодно (200), поэтому эти порты закрывают файрволом от всех, кроме подсети подов.

Второй — честно выключить эти цели в values, если они не нужны: kubeControllerManager.enabled: false, kubeScheduler.enabled: false, kubeEtcd.enabled: false, kubeProxy.enabled: false. Вместе с целями чарт убирает и связанные с ними алерты.

Доступ к интерфейсам. Для начала — kubectl port-forward -n monitoring svc/kps-grafana 3000:80 и svc/kps-prometheus 9090. Постоянный доступ к Grafana — через Ingress с сертификатом из части 18 и части 19; к Prometheus и Alertmanager собственной аутентификации нет, их наружу без прокси с аутентификацией не публикуют.

ServiceMonitor для hello

hello из части 5 уже отдаёт /metrics: счётчик запросов и версию сборки. Посмотреть можно через прокси API-сервера, не запуская отдельный под:

$ kubectl get --raw /api/v1/namespaces/demo/services/hello:http/proxy/metrics
# HELP hello_requests_total Requests served by the / handler.
# TYPE hello_requests_total counter
hello_requests_total 2122
# HELP hello_build_info Build information.
# TYPE hello_build_info gauge
hello_build_info{version="1.0"} 1

Чтобы Prometheus начал их собирать, нужен ServiceMonitor — объект, который говорит оператору: «опрашивай эндпоинты сервисов с такими метками, на таком порту, по такому пути». Метку на Service мы поставили ещё в части 15, а порт указывается по имени из Service (у hello он называется http):

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: hello
  namespace: demo
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: hello
  endpoints:
  - port: http
    path: /metrics
    interval: 30s

Оператор дописывает в конфигурацию Prometheus задание с Kubernetes service discovery: Prometheus сам следит за EndpointSlice сервисов с такими метками (так задано serviceDiscoveryRole: EndpointSlice в values; по умолчанию оператор всё ещё использует API Endpoints, объявленный устаревшим в Kubernetes 1.33) и опрашивает адреса всех подов за ними; к каждому ряду Prometheus добавит метки namespace, pod, service, container, endpoint, job и instance. PodMonitor делает то же без сервиса — по меткам подов; удобен для DaemonSet'ов и подов, у которых сервиса нет.

Если в неймспейсе demo действует политика «запретить всё входящее» из части 20, всё зависит от момента. Prometheus держит с целями постоянные соединения, а Calico уже открытые соединения не рвёт: на стенде почти четыре минуты после default-deny цели оставались up, пока мы сами не убрали политику. Но стоит подам смениться, новое соединение установить не удаётся:

$ kubectl rollout restart deploy/hello -n demo
$ curl -s 'localhost:9090/api/v1/targets?state=active' | jq -r '.data.activeTargets[] | select(.labels.job=="hello") | "\(.labels.pod) \(.health) \(.lastError)"'
hello-74d65597f7-5lfft down Get "http://172.20.172.25:8080/metrics": context deadline exceeded
hello-74d65597f7-qqdxp down Get "http://172.20.211.63:8080/metrics": context deadline exceeded
hello-74d65597f7-8vkl6 down Get "http://172.20.211.61:8080/metrics": context deadline exceeded

То есть мониторинг может сломаться не в момент применения политики, а при следующей выкатке. Нужна разрешающая политика — только для неймспейса мониторинга и только на порт метрик; с ней цели возвращаются в up при следующем опросе:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-prometheus
  namespace: demo
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: hello
  policyTypes: ["Ingress"]
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: monitoring
      podSelector:
        matchLabels:
          app.kubernetes.io/name: prometheus
    ports:
    - port: 8080
      protocol: TCP

Полезные запросы

Троттлинг CPU — доля периодов планировщика CFS, в которых контейнер упирался в CPU-лимит (часть 10). Выше 25 % — приложение регулярно тормозят, даже если средняя загрузка низкая. Ряды есть только у контейнеров с CPU-лимитом: hello в этом запросе не появится, а пробный под с limits.cpu: 50m и вечным циклом на стенде показывает ровно 1, то есть 100 %:

sum by (namespace, pod, container) (rate(container_cpu_cfs_throttled_periods_total{container!=""}[5m]))
  /
sum by (namespace, pod, container) (rate(container_cpu_cfs_periods_total{container!=""}[5m]))

Рестарты за последний час — по kube-state-metrics:

increase(kube_pod_container_status_restarts_total[1h]) > 0

OOM — контейнеры, последний раз завершённые по OOM, и сами события OOM от cAdvisor:

kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1
increase(container_oom_events_total{container!=""}[1h]) > 0

Память относительно лимита — кто близок к OOM:

max by (namespace, pod, container) (container_memory_working_set_bytes{container!=""})
  /
max by (namespace, pod, container) (kube_pod_container_resource_limits{resource="memory"})
  > 0.9

Requests против потребления — для подбора requests: отношение реального CPU к запрошенному за сутки. Значения около 0,1 означают, что под заказывает в десять раз больше, чем тратит, и занимает место на узле зря:

sum by (namespace, pod) (rate(container_cpu_usage_seconds_total{container!=""}[1d]))
  /
sum by (namespace, pod) (kube_pod_container_resource_requests{resource="cpu"})

Свободное место на PVC — метрики kubelet по смонтированным томам, и прогноз, что том заполнится в ближайшие четыре часа:

kubelet_volume_stats_available_bytes / kubelet_volume_stats_capacity_bytes < 0.1

predict_linear(kubelet_volume_stats_available_bytes[6h], 4 * 3600) < 0

predict_linear строит линейную регрессию по точкам за 6 часов и экстраполирует на 4 часа вперёд. Эти метрики есть только у томов, драйвер которых реализует получение статистики. У csi-driver-nfs и csi-driver-smb это так, но для общего NFS-экспорта со subDir «свободно» показывает место на всём экспорте, а не квоту тома: на стенде PVC на 1 ГиБ отчитывается о ёмкости 17 ГБ. А вот тома local-path — обычные каталоги узла, и рядов kubelet_volume_stats_* у них нет вовсе, значит, том самого Prometheus этими запросами не увидеть, и алерт KubePersistentVolumeFillingUp за ним не следит. Для него смотрят на файловую систему узла:

node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} < 0.1

Правило алерта

Правила записываются объектом PrometheusRule. Каждое правило алерта — выражение PromQL: пока оно возвращает хотя бы один ряд, алерт активен. for — сколько времени выражение должно быть истинным подряд, прежде чем алерт перейдёт из pending в firing и уйдёт в Alertmanager. У hello два показателя: доступность цели и счётчик запросов, — на них и напишем:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: hello
  namespace: demo
spec:
  groups:
  - name: hello
    rules:
    - alert: HelloTargetDown
      expr: up{namespace="demo", service="hello"} == 0
      for: 2m
      labels:
        severity: warning
      annotations:
        summary: Prometheus не может снять метрики с пода hello
        description: 'Под {{ $labels.pod }} не отвечает на /metrics уже 2 минуты.'
    - alert: HelloHighRequestRate
      expr: sum(rate(hello_requests_total{namespace="demo"}[5m])) > 50
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: hello получает больше 50 запросов в секунду
        description: 'Сейчас {{ $value | humanize }} запросов в секунду, HPA может не успевать.'

Метка severity нужна Alertmanager'у для маршрутизации: critical — будить дежурного, warning — в рабочий канал. Кроме алертов, в PrometheusRule пишут recording rules — заранее вычисляемые выражения, которые сохраняются как новые ряды: тяжёлый запрос для дашборда считается раз в интервал, а не при каждом открытии страницы.

В наборе правил чарта есть алерт Watchdog, который горит всегда. Это не ошибка, а проверка всей цепочки: его настраивают на внешний сервис «мёртвой руки», который поднимает тревогу, если Watchdog перестал приходить, — то есть сломался сам мониторинг.


Смотрим руками

API метрик напрямую. kubectl top — всего лишь клиент к агрегированному API:

$ kubectl get --raw /apis/metrics.k8s.io/v1beta1/namespaces/demo/pods/hello-74d65597f7-5lfft | jq '{window, containers}'
{
  "window": "27.174s",
  "containers": [
    {
      "name": "hello",
      "usage": {
        "cpu": "4203724n",
        "memory": "6228Ki"
      }
    }
  ]
}

window — интервал между двумя точками, по которым посчитан CPU: обычно около 15 секунд, но метки времени ставит kubelet, и окно бывает длиннее; 4203724n — наноядра, около 4,2m.

Сертификат kubelet — после serverTLSBootstrap подписан CA кластера, а не самим kubelet:

user@k8s-master-1:~$ sudo openssl x509 -in /var/lib/kubelet/pki/kubelet-server-current.pem -noout -issuer -subject -ext subjectAltName -enddate
issuer=CN=kubernetes
subject=O=system:nodes, CN=system:node:k8s-master-1
X509v3 Subject Alternative Name:
    DNS:k8s-master-1, IP Address:192.168.10.17
notAfter=Oct 24 10:32:48 2027 GMT

Что оператор сделал из ServiceMonitor. Сгенерированная конфигурация лежит в Secret, сжатая gzip:

$ kubectl get secret prometheus-kps-prometheus -n monitoring -o jsonpath='{.data.prometheus\.yaml\.gz}' \
    | base64 -d | gunzip | grep -A16 'job_name: serviceMonitor/demo/hello'
- job_name: serviceMonitor/demo/hello/0
  honor_labels: false
  kubernetes_sd_configs:
  - role: endpointslice
    namespaces:
      names:
      - demo
  scrape_interval: 30s
  metrics_path: /metrics
  relabel_configs:
  - source_labels:
    - job
    target_label: __tmp_prometheus_job_name
  - action: keep
    source_labels:
    - __meta_kubernetes_service_label_app_kubernetes_io_name
    - __meta_kubernetes_service_labelpresent_app_kubernetes_io_name

Цели и их состояние — через HTTP API Prometheus:

$ kubectl port-forward -n monitoring svc/kps-prometheus 9090 &
$ curl -s 'localhost:9090/api/v1/targets?state=active' | jq -r '.data.activeTargets[] | select(.labels.namespace=="demo") | "\(.labels.pod) \(.health) \(.lastScrapeDuration)"'
hello-74d65597f7-5lfft up 0.000396057
hello-74d65597f7-qqdxp up 0.000679508
hello-74d65597f7-8vkl6 up 0.000627355

$ curl -s localhost:9090/api/v1/query --data-urlencode 'query=prometheus_tsdb_head_series' | jq -r '.data.result[0].value[1]'
76398

TSDB на диске — в локальном томе, на узле, где запущен Prometheus. Изнутри контейнера не посмотреть: образ distroless, ls в нём нет (exec: "ls": executable file not found in $PATH), а путь к каталогу подскажет PV:

$ kubectl get pv $(kubectl get pvc -n monitoring prometheus-kps-prometheus-db-prometheus-kps-prometheus-0 -o jsonpath='{.spec.volumeName}') -o jsonpath='{.spec.hostPath.path}'
/opt/local-path-provisioner/pvc-3aa8da09-dea5-4d57-baaa-abeb390d8832_monitoring_prometheus-kps-prometheus-db-prometheus-kps-prometheus-0
user@k8s-node-2:~$ sudo ls -1 /opt/local-path-provisioner/pvc-3aa8da09-…/prometheus-db
01M6HPRQAS1NC6XZHWA1Y7GYB4
chunks_head
lock
queries.active
wal

Каталог с именем вида ULID — готовый блок: по умолчанию на два часа данных, а со временем блоки укрупняются. wal — журнал предзаписи, chunks_head — отображённые в память чанки свежих данных, lock защищает каталог от второго процесса, queries.active хранит выполняющиеся запросы. Именно с этими файлами Prometheus работает через mmap и блокировки, и именно поэтому им нужен локальный диск.

Алерт в работе. Под нагрузкой из шести подов hello получает больше тысячи запросов в секунду. Через 25 секунд после запуска выражение истинно, и алерт в pending; через пять минут — в firing, и тогда же он появляется в Alertmanager:

$ curl -s localhost:9090/api/v1/alerts | jq -r '.data.alerts[] | select(.labels.alertname | startswith("Hello")) | "\(.labels.alertname) \(.state) \(.activeAt)"'
HelloHighRequestRate firing 2026-11-03T19:07:55.540015347Z

$ kubectl port-forward -n monitoring svc/kps-alertmanager 9093 &
$ curl -s 'localhost:9093/api/v2/alerts?filter=alertname=~"Hello.*"' | jq -r '.[] | "\(.labels.alertname) \(.status.state) \(.startsAt) \(.annotations.description)"'
HelloHighRequestRate active 2026-11-03T19:12:55.540Z Сейчас 1.258k запросов в секунду, HPA может не успевать.

activeAt — когда выражение стало истинным, startsAt в Alertmanager — когда прошло for. Опыт с default-deny из раздела о ServiceMonitor заодно проверяет и второе правило: через две минуты после того, как цели стали down, у HelloTargetDown на каждый под по своему алерту в firing, в описании у каждого — его имя (Под hello-74d65597f7-8vkl6 не отвечает на /metrics уже 2 минуты.). Получателей в Alertmanager мы не настраивали, поэтому дальше API алерт пока никуда не уходит.

Уборка: kubectl delete -f для файлов политик default-deny и allow-prometheus и kubectl delete deployment load -n default, если генератор ещё работает. metrics-server, HPA и Prometheus оставляем: следующая часть продолжит на них.


Метрики отвечают на вопрос «сколько» и «когда»: сколько запросов, когда начался рост. На вопрос «какой именно запрос и почему» они не отвечают — для этого нужны логи и трассы, связанные с метриками общими метками и идентификаторами.


Источники


Дальше — часть 33: логи и трассы. Как устроены логи контейнеров на узле, сбор их агентом в Loki, LogQL, аудит API-сервера, трассировка на OpenTelemetry и Tempo и переход от графика к логу и от лога к трассе в Grafana.

Оставить комментарий могут только зарегистрированные пользователи.

Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.