К содержимому
Завалищев
База знаний
Инструкции 6 мин чтения

Тихая катастрофа: как enable_auto_commit=True уничтожил RAG-базу без единой ошибки

Параметр enable_auto_commit=True в Kafka привёл к безвозвратной потере документов из RAG-пайплайна. Разбираю механику сбоя, объясняю почему стандартный try/except не спасает, и даю конкретные паттерны защиты.

Худший баг — тот, которого не видно

RAG-пайплайн работает. Логи чистые. Мониторинг молчит. Пользователи загружают документы — они появляются в хранилище.

А потом кто-то ищет документ, загруженный вчера. Его нет в индексе. И позавчерашнего нет. И за прошлую неделю — тоже.

Документы физически лежат в MinIO. Но в векторную базу они никогда не попали. Kafka честно отметил их как «обработанные». Qdrant их никогда не видел. Именно это произошло в open-source RAG-пайплайне Aegis — один конфигурационный параметр молча уничтожал данные на протяжении неизвестного времени.

Архитектура: где прячется проблема

Стандартный production RAG: Spring Boot gateway принимает файл в MinIO → Kafka доставляет событие Python-воркеру → ML worker генерирует эмбеддинги → Qdrant принимает векторы.

Проблема возникла на стыке воркера и Qdrant. Но сначала был первый баг, запустивший цепочку.

Первый баг: 10 МБ → 62 МБ за одну строку кода

Наивный сплит text.split(" ") при бинарном файле на 10 МБ создавал один гигантский «чанк» — в бинарных данных нет пробелов. При JSON-сериализации каждый нулевой байт превращается в экранированную Unicode-последовательность — 6 символов вместо 1. Множитель 6x: 10 МБ × 6 = 60 МБ + вектор и заголовки = 62 922 836 байт.

Лимит Qdrant REST API — 32 МБ. HTTP 400. Соединение разорвано.

Исправление: RecursiveCharacterTextSplitter из LangChain. Разрезает по абзацам → переносам строк → пробелам, сохраняя логические границы и гарантируя предсказуемый размер чанков.

Второй баг: настоящая катастрофа

Конфигурация Kafka-консьюмера содержала параметр по умолчанию: enable_auto_commit = True.

Что это значит: Kafka автоматически, по таймеру, сдвигает offset — отмечая сообщение как «обработанное» — независимо от того, завершилась ли обработка успешно.

Что происходило при каждом сбое:

  • Kafka доставляет сообщение воркеру
  • Воркер пытается сделать upsert в Qdrant → 400 Bad Request
  • Блок except логирует ошибку
  • Таймер auto-commit срабатывает и сдвигает offset
  • Kafka считает документ обработанным, повторной доставки не будет — никогда

Документ существует в MinIO. В векторной базе его нет. Поиск его не найдёт. Никакой системе об этом не известно.

Почему try/except здесь не работает

Стандартный try/except ловит исключение, логирует его и продолжает работу. В контексте Kafka с auto-commit «продолжить работу» означает «потерять сообщение навсегда». Лог-запись существует, но в production с тысячами документов в день строка «Failed to process document abc123» утонет в шуме. Retry-механизма нет. Документ исчез.

Решение: Dead Letter Queue

При любой ошибке событие уходит в отдельный топик aegis.documents.failed вместе с оригинальным JSON-событием, полным стектрейсом и Correlation ID. Когда инфраструктура восстанавливается — DLQ проигрывается заново. Ноль потерь данных.

Пять паттернов защиты от деструктивных дефолтов

1. Отключайте auto-commit, управляйте offset вручную

enable_auto_commit=False + consumer.commit() только после успешной обработки. При исключении — send_to_dlq(message), без коммита.

2. DLQ для каждого консьюмера

Не бывает пайплайна, где ошибки невозможны. Каждый Kafka-консьюмер должен иметь DLQ — это требование, не опция.

3. Мониторьте расхождение хранилище vs индекс

Периодически сравнивайте количество документов в object store с проиндексированными в векторной базе. Расхождение > 0 — алерт.

4. Не доверяйте дефолтам брокеров

enable_auto_commit → False, acks → all, enable.idempotence → True. Дефолты Kafka оптимизированы для throughput, а не для надёжности.

5. Хаос-тестирование обязательно

Убейте базу данных во время записи. Оборвите сеть во время эмбеддинга. Если пайплайн теряет данные — он не готов к production.

Бонусные грабли из того же проекта

  • Тег :latest в Docker — MinIO-бакет исчез после обновления, потому что mc latest обновился молча. Вывод: пинить версии всегда.
  • Зависший Kafka-тред — CTRL+C не останавливал сервер: консьюмер в run_in_executor() игнорировал SIGINT. Нужен threading.Event() как kill switch.
  • Запятая в имени файла — Windows curl падал на "Book, Version 2.pdf": запятая интерпретировалась как разделитель HTTP-заголовков.

Выводы

enable_auto_commit=True — не баг Kafka. Это дефолт ради простоты. В туториалах работает идеально. Production — не туториал.

Самый опасный баг — не тот, который кладёт систему. А тот, который работает тихо, пока кто-то не заметит, что половина документов исчезла.

Проверьте свои Kafka-консьюмеры прямо сейчас. Grep по enable_auto_commit. Если видите True или не видите параметр вообще (дефолт — True) — у вас потенциальная бомба замедленного действия.