English translation is not available yet. Showing Russian content.

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

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

Dead letter queue (DLQ) для failed LLM инференс запросов — это механизм изоляции и последующей обработки запросов, которые не удалось выполнить после всех повторных попыток. Типичная архитектура строится на Kafka (или аналогичной очереди) с тремя топиками: main (входящие запросы), retry (повторные попытки с задержкой) и DLQ (финальные ошибки). Из DLQ запросы либо репроцессируются вручную после анализа, либо автоматически после исправления причины, либо генерируют алерт в Sentry / PagerDuty. Ключевые аспекты: стратегия повторных попыток (backoff|exponential backoff, jitter), мониторинг глубины DLQ, идемпотентность обработчиков и дедупликация.


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

Dead letter queue — это очередь (или топик), в которую попадают сообщения, которые не удалось обработать после всех разрешённых повторных попыток. В контексте LLM инференса это означает, что запрос к модели (например, вызов OpenAI API или локальной модели) завершился ошибкой, и система исчерпала лимит retry.

Зачем нужна DLQ для LLM

  • Изоляция ошибок: сбойные запросы не блокируют обработку нормальных.
  • Анализ первопричин: можно изучать паттерны ошибок (таймауты, rate limits, битые промпты).
  • Репроцессинг: после исправления ошибки (например, увеличения таймаута) можно повторно отправить запросы из DLQ.
  • Алертинг: при росте числа сообщений в DLQ — сигнал для инженера.

2. Причины failed LLM инференс запросов

Прежде чем проектировать DLQ, нужно понимать, какие ошибки могут возникать:

Тип ошибкиПримерСтратегия retry
Таймаут (timeout)LLM не ответила за 30 сExponential backoff, увеличить таймаут
Rate limit (превышение лимита)429 Too Many RequestsRetry с заголовком Retry-After
Ошибка модели (model error)500 Internal Server Error, перегрузкаRetry с задержкой, переключение на fallback
Битый промпт (invalid prompt)Слишком длинный контекст, невалидный JSONНе retry — сразу в DLQ
Ошибка авторизацииИстёк API-ключНе retry — алерт
Ошибка парсинга ответаLLM вернула неструктурированный текстRetry с другим prompt template

Вывод: не все ошибки стоит повторять. DLQ должна получать только те запросы, которые не удалось обработать после разумного числа retry, а также те, которые заведомо не могут быть ретраированы (например, битый промпт).


3. Архитектура с Kafka: main → retry → DLQ

Типичная схема на Apache Kafka:

[Producer] → main_topic → [Consumer] → успех? → (да) завершено
                                    ↓ (нет)
                              retry_topic (с задержкой)
                                    ↓
                              [Consumer] → успех? → завершено
                                    ↓ (нет, исчерпаны retry)
                              dlq_topic

Компоненты:

  • main_topic: все входящие запросы на инференс.
  • retry_topic: запросы, которые нужно повторить. Каждое сообщение содержит метаданные: количество попыток, время следующей попытки, исходная ошибка.
  • DLQ_topic: финальные неудачные запросы.

Реализация задержки:

Пример конфигурации топиков (в YAML для Kafka):

topics:
  - name: llm_requests_main
    partitions: 10
    replication: 3
  - name: llm_requests_retry
    partitions: 5
    replication: 3
    config:
      cleanup.policy: delete
      retention.ms: 86400000  # 1 день
  - name: llm_requests_dlq
    partitions: 3
    replication: 3
    config:
      cleanup.policy: compact  # храним последнюю версию для анализа

4. Стратегия повторных попыток (Retry Policy)

Exponential backoff с jitter — стандартный подход:

import random
import time

def next_retry_delay(attempt: int, base_delay: float = 1.0, max_delay: float = 60.0) -> float:
    delay = min(base_delay * (2 ** attempt), max_delay)
    jitter = random.uniform(0, delay * 0.1)  # 10% jitter
    return delay + jitter

Параметры:

  • max_retries: обычно 3–5 для LLM (больше — слишком долго).
  • base_delay: 1–2 секунды.
  • max_delay: 30–60 секунд.
  • retryable errors: список кодов ошибок, которые стоит повторять (429, 500, 503, таймауты).

Пример обработчика на Python с Kafka:

from kafka import KafkaConsumer, KafkaProducer
import json

consumer = KafkaConsumer('llm_requests_main', bootstrap_servers='localhost:9092')
producer = KafkaProducer(bootstrap_servers='localhost:9092')

MAX_RETRIES = 3

for msg in consumer:
    request = json.loads(msg.value)
    try:
        response = call_llm(request['prompt'])
        # успех — сохраняем результат
    except RetryableError as e:
        request['retry_count'] = request.get('retry_count', 0) + 1
        if request['retry_count'] <= MAX_RETRIES:
            delay = next_retry_delay(request['retry_count'])
            request['next_retry_at'] = time.time() + delay
            producer.send('llm_requests_retry', json.dumps(request).encode())
        else:
            producer.send('llm_requests_dlq', json.dumps(request).encode())
    except NonRetryableError as e:
        producer.send('llm_requests_dlq', json.dumps(request).encode())

5. Обработка сообщений из DLQ

После попадания в DLQ возможны три сценария:

5.1 Ручной репроцесс (manual reprocess)

  • Инженер анализирует ошибку через дашборд (Grafana, Kibana).
  • Исправляет причину (например, обновляет API-ключ).
  • Вручную отправляет сообщение обратно в main_topic через UI или скрипт.

