Как спроектировать API и архитектуру сервиса генерации отчетов с большими изображениями с учетом масштабируемости и возможных проблем
Что проверяют
Понимаете ли вы, что тяжёлую генерацию нельзя делать внутри HTTP-запроса и что большие изображения упираются в память и сеть, а не в процессор. Следом обычно спрашивают, что будет, если воркер упадёт посреди отчёта, и как не сгенерировать один отчёт дважды.
Как отвечать
Начните с уточнений: размер изображений, частота запросов, формат результата, допустимое время ожидания. Потом каркас:
- асинхронный API:
POST /reportsпринимает параметры и заголовокIdempotency-Key, отвечает202 Acceptedс идентификатором задачи.GET /reports/{id}отдаёт статус, по готовности ссылку на файл. Вместо опроса можно вебхук; - загрузка изображений: клиент кладёт файлы напрямую в S3-совместимое хранилище по presigned URL, большие через multipart. Гонять гигабайты через API, тем более base64 в JSON, не нужно;
- очередь и воркеры: Celery, Dramatiq или arq с брокером на Redis или RabbitMQ. API на FastAPI только ставит задачу, воркеры масштабируются отдельно по длине очереди;
- память: изображения ужимаются до разрешения печати перед вставкой (
Image.thumbnailв Pillow), файлы читаются потоком. В Celeryworker_max_memory_per_childиworker_max_tasks_per_childперезапускают разросшийся процесс; - результат: готовый файл в хранилище с TTL, отдача по короткоживущей подписанной ссылке, в базе статус и метаданные;
- надёжность: задача идемпотентна,
acks_lateплюс ограниченные ретраи с backoff, у задачи есть таймаут, упавшие насовсем попадают в отдельную очередь.
Отдельно назовите безопасность: проверка типа и размера файла, лимит Image.MAX_IMAGE_PIXELS в Pillow против «бомбы распаковки», доступ к отчёту только владельцу.
Где спотыкаются
Генерируют отчёт синхронно в обработчике. На первом тяжёлом запросе упираются в таймаут прокси, клиент повторяет запрос и запускает вторую генерацию.
Забывают про память: десяток воркеров держит несжатые изображения по сотне мегабайт, и контейнер убивает OOM killer.
Рисуют микросервис на каждую функцию. Для такой задачи обычно хватает API, очереди, воркеров и хранилища, а дробление без причины добавляет сетевых отказов.