uncloth.app Гайды

Масштабирование генерации изображений: очереди, идемпотентность, ретраи

Генерация изображений ломает допущения, на которых построено большинство веб-архитектур. Запросы идут десятки секунд, а не миллисекунды; мощность действительно конечна, потому что она физическая; а неудавшийся запрос уже стоил реальных денег. Паттерны, которые держат это корректным, хорошо известны — просто их обычно нет в типовом CRUD-бэкенде.

Очередь и есть архитектура

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

Поставьте посередине устойчивую очередь. «Устойчивая» здесь ключевое слово: список в памяти теряет все задачи в работе при деплое, а деплои случаются под трафиком. Таблица в базе на этом масштабе прекрасно справляется и попутно даёт восстанавливаемую запись состояния.

Backpressure — это функциональность

С очередью вы можете решить, сколько задач допускается в работе одновременно, и этот лимит — продуктовое решение, а не техническая деталь. Слишком высокий — и всем медленно. Слишком низкий — и мощность простаивает, пока копится очередь. Важно, что лимит существует и применяется в одном месте: тогда всплеск трафика удлиняет очередь, а не деградирует каждый запрос.

Идемпотентность и неопределённый сбой

Деньги стоит не ответ с ошибкой — с ошибками всё просто. Стоит неопределённость: запрос ушёл из вашего процесса, соединение оборвалось, и вы не можете сказать, создалась ли задача. Повторите — и, возможно, заплатили дважды. Не повторите — и пользователь, возможно, заплатил впустую.

Ключ идемпотентности снимает это, делая ретрай безопасным: тот же ключ возвращает исходную задачу вместо создания второй. Правила просты и их легко нарушить: генерируйте ключ в момент действия пользователя, а не в момент HTTP-вызова; храните его вместе с записью задачи; переиспользуйте при каждом ретрае этого действия.

# один и тот же ключ на все ретраи одного действия пользователя
KEY=$(uuidgen)
curl -X POST https://api.example/api/v1/jobs \
  -H "X-API-Key: $API_KEY" -H "Idempotency-Key: $KEY" \
  -F image=@photo.jpg -F feature=undress -F consent=confirmed

Что не стоит ретраить автоматически

Есть категория сбоев, где безопасный ход — остановиться. Если процесс упал между отправкой запроса и записью ответа, вы действительно не знаете, что произошло, — и без ключа для дедупликации автоматический ретрай превращается в подбрасывание монетки, которое может списать деньги дважды. Помечайте такие задачи как требующие внимания вместо слепого повтора. Видимая зависшая задача — лучший исход, чем тихое двойное списание.

Сверка состояния: цикл, который вас спасает

Живые события удобны и ненадёжны. Сокеты рвутся, процессы перезапускаются, и единственное важное сообщение приходит, когда никто не слушает. Лечится фоновым циклом, который периодически спрашивает у API про каждую задачу, которую ваша база всё ещё считает активной, и обновляет состояние по ответу.

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

Прогресс без долбёжки по базе

Генерация отдаёт прогресс часто — потенциально несколько раз в секунду на задачу. Запись каждого тика в базу умножает нагрузку на запись, не добавляя информации. Ограничьте запись примерно одним разом в секунду и всегда пишите финальное состояние, а живые значения транслируйте подключённым клиентам вообще без обращения к хранилищу.

Чек-лист, который выдерживает продакшн

  • Устойчивая очередь между пользователями и API, восстанавливаемая после рестарта.
  • Явный лимит задач в работе, применяемый в одном месте.
  • Ключ идемпотентности, созданный на действие пользователя, сохранённый и переиспользуемый при ретрае.
  • Неопределённые сбои отправки помечаются, а не ретраятся автоматически.
  • Цикл сверки, который сводит состояние независимо от пропущенных событий.
  • Ограниченная запись прогресса, живой прогресс через сокет.

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

Частые вопросы

Нужен ли брокер сообщений?

На этом масштабе обычно нет. Таблица в базе с запросом на захват задачи даёт устойчивость и восстанавливаемую историю состояния без ещё одной движущейся детали.

Сколько задач держать в работе одновременно?

Начните с малого, сравнивайте время ожидания в очереди со временем выполнения и поднимайте лимит, пока задержка перестанет улучшаться. Само число важно меньше, чем наличие одного применяемого лимита.

Должны ли упавшие задачи ретраиться автоматически?

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

Разрабатывайте на uncloth.app

Один REST-эндпоинт, одиннадцать пресетов, результат приходит на ваш бэкенд. Расскажите, что вы делаете, и мы вышлем доступы.

Запросить доступ

Все руководства