Алерт сработал, и никто его не увидел: метка 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.