← Все статьи

Алерт сработал, и никто его не увидел: метка cluster потерялась в агрегации

Нода четыре часа сидела на 90–98% CPU и закончила OOM kill. Алерт на CPU сработал, но avg by(instance) выкинул метку, по которой выбирали все маршруты и дашборды.

В Gambchamp рабочая нода боевого EKS-кластера ночью около четырёх часов сидела на 90–98% CPU и закончила HostOomKillDetected. Алерт про OOM дошёл до канала команды. Алерт про CPU ноды — нет, и в панели «история алертов» в Grafana его тоже не было. Первый вывод естественный: алерт на CPU не сработал, значит, порог неправильный.

«Не сработал» — утверждение, которое надо проверить

Срабатывание, маршрутизация и отображение — три разных этапа, а серия ALERTS прямо говорит о первом. Range-запрос за окно инцидента показал, что HighCpuLoad сработал: pending незадолго до 23:00 UTC, firing с 02:40 до 03:43.

Вопрос сменился с «почему не сработал» на «куда он делся».

Метка, которая исчезла

Правило агрегировало через avg by(instance). Это выкидывает все остальные метки, включая cluster. А оба потребителя выбирали по cluster:

  • отдельный Alertmanager этого продукта маршрутизирует по метке cluster;
  • панель Grafana запрашивает ALERTS{alertstate="firing", cluster=~"$cluster"}.

Без метки алерт попал только в общий маршрут по умолчанию, куда никто из этой команды не смотрел. Алерт про OOM дошёл, потому что агрегирует через count by(node, cluster) — метку он сохранил.

Ещё две гипотезы я отбросил по данным, прежде чем что-то трогать:

  • ломается джойн and on(instance, application) — ни у одной серии нет application, все 84 сматчились;
  • for: 1h сбрасывается провалами — был непрерывный отрезок 124 минуты выше 90%.

Исправление

В общем правиле для нод: avg by(instance) стало avg by(instance, cluster), а for: 1h стало for: 15m — час слишком долго для ноды, которая идёт к OOM. Бэктест нового правила по всему флоту за семь дней сработал только на двух нодах, так что более короткое удержание не добавляет шума.

Изменение ушло через Argo CD, затем POST /-/reload в vmalert — после проверки, что новое правило действительно доехало до тома пода. Reload вместо рестарта сохраняет таймеры for: у всех остальных групп правил.

Сама сатурация шла от Kafka, которая запрашивала 200m CPU, а потребляла около целого ядра, поэтому планировщик посадил её рядом с двумя репликами ETL. Это исправили отдельно, подняв requests.

Бонусная ловушка по дороге: cAdvisor экспортирует каждый контейнер дважды — как .scope и как .slice пода, оба с меткой контейнера. Наивный sum by(pod)(rate(container_cpu_usage_seconds_total)) удваивается.

Выводы

  • Если маршрут или дашборд выбирает по метке, каждая агрегация, которая их питает, обязана эту метку сохранять.
  • «Алерт не сработал» — гипотеза. Проверь ALERTS, прежде чем менять пороги.
  • Поищи в правилах sum/avg/max by(...) без cluster — каждое такое место — то же слепое пятно.
  • При изменении правил предпочитай reload рестарту, чтобы другие алерты сохранили таймеры pending.