Билол Саидумаров
Все статьи

Три ошибки, когда разносишь пайплайн в очередь

19 мая 2024 · 5 минут · архитектура, очереди, миграция
Если коротко
  • Один честный сигнал, после которого синхронный пайплайн становится дороже асинхронного.
  • Минимальная архитектура «шлюз + очередь + воркер» — три компонента, без героики.
  • Три ошибки миграции, которые делал я. Все три — про предположение, что «ничего не пойдёт не так».
Изображение · hero · 1600×900
HTTP-шлюз слева; очередь — как пневмопочта; справа два воркера, один работает, второй ждёт.
Minimalist isometric illustration: HTTP gateway on the left passing a small parcel into a horizontal queue (pneumatic-tube style), the queue feeding two worker servers on the right. One worker glows softly (processing), the other is idle. Calm slate-to-indigo gradient, no logos, no readable text, clean line work, 16:9, 1600x900.

Сначала вы держите весь пайплайн в одном HTTP-обработчике. Это нормально. Это даже правильно — пока работает.

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

Когда пора

Сигнал один: обработка одного запроса длится дольше, чем клиент готов ждать. Для веб-API это 2-3 секунды.

Не миграция ради миграции. Не «у всех очереди — давайте и мы». Раздельные процессы — это операционная сложность. Платите её, когда выгода становится больше боли. Не раньше.

Минимальная архитектура

Три компонента, ничего больше:

  1. Шлюз (FastAPI). Принимает запрос, валидирует, пишет в БД «задача создана» со статусом queued, кладёт id в очередь, возвращает 202 с этим id.
  2. Очередь (RabbitMQ). Один durable queue. Сообщение — JSON с job_id и document_id. Никакого крупного контента — он уже в БД.
  3. Воркер. Читает сообщение, по id поднимает данные, прогоняет пайплайн, обновляет статус.

Клиент пингует GET /jobs/{id} или (лучше) дашборд сам опрашивает статус и обновляет UI. WebSocket в первой версии — это понты, отложите.

Ошибка 1. Полезная нагрузка в сообщении

Я начинал с того, что клал в сообщение «удобно всё, что пригодится воркеру».

Сообщение разрастается, RabbitMQ упирается в память, ретраи тащат гигабайты по сети — потому что «удобно». В сообщении должен быть только id. Всё остальное — поднимается из БД.

Ошибка 2. Молчаливое предположение об «exactly once»

Сообщение могут доставить дважды. Это нормально, это by design.

Воркер обязан проверять статус задачи и пропускать уже выполненные. Иначе на каждом ретрае получаете дубликаты в индексе и потом долго объясняете аналитикам, почему документов стало больше, чем было загружено.

Ошибка 3. Воркер падает молча

Если воркер уронился в середине, задача остаётся в статусе running навсегда. Очередь думает, что её взяли, БД — что с ней работают. Все вежливо ждут.

Лечится одним из двух: heartbeat от воркера, либо правило «протухшие running старше N секунд может подобрать другой воркер». Без этого ваш самый страшный баг — отсутствие багов, при котором ничего не происходит.

Что сделать дальше
  • Замерьте p95 обработки одного запроса. Меньше 2 секунд — не торопитесь с очередью.
  • Больше — добавьте таблицу processing_jobs с явной state-машиной (queued / running / succeeded / failed).
  • Дальше — «Когда LLM падает, сервер не должен падать с ним»: про каскадные падения внутри воркера.