← Все статьи

Панель Grafana, которая говорит, когда под упадёт по OOM

Как превратить тренд памяти в ETA до OOM и до заполнения диска на PromQL, два бага, из-за которых панель врала, и утечка, которую она поймала в первый же день.

В Gambchamp я отвечаю за мультитенантную продуктовую платформу на AWS EKS: все кластеры пишут метрики в один VictoriaMetrics, перед которым стоит общая Grafana. Дашборды golden signals показывают, что сломано сейчас. Мне был нужен дашборд, который отвечает на другой вопрос: если тренд сохранится, когда у этого пода кончится память и когда заполнится этот том?

Формула

Основа — одна строка:

(limit - current) / clamp_min(deriv(usage[6h]), 1)

deriv строит прямую по точкам окна и возвращает её наклон в байтах в секунду. Оставшийся запас, делённый на этот наклон, — секунды до лимита. Важны два окна: 6 часов ловят острый рост, 24 часа ловят медленные утечки, которые прячет шум короткого окна.

clamp_min(..., 1) делает две вещи. Защищает от деления на ноль и превращает уменьшающийся ряд в огромный ETA вместо отрицательного. Без этого таблица забивается -Inf и отрицательным мусором. Пороги в таблице идут от меньшего к большему: красный меньше 24 часов, оранжевый до 72 часов, зелёный дальше. Единица s сама форматируется в дни и часы.

Та же идея легла и на managed-базы: свободное место, делённое на скорость, с которой оно уходит, даёт ETA до заполнения диска.

Утечка в первый же день

При первой отрисовке панель подсветила один admin-бэкенд с ETA ноль часов. Я проверил: его убивал OOMKill с кодом 137, около 900 Mi при лимите 1 Gi, шесть рестартов. CronJob перезапускал его по расписанию, пряча симптом и ничего не исправляя.

Баг первый: отрицательный ETA у подов без лимитов

Часть бэкендов задавала лимит памяти только сайдкару, а не основному контейнеру. Тогда sum(limits) - sum(used) на уровне пода вычитает потребление всего пода из лимита сайдкара, уходит в минус, и панель красит совершенно стабильный под в красный. Исправление: считать по контейнерам с inner join, чтобы контейнеры без лимита выпадали, и оставлять только ряды с реально положительным наклоном.

Баг второй: двойной счёт в cAdvisor

На этой инсталляции cAdvisor ставит одинаковое значение container на каждый уровень cgroup пода: cgroup самого пода, каждый контейнер и pause-контейнер. sum by(pod) складывает cgroup пода, который уже и есть сумма, с контейнерами, и все панели памяти и CPU удвоились. Один под показывал 156 % лимита при реальных 79 %. Правило, которому я теперь следую: max by(pod) для метрик cAdvisor, sum by(pod) для лимитов и реквестов из kube-state.

Чего прогноз не видит

Тренд ловит только монотонные утечки. Под, который уже ходит по кругу «рост, OOMKill, рестарт», имеет наклон регрессии около нуля, и прогноз правильно его не показывает. Такие поды видны на панелях рестартов и процента памяти. Пустая таблица прогноза означает «сейчас нет монотонной утечки», а не «всё здорово».

Ещё две ловушки той же недели. Для рестартов читай сырой счётчик, а не increase[1h]: пересозданный под начинает новый ряд с нуля, и increase прячет шторм рестартов ровно на том поде, который тебе нужен. И автомасштаб оси Y врёт на больших медленных значениях: том базы выглядел так, будто падает с обрыва, а изменение было 15 MB из 163 GiB, ETA почти семь лет.

Что вынести

  • Оставшийся запас, делённый на deriv, — рабочий ETA, но наклон надо ограничивать снизу и оставлять только растущие ряды.
  • Метрики cAdvisor и kube-state агрегируются по-разному; проверь один под руками, прежде чем верить любому sum by(pod).
  • Трендовый прогноз и счётчик рестартов ловят разные классы отказов. Нужны оба.
  • Медленную ёмкость оценивай по абсолютному числу и ETA, а не по крутизне линии.