Worker может почти не расходовать CPU и одновременно проигрывать нагрузке: тысячи сообщений ждут в SQS, а Horizontal Pod Autoscaler видит спокойный контейнер. Для асинхронной системы давление измеряется не загрузкой процессора, а объёмом и возрастом невыполненной работы. KEDA превращает backlog очереди во внешнюю метрику HPA и умеет возвращать Deployment в ноль, когда очередь опустела.

31 июля Альбена Галабова из Itgix описала настройку KEDA для Amazon SQS в блоге CNCF. Материал Linux Foundation доступен по CC BY 3.0; эта русская адаптация добавляет эксплуатационные ограничения и сверяет manifest с KEDA 2.20. На 3 августа последний опубликованный patch-релиз — v2.20.2.

Почему CPU не описывает очередь

CPU показывает, насколько занят запущенный процесс, но не отвечает на вопрос, сколько работы ждёт снаружи. Один worker может быстро забрать batch из SQS и долго ждать внешнюю базу. CPU останется низким, хотя новые сообщения продолжат поступать. Обратная ситуация тоже возможна: тяжёлая локальная обработка загрузит CPU, но backlog уже исчез.

Для queue-driven workload полезный сигнал ближе к бизнес-очереди:

  • ApproximateNumberOfMessages — сообщения, доступные для получения;
  • ApproximateNumberOfMessagesNotVisible — сообщения, которые consumer уже взял, но ещё не удалил;
  • ApproximateNumberOfMessagesDelayed — отложенные сообщения, которые пока нельзя получить.

По умолчанию SQS-scaler KEDA складывает visible и in-flight сообщения. Delayed добавляются только при scaleOnDelayed: "true". Это не универсально правильная формула: она должна совпадать с семантикой consumer и visibility timeout.

Как KEDA делит ответственность с HPA

KEDA опрашивает SQS, публикует external metric и создаёт HPA для целевого Deployment. При положительном числе replicas масштабирование выполняет обычный HPA. KEDA отдельно управляет активацией из нуля и возвратом в ноль.

Эта граница объясняет два параметра, которые часто путают:

  • cooldownPeriod ждёт после последнего активного trigger перед переходом в 0;
  • horizontalPodAutoscalerConfig.behavior управляет изменениями в диапазоне 1→N и N→1.

Спецификация ScaledObject прямо указывает: cooldownPeriod не заменяет HPA stabilization window. Если настроить только cooldown, резкие колебания между 2 и 20 replicas останутся без контроля.

Базовый ScaledObject без ловушки одного сообщения

Ниже пример для Deployment orders-worker. Он использует очередь из переменной QUEUE_URL, опрашивает её раз в 10 секунд и целится в 10 сообщений на Pod:

apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
  name: sqs-auth
  namespace: workers
spec:
  podIdentity:
    provider: aws
    identityOwner: workload
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: orders-worker
  namespace: workers
spec:
  scaleTargetRef:
    name: orders-worker
  pollingInterval: 10
  cooldownPeriod: 120
  minReplicaCount: 0
  maxReplicaCount: 30
  advanced:
    horizontalPodAutoscalerConfig:
      behavior:
        scaleDown:
          stabilizationWindowSeconds: 60
  triggers:
    - type: aws-sqs-queue
      authenticationRef:
        name: sqs-auth
      metadata:
        queueURLFromEnv: QUEUE_URL
        awsRegion: eu-west-1
        queueLength: "10"
        activationQueueLength: "0"
        scaleOnInFlight: "true"
        scaleOnDelayed: "false"

activationQueueLength — порог активности, а не число сообщений для первой replica. Scaler активен, когда metric выше порога. Значение 1 оставит Deployment в нуле при ровно одном сообщении; если любое сообщение должно разбудить worker, используйте 0 или не задавайте поле — это default KEDA 2.20.

queueURLFromEnv читается из контейнера scale target. Если переменная находится не в первом контейнере Pod, задайте scaleTargetRef.envSourceContainerName; иначе KEDA не найдёт URL, хотя Deployment работает.

queueLength нужно получить из нагрузочного теста

KEDA вычисляет желаемое число replicas примерно так:

desired replicas = ceil(actual messages / queueLength)

При queueLength: 10 backlog 25 даёт три replicas, но число 10 ничего не говорит о реальной производительности. Один Pod может обрабатывать десять коротких задач в секунду или одну внешнюю транзакцию пять минут.