5.2 Автоматический репроцесс после фикса (auto-reprocess)

  • Если ошибка связана с временным сбоем (например, перезагрузка модели), можно настроить scheduler, который периодически (раз в час) переотправляет сообщения из DLQ в retry_topic.
  • Важно: добавить TTL (time-to-live) для сообщений в DLQ, чтобы старые запросы не репроцессились бесконечно.

5.3 Алертинг (alerting)

  • При появлении нового сообщения в DLQ отправляется алерт в Sentry, PagerDuty или Slack.
  • Алерт должен содержать: ID запроса, тип ошибки, время, количество попыток.

Пример алерта через Sentry:

import sentry_sdk

sentry_sdk.capture_message(
    f"LLM request failed after retries: {request_id}, error: {error_type}",
    level='error'
)

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

Для эффективного управления DLQ нужно отслеживать:

МетрикаОписаниеИнструмент
DLQ depthКоличество сообщений в DLQPrometheus + Grafana
DLQ ageВремя нахождения сообщения в DLQKafka consumer lag
Retry rateДоля запросов, ушедших на retryСчётчик в приложении
Error distributionРаспределение по типам ошибокSentry / ELK
Reprocess success rateДоля успешных повторных обработок из DLQЛоги

Пороги для алертов:

  • DLQ depth > 100 за 5 минут → warning.
  • DLQ age > 1 час → critical (возможно, проблема не решается автоматически).

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

Не всегда нужен Kafka. Для небольших систем подойдут:

ОчередьВстроенная DLQЗадержкаСложность
RabbitMQDead letter exchange + TTLДа (TTL)Средняя
AWS SQSRedrive policy (maxReceiveCount)Да (visibility timeout)Низкая
Redis StreamsНет встроенной, реализуется через группыНет (нужен consumer)Высокая
Google Pub/SubDead letter topicНет (через subscription)Средняя

Выбор: если у вас уже есть Kafka для других микросервисов — используйте его. Если LLM-сервис изолирован — SQS или RabbitMQ проще.


8. Best practices

  • Идемпотентность: обработчик должен безопасно повторять один и тот же запрос (например, если LLM возвращает тот же результат для того же промпта).
  • Дедупликация: используйте уникальный request_id в каждом сообщении. При повторной отправке проверяйте, не был ли запрос уже обработан (через Redis или БД).
  • TTL для DLQ: сообщения в DLQ не должны храниться вечно. Установите retention (например, 7 дней), после чего они автоматически удаляются.
  • Партиционирование: для Kafka используйте ключ партиции на основе request_id, чтобы гарантировать порядок обработки одного запроса.
  • Разделение ошибок: разные типы ошибок могут направляться в разные DLQ (например, dlq_timeout, dlq_rate_limit) для упрощения анализа.

9. Пример полной реализации (схематично)

# consumer.py
from kafka import KafkaConsumer, KafkaProducer
import json, time, random

consumer = KafkaConsumer('llm_requests_main', group_id='llm_worker')
producer = KafkaProducer()

def process_request(request):
    # вызов LLM
    response = call_llm(request['prompt'], timeout=30)
    return response

for msg in consumer:
    request = json.loads(msg.value)
    try:
        result = process_request(request)
        # сохраняем результат
    except RetryableError as e:
        retry_count = request.get('retry_count', 0) + 1
        if retry_count <= 3:
            delay = min(2 ** retry_count, 60) + random.uniform(0, 2)
            request['retry_count'] = retry_count
            request['next_retry_at'] = time.time() + delay
            producer.send('llm_requests_retry', json.dumps(request).encode())
        else:
            request['final_error'] = str(e)
            producer.send('llm_requests_dlq', json.dumps(request).encode())
    except NonRetryableError as e:
        request['final_error'] = str(e)
        producer.send('llm_requests_dlq', json.dumps(request).encode())

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

Задача: Реализовать DLQ для LLM-сервиса, который вызывает OpenAI API, с обработкой rate limit и таймаутов.

Инструменты: Python, Kafka (или Redpanda), Docker, Prometheus + Grafana (опционально).

Шаги:

  1. Разверните Kafka в Docker (один брокер, три топика: main, retry, dlq).
  2. Напишите producer, который отправляет 1000 запросов с разными промптами (часть из них — заведомо битые, часть — нормальные).
  3. Напишите consumer, который:
    • вызывает OpenAI API (используйте openai.ChatCompletion.create с таймаутом 10 с);
    • при ошибке 429 — retry с exponential backoff;
    • при таймауте — retry до 3 раз;
    • при ошибке 400 (bad request) — сразу в DLQ.
  4. Настройте consumer для retry_topic, который проверяет next_retry_at и спит до нужного момента.
  5. Настройте consumer для DLQ, который логирует ошибки и отправляет алерт в консоль (имитация Sentry).
  6. (Опционально) Добавьте метрики через Prometheus client и визуализируйте глубину DLQ в Grafana.

Ожидаемый результат:

  • Все успешные запросы обработаны.
  • Rate limit запросы повторены и в итоге выполнены.
  • Таймаутные запросы после 3 попыток попали в DLQ.
  • Битые промпты сразу в DLQ.
  • В логах видно, какие запросы и почему ушли в DLQ.

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

ВопросТема
238Как реализовать retry стратегии для LLM вызовов?
239Как обрабатывать ошибки LLM в агентных системах?
241Как мониторить и логировать работу AI-агентов?
242Как обеспечить отказоустойчивость агентного пайплайна?
243Как проектировать fallback для LLM?
245Как тестировать устойчивость агентной системы к сбоям?

Навигация