中文翻译暂不可用,显示俄语原文。

Как вы проектируете dead letter queue для failed LLM инференс запросов?

Краткий тезис

Dead Letter Queue (DLQ) — это механизм отказоустойчивости, который собирает запросы к LLM, не обработанные после всех повторных попыток. Правильная архитектура DLQ для LLM-инференса включает несколько топиков с разными задержками повторной обработки (retry-1s, retry-10s, retry-1m), priority|приоритетный consumer, читающий из retry-топиков раньше, чем из основного input, и финальный dlq-топик для ручного разбора и алертов. Это позволяет не терять запросы, минимизировать влияние временных сбоев и анализировать корневые причины отказов.


1. Термин: Dead Letter Queue (DLQ)

Dead Letter Queue — это очередь (или топик в Kafka), в которую попадают сообщения, не обработанные после исчерпания всех попыток повторной обработки. В контексте LLM-инференса DLQ хранит запросы, которые не удалось выполнить из-за ошибок модели, таймаутов, превышения лимитов rate limiter’а или других неисправимых проблем.

Зачем DLQ для LLM

  • Не терять запросы — пользовательский запрос не должен бесследно исчезать.
  • Анализировать ошибки — можно изучать паттерны отказов и улучшать систему.
  • Ручное восстановление — оператор может повторно отправить запрос после исправления проблемы.

2. Архитектура на Kafka с несколькими retry-топиками

Предлагается следующая схема на базе Apache Kafka (или совместимых брокеров, например, Schema Redpanda):

[Producer] → input → [Consumer] → успех (ответ)
                      ↓ ошибка
                   retry-1s → [Consumer] → успех
                      ↓ ошибка
                   retry-10s → [Consumer] → успех
                      ↓ ошибка
                   retry-1m → [Consumer] → успех
                      ↓ ошибка
                   dlq → [Manual reprocess / Alert]

Топики и их назначение

ТопикЗадержка перед повторной попыткойКогда используется
inputнет (немедленно)Первичная обработка
retry-1s1 секундаПервая повторная попытка (например, после таймаута)
retry-10s10 секундВторая попытка (после временной недоступности)
retry-1m1 минутаТретья попытка (после rate limit или перегрузки)
dlqФинальная точка — запрос не обработан

Как реализовать задержку
В Kafka нет встроенной задержки сообщений. Обычно consumer после неудачи отправляет сообщение в следующий retry-топик, а consumer этого топика спит указанное время перед чтением. Или используется Kafka Streams с punctuator для отложенной отправки. Более простой вариант — использовать RabbitMQ с x-delay или AWS SQS с DelaySeconds, но Kafka даёт больше контроля над порядком и масштабируемостью.


3. Consumer с приоритетами

Consumer должен читать сообщения из retry-топиков с более высоким приоритетом, чем из input. Это гарантирует, что повторные попытки не будут откладываться бесконечно из-за наплыва новых запросов.

Реализация приоритетов

  • Используем один consumer group, который подписан на все топики.
  • В цикле опрашиваем топики в порядке приоритета: retry-1sretry-10sretry-1minput.
  • Если в retry-топике есть сообщение, обрабатываем его, иначе переходим к следующему.
  • Важно: не блокировать чтение из input, если retry-топики пусты.

Пример псевдокода на Python (с использованием confluent_kafka):

import time
from confluent_kafka import Consumer, KafkaError

TOPICS_PRIORITY = ['retry-1s', 'retry-10s', 'retry-1m', 'input']

c = Consumer({
    'bootstrap.servers': 'localhost:9092',
    'group.id': 'llm-consumer',
    'auto.offset.reset': 'earliest'
})
c.subscribe(TOPICS_PRIORITY)

def process_message(msg):
    # вызов LLM, обработка
    pass

while True:
    # poll с таймаутом 100ms
    msg = c.poll(0.1)
    if msg is None:
        continue
    if msg.error():
        if msg.error().code() == KafkaError._PARTITION_EOF:
            continue
        else:
            print(msg.error())
            break

    topic = msg.topic()
    try:
        process_message(msg)
        c.commit(msg)  # успех — коммитим
    except Exception as e:
        # Определяем следующий retry-топик
        next_topic = get_next_retry_topic(topic)
        if next_topic:
            # отправляем в следующий retry-топик
            produce_to_topic(next_topic, msg.value())
        else:
            # отправляем в dlq
            produce_to_topic('dlq', msg.value())
        c.commit(msg)  # коммитим исходное сообщение (оно уже не нужно)

Функция get_next_retry_topic

RETRY_CHAIN = {
    'input': 'retry-1s',
    'retry-1s': 'retry-10s',
    'retry-10s': 'retry-1m',
    'retry-1m': None  # конец цепочки
}