Настройте target по результатам теста consumer:

  1. измерьте устойчивую скорость обработки одной replica;
  2. задайте допустимое время очистки burst;
  3. проверьте разные размеры сообщения и latency зависимостей;
  4. ограничьте maxReplicaCount пропускной способностью базы, API и других downstream;
  5. повторите тест с ошибками и retry, а не только с happy path.

Autoscaler способен быстро создать 30 Pod и перенести bottleneck в PostgreSQL. Backlog-aware scaling согласует replicas с очередью, но не знает capacity всей системы.

In-flight сообщения могут помогать или мешать

Default scaleOnInFlight: "true" учитывает сообщения внутри visibility window. Это удерживает capacity, пока работа ещё выполняется, и защищает от раннего scale-in. Для длинных jobs такое поведение обычно разумно.

Но in-flight backlog может быть ложным сигналом. Consumer завершился после ReceiveMessage, сообщение остаётся невидимым до timeout, а KEDA продолжает считать его работой. Слишком длинный visibility timeout удерживает replicas; слишком короткий создаёт повторную доставку ещё работающей задачи.

Решение зависит от модели обработки:

  • оставьте in-flight в формуле, если оно отражает занятые worker slots;
  • отключите через scaleOnInFlight: "false", если масштабировать нужно только ожидающие сообщения;
  • продлевайте visibility для долгих tasks и делайте обработчик идемпотентным;
  • согласуйте terminationGracePeriodSeconds со временем безопасного завершения или возврата сообщения.

Метрика приблизительная — SLO стройте по возрасту

AWS предупреждает, что значения ApproximateNumberOfMessages* eventually consistent и могут сходиться как минимум минуту после остановки producers. Поэтому короткий polling interval не превращает SQS в точный счётчик. Небольшие колебания около порога неизбежны.

Depth удобен для управления capacity, но пользовательское ожидание лучше отражает возраст. CloudWatch-метрика ApproximateAgeOfOldestMessage показывает возраст старейшего необработанного сообщения. Дашборд должен связывать минимум четыре сигнала:

visible backlog → in-flight backlog → age of oldest → processing/error rate

Если depth стабилен, а возраст растёт, workers не успевают. Если replicas растут, но throughput нет, причина находится в consumer или downstream, а не в autoscaler.

Разделяйте IAM scaler и worker

KEDA нужно читать свойства очереди. Для полного queue URL его минимальная задача — sqs:GetQueueAttributes на конкретный ARN. Сам consumer отдельно вызывает ReceiveMessage, DeleteMessage, ChangeMessageVisibility и, при необходимости, GetQueueUrl.

Не объединяйте роли автоматически. Компрометация KEDA operator не должна давать право получать или удалять сообщения, а компрометация worker — читать другие очереди. В примере используется podIdentity.provider: aws; старый provider aws-eks и поле scaler metadata identityOwner помечены deprecated и будут удалены в KEDA 3.

Выбор identityOwner: workload означает, что KEDA использует identity ServiceAccount целевого workload. Альтернатива — роль самого KEDA operator. Решение нужно закрепить в threat model и проверить trust policy, а не выбирать по тому, какой manifest первым заработал.

Как проверить rollout

Перед production создайте тестовую очередь и пройдите наблюдаемый сценарий:

  1. при пустой очереди Deployment устойчиво остаётся в 0;
  2. одно сообщение активирует минимум одну replica;
  3. burst создаёт ожидаемое число Pod без превышения downstream capacity;
  4. после очистки HPA снижает replicas, затем KEDA переводит 1→0;
  5. ошибка IAM или SQS API видна в ScaledObject conditions и логах operator;
  6. недоступная external metric приводит к заранее выбранному fallback, а не случайному состоянию;
  7. завершение Pod не теряет сообщение и не запускает неограниченные дубликаты.

Проверьте kubectl describe scaledobject, созданный HPA, operator logs, SQS attributes и фактический throughput. Успех — не «Pod стало больше», а возраст очереди вернулся к SLO без перегрузки зависимостей.

KEDA решает правильную задачу только с правильным сигналом. Для SQS этим сигналом обычно служит backlog, но production-настройка начинается не с копирования YAML. Она начинается с пропускной способности worker, семантики visibility timeout, IAM-границы и допустимого возраста сообщения.