Архитектурные решения, грабли и RabbitMQ

Привет!

Я Python-инженер. Последние несколько лет я в одиночку строил довольно сложную бэкенд‑систему, и за это время набил немало шишек и нашел, как мне кажется, несколько интересных решений. В этой статье я хочу поделиться не «историей успеха», а конкретными архитектурными проблемами и их решениями при построении высокопроизводительного сервиса на асинхронном Python.

Статья будет полезна тем, кто работает с FastAPI, микросервисами и думает о надежности и масштабируемости своих систем.

1. Проблема: Хаос из 20+ AI-моделей

Все началось с простой бизнес-задачи: предоставить унифицированный доступ к десяткам различных AI-моделей (от OpenAI и Gemini до Sora и Kling). Проблема в том, что каждое API - это свой мир:

  • Разные форматы аутентификации (API-ключи, JWT-токены).

  • Абсолютно разная структура запросов и ответов (image_url vs reference_image).

  • Разное поведение: одни отвечают сразу, другие требуют асинхронного опроса статуса.

Просто "подключать" их по очереди в каждом новом клиенте (например, Telegram-боте) - это прямой путь к неподдерживаемому спагетти-коду. Стало очевидно, что нужно централизованное решение - API-шлюз.

2. Архитектурное решение №1: Декаплинг с FastAPI и RabbitMQ

Для API-шлюза я выбрал FastAPI. Его асинхронная природа идеально подходила для I/O-bound задачи - принять запрос и быстро "перекинуть" его дальше.

Но главная проблема это долгие задачи. Некоторые AI-модели генерируют видео по 3-5 минут. Заставлять клиента ждать столько времени просто безумие.

Мой путь к очередям:

  1. Наивное решение: Сначала я реализовал очередь на MongoDB. API создавал в коллекции документ со статусом pending, а воркеры в бесконечном цикле опрашивали (poll) эту коллекцию. Это работало, но создавало лишнюю нагрузку на БД и было ненадежно.

  2. Правильное решение: Я понял, что изобретаю велосипед, и перешел на RabbitMQ. Это было ключевым решением. API-сервер (Producer) теперь просто публикует сообщение-задачу в очередь и мгновенно отвечает клиенту 202 Accepted. А отдельные воркеры (Consumers) разбирают эту очередь.

  3. Обеспечение надежности: Чтобы задачи не терялись, если воркер падает, я внедрил два стандартных механизма:

    • Acknowledgements (ack/nack): Воркер подтверждает получение сообщения только после успешной обработки. Если он "умирает", сообщение возвращается в очередь.

    • Dead Letter Queue (DLQ): "Сломанные" сообщения, которые вызывают ошибку несколько раз подряд, автоматически отправляются в отдельную очередь для ручного разбора, не блокируя основной поток.

3. Архитектурное решение №2: Правильная база для правильной задачи

Я использовал гибридный подход к хранению данных, и он себя полностью оправдал:

  • MySQL (PostgreSQL): Для всех структурированных, транзакционных данных, где важна целостность: профили пользователей, API-ключи, биллинг, тарифы.

  • MongoDB: Для оперативных, часто меняющихся данных: хранение статусов задач, их параметров и JSON-результатов. Гибкая схема и скорость чтения Mongo здесь подходят идеально.

4. Грабли: Как моя MongoDB раздулась до 8GB и чему меня это научило

А теперь о главной ошибке. На старте, для простоты, я передавал в очередь и сохранял в Mongo изображения в формате Base64. Это работало.

Проблема проявилась через несколько месяцев, когда я увидел, что коллекция с задачами раздулась до 8GB при всего 180,000 записей. Аналитика стала тормозить, бэкапы стали огромными. Я понял, что система скоро "встанет".

Решение: Я провел рефакторинг, внедрив паттерн "Pass by Reference". Теперь API-сервер не передает Base64 в очередь, а:

  1. Сначала загружает файл на S3-совместимое хранилище.

  2. Кладет в очередь и в Mongo только легковесную ссылку (URL) на этот файл.

Это простое изменение сократило размер базы на 90% и в разы ускорило все операции. Урок: никогда не храните тяжелые бинарные данные в оперативной базе данных.

5. Мой подход к разработке: "Дирижер и Оркестр"

Многие спросят, как я справился с этим в одиночку. Я активно использую AI‑ассистентов, но не как «автопилот», а как «оркестр». Моя роль это быть архитектором и дирижером:

  • Я определяю проблему («база раздувается»).

  • Я проектирую решение («нужно вынести файлы на S3»).

  • Я ставлю конкретную техническую задачу («напиши мне сервис для загрузки на S3»).

Этот подход позволяет мне фокусироваться на архитектуре и качестве, делегируя рутинную реализацию.

Заключение

Строить high-load систему в одиночку - это вызов. Ключевые уроки, которые я вынес:

  1. Не бойся использовать промышленные инструменты (как RabbitMQ) с самого начала. Они сэкономят тебе месяцы в будущем.

  2. Выбирай правильное хранилище для правильного типа данных.

  3. Проактивно ищи и исправляй «бутылочные горлышки», а не жди, пока все сломается.


P.S. Да, мне 18 лет, и я понимаю, что мой путь не совсем стандартный. Именно поэтому я и решил поделиться своим практическим опытом — надеюсь, он будет кому‑то полезен, и буду очень благодарен за конструктивный фидбек от более опытных коллег.

Мой GitHub