def get_next_retry_topic(current_topic):
    return RETRY_CHAIN.get(current_topic)

4. Обработка из DLQ: ручное восстановление и алерты

Когда запрос попадает в dlq, система должна:

  1. Отправить алерт — например, в Sentry или PagerDuty, чтобы дежурный инженер знал о проблеме.
  2. Сохранить запрос в долговременное хранилище (база данных, S3) для анализа.
  3. Предоставить интерфейс для ручного reprocess — админка, где можно просмотреть содержимое DLQ и повторно отправить запрос в input (или в конкретный retry-топик) после исправления причины.

Пример админки (веб-интерфейс):

  • Список сообщений из dlq с метаданными (время, ошибка, тело запроса).
  • Кнопка «Reprocess» — отправляет сообщение обратно в input.
  • Возможность редактировать запрос перед повторной отправкой.

5. Дополнительные соображения

Экспоненциальная задержка

Вместо фиксированных задержек (1s, 10s, 1m) можно использовать экспоненциальную задержку с джиттером: delay = base * 2^attempt + random(0, base). Это снижает нагрузку при массовых сбоях.

Максимальное количество retry

Ограничить число попыток (например, 3). После этого — только DLQ. Иначе можно зациклиться.

Обработка rate limiting

Если LLM возвращает 429 Too Many Requests, лучше не сразу retry, а подождать дольше (например, retry-1m). Можно добавить отдельный топик retry-rate-limit.

Fallback

Вместо DLQ можно попробовать fallback-модель (меньшая, более дешёвая) или кэшированный ответ. DLQ — последняя инстанция.

Идемпотентность

Повторные попытки могут привести к дублированию ответов. Клиент должен иметь идентификатор запроса (request_id) и игнорировать дубликаты.


6. Мониторинг и метрики

Для DLQ важно отслеживать:

  • Lag (отставание) каждого топика — сколько сообщений ждут обработки.
  • Количество сообщений в DLQ — если растёт, значит что-то сломалось.
  • Частота ошибок по типам (таймаут, rate limit, ошибка модели).
  • Время обработки — сколько запрос провёл в retry-цепочке.

Инструменты: Prometheus + Grafana, Kafka Lag Exporter.


7. Альтернативы Kafka

БрокерВстроенная задержкаПриоритетыПростота
KafkaНет (нужен consumer sleep)Ручная реализацияСредняя
RabbitMQДа (плагин x-delay)Да (через x-priority)Высокая
AWS SQSДа (DelaySeconds)Нет (FIFO с группами)Очень высокая
Redis StreamsНет (нужен XREADGROUP с BLOCK)ЧастичноНизкая

Выбор зависит от инфраструктуры: если уже есть Kafka — используем её; если стартап — проще RabbitMQ или SQS.


8. Потенциальные проблемы и их решение

  • Дубликаты сообщений — при сбое consumer может отправить одно и то же сообщение в retry-топик дважды. Решение: идемпотентность на стороне producer (уникальный ключ сообщения) и дедупликация в consumer.
  • Нарушение порядка — если важен порядок обработки запросов, используйте Kafka с одним partition на пользователя или FIFO-очередь (SQS FIFO).
  • Бесконечный retry — всегда задавайте максимальное количество попыток и TTL для сообщения.

Пет-проект для закрепления

Задача Реализовать DLQ для LLM-инференса на Kafka с тремя retry-топиками, приоритетным consumer и веб-админкой для ручного reprocess.

Инструменты

Шаги:

  1. Запустите Kafka в Docker: docker-compose up -d с одним брокером.
  2. Создайте топики: input, retry-1s, retry-10s, retry-1m, dlq.
  3. Напишите producer, который отправляет случайные запросы в input.
  4. Напишите consumer с приоритетным опросом (как в примере выше). При ошибке (имитация — случайный сбой) отправляйте в следующий retry-топик.
  5. После 3 неудач отправляйте в dlq.
  6. Реализуйте FastAPI-админку:
    • GET /dlq — список сообщений.
    • POST /dlq/{id}/reprocess — отправляет сообщение обратно в input.
  7. Добавьте алерт (логирование в Sentry или просто print).
  8. Запустите и проверьте, что сообщения проходят цепочку и попадают в DLQ.

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


Связь с другими вопросами

ВопросТема
406Как вы обрабатываете ошибки LLM (timeout, rate limit)?
408Как вы проектируете систему мониторинга для LLM-инференса?
409Как вы реализуете circuit breaker для LLM?
410Как вы управляете очередями запросов к LLM?
411Как вы тестируете отказоустойчивость LLM-сервиса?
412Как вы логируете запросы и ответы LLM?

Навигация