← Все статьи

Ребаланс-шторм оказался обычным деплоем: порог, который растёт вместе с группой

Алерт на ребалансы Kafka горел на 60 в час при каждом релизе. Метрика суммировалась по 40 консьюмерам и усреднялась по скользящему часу — алерт описывал прошлое.

В Gambchamp алерт KafkaConsumerRebalanceStorm загорелся по одной consumer-группе: 60 ребалансов в час при пороге 30, и держался красным больше получаса. Соседние группы того же кластера показывали 8 и 12. Вывод дежурного очевиден: группа больше ребалансится, чем работает, идём искать падающие поды.

Подвох: так было на каждом релизе, и канал постепенно приучились игнорировать. Для алерта это худший исход.

Что я отбросил первым

  • Падающие поды: у всех RESTARTS=0.
  • Проблемы брокеров: задета одна группа, остальные спокойны.
  • Петля автоматики, которая что-то перезапускает: reloader молчал шесть дней.

Diff pod-шаблонов старого и нового ReplicaSet показал ровно одно изменение — тег образа. Обычный релиз.

Цифра измеряла размер, а не поведение

Порог был абсолютным, а метрика — суммой по всей группе.

  1. ETL-сервис держит 20 консьюмеров на под в одной consumer-группе — по одному на топик, но с общим group.id. При двух репликах это 40 участников.
  2. У счётчика ребалансов есть метки группы, окружения, сервиса и версии — и ни одной, различающей консьюмеров. Все 20 инкрементят одну серию. На свежеподнятом поде у обычных групп 1.0, у этой — 20.0.
  3. Rolling update — это 20 входов на каждый новый под плюс переназначения у старых: около 60, механически. Срабатывание на релизе было гарантировано конструкцией.
  4. Сверху правило использовало 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: даёт иммунитет к всплескам вроде деплоев и скейла.
  • Проверяй проверяльщика: негативный контроль отличает «нет данных» от «битого запроса».