Стандартный автоскейлер Kubernetes умеет масштабировать поды по процессору и памяти, и для веб-приложений этого часто хватает. Но представьте обработчик очереди, который разбирает сообщения. Его процессор может быть почти простаивающим, пока в очереди копятся тысячи задач, потому что бутылочное горлышко не в вычислениях, а в скорости разгребания. Масштабировать такой обработчик по процессору бессмысленно: метрика не отражает реальную потребность. Гораздо честнее скейлить по длине очереди. Разберём, как связать длину очереди RabbitMQ с автоскейлером через Prometheus Adapter и где у этой схемы тонкие места.

Почему процессорная метрика обманывает для обработчиков очередей

Суть проблемы в несоответствии метрики и узкого места. Автоскейлер по умолчанию смотрит на загрузку процессора и добавляет поды, когда она растёт. Для обработчика очереди это ложный сигнал. Сообщения могут ждать в очереди тысячами, а процессор обработчика при этом загружен умеренно, потому что он упирается в скорость обработки одного сообщения, в ожидание внешних сервисов или в сеть, а не в вычисления.

Автоскейлинг по очереди мощен именно для приложений, потребляющих сообщения, число которых колеблется во времени и плохо предсказуемо. Логика очевидна: когда в очереди много ждущих сообщений, нарастить число подов, чтобы быстрее их разгрести, а когда очередь пуста, ужать обратно, не тратя ресурсы зря. Вместо абстрактного порога загрузки процессора скейлинг идёт по бизнес-значимому сигналу: сколько работы реально ждёт обработки.

Из каких звеньев собирается вся цепочка

Чтобы автоскейлер увидел длину очереди, нужно выстроить мост от RabbitMQ к нему через несколько компонентов. Цепочка такая: RabbitMQ отдаёт метрики, Prometheus их собирает, Prometheus Adapter превращает их в метрики, понятные Kubernetes, а автоскейлер их потребляет.

RabbitMQ  ->  Prometheus  ->  Prometheus Adapter  ->  Custom/External Metrics API  ->  HPA

Каждое звено играет свою роль. RabbitMQ умеет отдавать метрики Prometheus через специальный встроенный модуль, который выставляет наружу множество показателей, включая число готовых к обработке сообщений. Prometheus периодически собирает эти показатели. Но сам автоскейлер не умеет читать из Prometheus напрямую, ему нужны метрики через специальный программный интерфейс Kubernetes. Вот этот разрыв и закрывает Prometheus Adapter: он запрашивает значения у Prometheus и выставляет их через интерфейс метрик Kubernetes.

Чем метрики на под отличаются от внешних

Тут кроется развилка, которую важно понять сразу, потому что от неё зависит вся конфигурация. Kubernetes поддерживает два разных интерфейса для нересурсных метрик, и они принципиально различаются.

Первый это интерфейс пользовательских метрик. Он применяется, когда метрика привязана к объекту Kubernetes вроде пода, развёртывания или пространства имён. Метрики тут должны содержать метки объектов Kubernetes и масштабируют по среднему значению на под. Второй это интерфейс внешних метрик. Он нужен, когда метрика живёт вне Kubernetes и не привязана к конкретному поду, как раз случай общей длины очереди в брокере.

Для длины очереди RabbitMQ обычно подходит именно внешний интерфейс, потому что очередь это не свойство пода, а общий ресурс. Длина очереди одна на всех потребителей, и масштабировать имеет смысл по её абсолютному значению, а не по среднему на под. Хотя возможен и подход через пользовательские метрики, если хочется держать целевое число сообщений на под.

Как настроить Prometheus Adapter под длину очереди

Адаптер ставят в кластер через готовый пакет, а его поведение задаётся правилами, которые описывают, какую метрику из Prometheus как выставить наружу. Для внешней метрики длины конкретной очереди правило выглядит так.

rules:
  external:
    - seriesQuery: 'rabbitmq_queue_messages_ready'
      resources:
        template: <<.Resource>>
      name:
        as: "queue_messages_ready"
      metricsQuery: |
        sum(<<.Series>>{queue="orders"})

Здесь правило берёт показатель готовых сообщений, суммирует его по конкретной очереди заказов и выставляет наружу под понятным именем. После применения стоит убедиться, что метрика действительно доступна автоскейлеру, запросив интерфейс метрик напрямую.

# Проверить, что пользовательские метрики видны
kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq .

# Или внешние метрики
kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1" | jq .

Если нужное имя метрики появилось в выводе, значит мост собран и можно настраивать сам автоскейлер.

Как описать автоскейлер по очереди

Теперь главное звено. Автоскейлер описывается манифестом, где указывается целевое приложение, границы числа реплик и метрика, по которой принимается решение о масштабировании.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: queue-worker-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: queue-worker
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: External
      external:
        metric:
          name: queue_messages_ready
        target:
          type: Value
          value: "500"

Этот автоскейлер будет наращивать число обработчиков, пока длина очереди не опустится ниже целевого значения. Если же выбран подход через метрики на под, цель задаётся как среднее значение на под, и тогда автоскейлер держит, к примеру, около тридцати задач на каждый под.

