Ребаланс-шторм оказался обычным деплоем: порог, который растёт вместе с группой
Алерт на ребалансы Kafka горел на 60 в час при каждом релизе. Метрика суммировалась по 40 консьюмерам и усреднялась по скользящему часу — алерт описывал прошлое.
В Gambchamp алерт KafkaConsumerRebalanceStorm загорелся по одной consumer-группе: 60 ребалансов в час при пороге 30, и держался красным больше получаса. Соседние группы того же кластера показывали 8 и 12. Вывод дежурного очевиден: группа больше ребалансится, чем работает, идём искать падающие поды.
Подвох: так было на каждом релизе, и канал постепенно приучились игнорировать. Для алерта это худший исход.
Что я отбросил первым
- Падающие поды: у всех
RESTARTS=0. - Проблемы брокеров: задета одна группа, остальные спокойны.
- Петля автоматики, которая что-то перезапускает: reloader молчал шесть дней.
Diff pod-шаблонов старого и нового ReplicaSet показал ровно одно изменение — тег образа. Обычный релиз.
Цифра измеряла размер, а не поведение
Порог был абсолютным, а метрика — суммой по всей группе.
- ETL-сервис держит 20 консьюмеров на под в одной consumer-группе — по одному на топик, но с общим
group.id. При двух репликах это 40 участников. - У счётчика ребалансов есть метки группы, окружения, сервиса и версии — и ни одной, различающей консьюмеров. Все 20 инкрементят одну серию. На свежеподнятом поде у обычных групп
1.0, у этой —20.0. - Rolling update — это 20 входов на каждый новый под плюс переназначения у старых: около 60, механически. Срабатывание на релизе было гарантировано конструкцией.
- Сверху правило использовало
increase(...[1h])— скользящее окно. Всплеск сидел в нём целый час после того, как группа уже сошлась. Алерт описывал прошлое.
Я спросил /metrics самого приложения, что значит счётчик, вместо того чтобы гадать по имени. Один curl заменил час рассуждений.
Исправление: нормировать и сделать окно короче for
(
sum by(cluster, consumer_group)(rate(kafka_consumer_rebalances_total[10m])) * 3600
/
clamp_min(sum by(cluster, consumer_group)(kafka_consumer_task_alive), 1)
) > 10
с for: 15m. Единица теперь — «ребалансов на консьюмера в час».
Главный приём: окно в 10 минут короче, чем for в 15. Всплеск деплоя живёт в окне ровно десять минут и выпадает; продержаться пятнадцать он не может. Деплой исключён конструкцией, а не подбором порога.
Я проверил выражение на живом движке, включая негативный контроль: заведомо битый запрос обязан вернуть 422, чтобы пустой результат нельзя было спутать с «нет данных». Ещё проверил, что ни у одной группы нет счётчика без серии-знаменателя: деление молча выкинуло бы её. По истории порог держится примерно до 6; при 4–5 хвост деплоя растягивается до 16 минут, и правило снова врёт.
Настоящий корень — 20 консьюмеров в одной группе с eager-ребалансом — остаётся правкой кода для команды. Это была правка наблюдаемости.
Выводы
- Счётчик за скользящее окно — это не «сейчас».
increase[1h]держит всплеск час после его конца. - Абсолютный порог по агрегату честен только при одинаковой кардинальности. Нормируй.
- Окно
rateкорочеfor:даёт иммунитет к всплескам вроде деплоев и скейла. - Проверяй проверяльщика: негативный контроль отличает «нет данных» от «битого запроса».