metrics:
  - type: Pods
    pods:
      metric:
        name: queue_length
      target:
        type: AverageValue
        averageValue: "30"

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

Почему ровный темп обработки это скрытое условие

Тут прячется неочевидное, но критичное требование. Масштабирование по длине очереди надёжно работает только тогда, когда потребители обрабатывают сообщения с примерно постоянной скоростью. Темп обязан быть ровным, потому что если одни сообщения обрабатываются час, а другие пару секунд, скейлинг становится ненадёжным.

Логика проста. Автоскейлер исходит из того, что добавление пода пропорционально ускоряет разгребание очереди. Но если сообщения сильно разнятся по трудоёмкости, то добавление подов не даёт предсказуемого эффекта: новые поды могут схватить лёгкие сообщения и быстро освободиться, пока тяжёлые держат старые поды занятыми. Поэтому для надёжного скейлинга по очереди стоит либо выровнять трудоёмкость сообщений, либо разнести разнотипную работу по разным очередям с отдельными обработчиками и отдельными автоскейлерами.

Какая ловушка ждёт с агрегатными метриками

Отдельная мина связана с тем, как RabbitMQ отдаёт метрики. По умолчанию встроенный модуль метрик возвращает агрегированные показатели по всем очередям разом. Это затрудняет масштабирование конкретного приложения по бэклогу одной отдельной очереди, ведь агрегат смешивает все очереди в кучу.

Лечится это включением метрик по отдельным объектам, чтобы получать длину каждой очереди в отдельности. Тогда в правиле адаптера можно отфильтровать именно нужную очередь по её имени. Без этого автоскейлер будет реагировать на суммарный бэклог всех очередей, что почти наверняка приведёт к неверным решениям о масштабировании, если в кластере несколько разных очередей с разной динамикой.

Как настроить поведение масштабирования и не раскачать систему

Голый порог недостаточен, потому что без сглаживания автоскейлер начнёт дёргать число подов туда-сюда при любых колебаниях очереди. Для этого в манифесте задают поведение масштабирования с окнами стабилизации и политиками.

  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 50
          periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 30

Здесь масштабирование вниз идёт осторожно: окно стабилизации в пять минут не даёт ужимать поды при кратковременном опустошении очереди, а политика ограничивает скорость сокращения половиной за минуту. Масштабирование вверх делают более отзывчивым с коротким окном, потому что на рост бэклога реагировать нужно быстро. За поведением автоскейлера в продакшене стоит следить и ставить оповещения на момент, когда он упирается в минимальное или максимальное число реплик, потому что это сигнал об ограничении масштабирования.

Где у Prometheus Adapter предел и при чём тут KEDA

Честно стоит сказать о границах подхода. Связка автоскейлера с адаптером метрик мощна, но имеет ограничения, особенно для событийно-управляемых нагрузок и сложных интеграций с внешними системами. Главное из них в том, что обычный автоскейлер не умеет масштабировать до нуля: минимум это хотя бы один под, даже когда очередь давно пуста.

Здесь на сцену выходит специализированный инструмент событийно-управляемого автоскейлинга. Он расширяет возможности Kubernetes, добавляя масштабирование до нуля и поддерживая множество внешних источников из коробки, включая RabbitMQ, очереди разных облаков и брокеры сообщений. Для чисто очередных нагрузок он часто оказывается удобнее, потому что избавляет от ручной сборки моста через Prometheus и адаптер и умеет схлопывать обработчик в ноль подов в простое. Поэтому если задача сводится именно к скейлингу по очереди, стоит честно взвесить, не проще ли взять событийно-управляемый автоскейлер вместо ручной связки. Связка через Prometheus Adapter выигрывает там, где помимо очереди нужно масштабировать и по другим метрикам Prometheus, держа всё в одном месте.

Какой стратегии придерживаться

Автоскейлинг по длине очереди превращает обработчик из глухого к нагрузке в чутко реагирующий на реальный бэклог. Разумный подход складывается из нескольких решений. Выбрать правильный интерфейс метрик: внешний для абсолютной длины очереди, на под для среднего значения на обработчик. Включить метрики по отдельным очередям, чтобы не реагировать на агрегат всех очередей разом. Убедиться, что обработка идёт ровным темпом, иначе скейлинг будет ненадёжным. Настроить поведение с осторожным сжатием и отзывчивым ростом. И трезво оценить, не подойдёт ли событийно-управляемый автоскейлер лучше, особенно если нужна способность схлопываться в ноль.

Главная мысль в том, что метрика для масштабирования должна отражать реальное узкое место, а не удобный для измерения показатель. Тот, кто скейлит обработчик очереди по процессору, обречён либо копить бэклог при простаивающих подах, либо держать лишние поды впустую. А тот, кто связал автоскейлер с длиной очереди и выровнял темп обработки, получает систему, которая сама подстраивает мощность под поток работы, разгребая всплески и экономя ресурсы в затишье.