06.08.2026 · PHP highload · архитектура · ~15 мин чтения

Hybrid sync/async: как генерировать документы под нагрузкой и не ловить таймауты

1. Введение: проблема тяжёлых операций в синхронном мире

Каждому, кто работал с B2B-платформами, знаком этот сценарий. Пользователь нажимает «Сформировать отчёт», и браузер повисает на десятки секунд. Запрос ушёл на сервер, сервер собирает данные, рендерит PDF или Excel — и всё это время пользователь смотрит на спиннер, не понимая, работает система или нет. Через некоторое время он нажимает кнопку ещё раз. Потом ещё раз. И получает три одинаковых отчёта, два из которых он не хотел, а один — не получил вовсе, потому что всё упало.

Это не баг. Это дизайн: тяжёлая операция выполняется синхронно внутри HTTP-запроса.

На высоконагруженной B2B-логистической платформе, которую я проектировал и развивал для «Газпромнефть-Снабжение», генерация документов была одной из критичных операций: полный цикл документооборота — от формирования больших отчётов до подписания через интеграцию со сторонними системами. Здесь цена таймаута выше, чем в обычном веб-приложении: документ — это не украшение интерфейса, а бизнес-процесс клиента. Сорванный срок генерации означает сорванный срок подписания, а значит — задержку сделки и потери для обеих сторон.

Почему операции становятся тяжёлыми

Синхронная генерация перестаёт справляться по трём причинам, и все они действуют одновременно.

Первая — рост объёма данных на операцию. Отчёт — это не «SELECT из одной таблицы». Это выборки по десяткам сущностей, агрегации, форматирование, рендеринг. Чем больше данных накопилось в системе, тем дольше каждая генерация. Операция, которая вчера занимала секунду, сегодня занимает десять — просто потому, что данных стало больше.

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

Третья — интеграции с внешними системами. Полный цикл документооборота не заканчивается на нашей стороне: документ нужно передать, подписать, получить подтверждение. Внешние сервисы бывают медленными, а их API — недостаточно документированными. Когда HTTP-запрос пользователя висит, ожидая ответа чужой системы, мы отдаём управление временем выполнения неподконтрольному нам сервису.

Что происходит, когда это ломается

Последствия синхронного подхода видны на пяти уровнях.

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

Бизнес. Для B2B-клиента документ — часть его собственного процесса. Каждая задержка — это его сорванные сроки, его штрафы по договору, его упущенные сделки. Доверие к платформе падает быстрее, чем успевает расти функциональность.

Инфраструктура. Занятые тяжёлыми генерациями процессы не обслуживают лёгкие запросы. Один долгий запрос отъедает ресурс, который нужен сотне быстрых. Система начинает деградировать целиком, а не только на «тяжёлом» эндпоинте.

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

Команда. Инциденты вместо фич, авралы вместо плановой работы. Команда тушит пожары — и сгорает capacity, которая должна была идти на развитие продукта.

Почему «лечение симптомов» не работает

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

Третье «простое» решение — перевести всё в фон. Оно тоже не работает: пользователю нужен не «статус задачи», а результат — готовый документ. Бизнес-процесс клиента не заканчивается отправкой в очередь. Если мы просто уберём операцию из запроса, но не дадим пользователю способа узнать, когда она выполнилась, — мы заменили таймаут непониманием.

Поэтому задача формулируется так: меньше таймаутов + гарантированное выполнение + видимый статус. Решение, к которому мы пришли, — гибридная синхронно-асинхронная архитектура обработки задач. О ней — дальше.

Гибридная синхронно-асинхронная архитектура: пользователь → веб-приложение (синхронно: приём, валидация, статус) → очередь RabbitMQ → воркеры → статус в БД и уведомление
Гибридная архитектура: синхронный приём и статусы + асинхронная очередь и воркеры

2. Подход: гибридная синхронно-асинхронная обработка

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

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

Критерии «в очередь»

На практике достаточно одного из этих признаков, чтобы операция была кандидатом на асинхронность:

  • время выполнения превышает порог ожидания пользователя (если пользователь готов смотреть на спиннер меньше, чем операция занимает, — это кандидат в очередь);
  • операция зависит от внешних систем с неизвестным SLA;
  • стоимость операции растёт вместе с объёмом данных;
  • операция повторяется на каждый заказ или документ — значит, её суммарная стоимость умножается на объём бизнеса;
  • пользователю не нужен результат немедленно — достаточно уведомления о готовности.

Что остаётся синхронным всегда

Асинхронность — не про «всё перевести в фон». Синхронными остаются: приём запроса, валидация, сохранение данных, чтение статуса задачи, любые лёгкие операции в пределах сотен миллисекунд. Правило простое: UI ждёт только «решение принято», а не «работа выполнена». Пользователь должен получить мгновенный отклик, что его запрос принят и обрабатывается, — и видеть статус выполнения в реальном времени.

Почему RabbitMQ

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

«Быстро сегодня ≠ быстро завтра»

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

Как понять заранее, что станет проблемой? Четыре инструмента:

  1. Мониторинг трендов. Смотрим на латентность p50/p95/p99 по операциям, объём данных, темп роста. Тренд важнее абсолютного значения: если p95 растёт квартал к кварталу — граница смещается.
  2. Capacity planning. Экстраполируем рост, гоняем стресс-тесты и нагрузочное тестирование, чтобы узнать предел до того, как в него упрёмся в проде.
  3. SLA и бизнес-требования. Фиксируем, что критично, какой отклик допустим, и пересматриваем при изменении продуктовых требований.
  4. Архитектурные индикаторы. Операции, растущие вместе с объёмом данных (линейно по данным), операции с внешними зависимостями, операции с блокировками и конкуренцией за ресурсы — кандидаты на пересмотр границы.

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

Экономика решения

Любой архитектурный выбор — это инвестиция, и её нужно считать на горизонте нескольких лет: внедрение (разработка, инфраструктура, обучение), поддержка (инциденты, мониторинг, on-call), сопровождение (обновления, техдолг, доработки). Подход оправдан, когда стоимость решения меньше стоимости проблемы: реальные таймауты, потери бизнеса, SLA. Считать нужно в чел.-днях и в деньгах, а не в «красоте архитектуры». На практике для нашего случая расчёт был простым: стоимость очереди на RabbitMQ оказалась кратно ниже стоимости простоя генерации документов в пиковые периоды.

3. Механизм: очередь задач, у которой есть состояние

Теперь о том, как это устроено изнутри — на уровне, который можно переиспользовать в любом проекте, независимо от конкретного брокера.

Жизненный цикл задачи

Задача проходит путь: приём → постановка в очередь → обработка воркером → уведомление пользователя о готовности. Пользователь инициирует генерацию, система валидирует запрос, сохраняет задачу, публикует её в очередь — и отвечает пользователю: «принято, статус такой-то». Дальше воркер забирает задачу, выполняет генерацию и меняет статус. Пользователь видит результат через интерфейс статусов — без повторных кликов и без гадания.

Задача — это не только сообщение в очереди

Ключевое архитектурное решение: задача — это объект со статусом в базе данных, а не только сообщение в очереди. У задачи есть таблица с состоянием: new → processing → done / failed / retry / cancelled. Очередь — это транспорт и буфер; база данных — состояние, история и контроль.

Зачем два хранилища? Очередь без таблицы не отвечает на вопрос «что сейчас со всеми задачами?» — она умеет только доставлять сообщения. Таблица без очереди не даёт гарантии доставки и буферизации. Вместе они закрывают друг друга: очередь доставляет, база данных помнит. Если воркер упал — задача остаётся в таблице со статусом processing, и механизм контроля вернёт её в работу. Это и есть та самая «гарантированная генерация документов даже при частичных отказах системы»: задача переживает падение отдельного компонента и дорабатывается.

Статусы и переходы: кто и когда переводит

Модель статусов решает три классические проблемы распределённой обработки: гонки, повторную обработку и «зависшие» задачи.

Конечный автомат статусов задачи: new → processing → done/failed; failed → retry с backoff; после лимита → DLQ; processing → new по TTL и heartbeat
Конечный автомат статусов: атомарные переходы, retry с backoff, DLQ, возврат по TTL
  • new → processing: воркер атомарно «забирает» задачу (атомарный UPDATE с условием по статусу). Два воркера не могут забрать одну задачу — это исключает race condition на уровне базы.
  • processing → done / failed: сам воркер по итогу обработки. Failed с возможностью повтора уходит в retry.
  • processing → new: если воркер умер во время обработки, по истечении таймаута (TTL) и heartbeat-контроля задача возвращается в очередь. «Зависших» задач не бывает — есть задачи, у которых истёк срок обработки.
  • new → retry: после неудачи с экспоненциальной паузой; после лимита попыток — в очередь недоставленных (dead-letter) для ручного разбора, а не в бесконечный цикл.

Идемпотентность — обязательное свойство обработчиков: повторная обработка задачи не должна создавать дубли документов. Очередь доставляет сообщения как минимум один раз (at-least-once), значит, повторная доставка — норма, а не авария. Защита одна: уникальные ключи и проверка результата перед созданием нового.

Производитель и потребитель

На стороне производителя важны две вещи. Первая — публикация с подтверждением (publish confirm), чтобы сообщение не терялось при падении брокера. Вторая — атомарность записи задачи и публикации: нельзя записать задачу в базу, но не опубликовать её в очередь (или наоборот). Для этого используется паттерн transactional outbox: событие пишется в outbox-таблицу в той же транзакции, что и бизнес-данные, а отдельный релизер публикует его в очередь — причём только после успешного коммита данных в базу, а не в процессе записи. Это исключает рассинхрон «база и очередь разошлись» — классическую dual-write проблему.

Transactional outbox: бизнес-данные и событие пишутся атомарно в одной транзакции, после COMMIT релизер публикует событие в очередь, потребитель подтверждает обработку
Transactional outbox: атомарная запись данных и события, публикация только после коммита

На стороне потребителя: подтверждение (ack) отправляется только после полного завершения обработки; битые сообщения не возвращаются в цикл, а уходят в dead-letter; воркер не хватает больше сообщений, чем может обработать (prefetch).

Таймауты и контроль

У каждой задачи есть дедлайн — SLA на обработку. Контроль строится на трёх механизмах: watchdog для зависших задач (processing дольше нормы → перезабор), heartbeat воркеров (жив ли обработчик), и метрики очередей (глубина, возраст сообщений, число ошибок). Без этого «асинхронно» превращается в «потерянно»: если очередь молча копится, пользователь узнаёт о проблеме последним.

Именно эти механизмы — контроль и мониторинг генерации документов — стали для платформы гарантией надёжности критически важных бизнес-процессов: документ либо готов, либо у него есть понятный статус и срок, когда он будет готов. Вместе с горизонтальным масштабированием воркеров это обеспечило устойчивость платформы при 100% росте пиковой нагрузки без увеличения времени отклика критических операций.

4. Поведение при нагрузке: от чего растёт нагрузка и что с этим делать

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

Очередь как буфер: продюсеры создают пиковые всплески, очередь сглаживает нагрузку, воркеры горизонтально масштабируются
Очередь как буфер: принимает пик, воркеры масштабируются по глубине очереди

От чего растёт нагрузка

Рост нагрузки надо рассматривать в двух плоскостях, и путать их нельзя.

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

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

Как считать ёмкость

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

Как мерить

Главные KPI очереди — глубина и возраст сообщений (lag): сколько задач ждёт и как долго самая старая из них. Плюс метрики по каждому воркеру: сколько задач обработано за час, сколько ошибок, какое среднее время обработки. Плюс ресурсы брокера и воркеров. Только в совокупности эти цифры дают картину «очередь копится, потому что потребление отстаёт» — а не просто «что-то не так».

Механизмы устойчивости

Из практики системного дизайна здесь работают проверенные механизмы:

  • горизонтальное масштабирование воркеров: рост нагрузки должен означать рост числа обработчиков, а не рост таймаутов;
  • ограничение скорости потребления (backpressure) с настройкой prefetch и батчами;
  • партиционирование для параллельной обработки;
  • сама очередь как буфер между пиками нагрузки;
  • приоритеты — важное обрабатывается раньше;
  • лимиты параллелизма на воркер и таймауты на обработку;
  • изоляция: разные типы задач — в разных очередях, чтобы тяжёлый тип не забирал ресурс у лёгкого;
  • кэширование тяжёлых вычислений;
  • автоскейлинг воркеров по глубине очереди.

Важно помнить и о пределах этих механизмов:

  • горизонтальное масштабирование воркеров упирается в базу данных и внешние системы — их так просто не масштабируешь;
  • backpressure упирается в пользователя — задачи начинают ждать;
  • приоритеты — в голод низкоприоритетных задач;
  • кэш — в инвалидацию.

Знание пределов не менее ценно, чем знание механизмов.

Как проверять до продакшена

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

Именно так проверяется заявленное свойство архитектуры: устойчивость при росте пиковой нагрузки без увеличения времени отклика критических операций.

На платформе эти механизмы — горизонтальное масштабирование воркеров в составе High Availability архитектуры — дали измеримый результат: устойчивая работа при 100% росте пиковой нагрузки без увеличения времени отклика критических операций.

5. Интеграции и внешние системы: очередь как шина данных

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

Схемы взаимодействия

Сервисы общаются по-разному, и выбор схемы — это выбор по характеру операции:

  • синхронный запрос-ответ (REST, gRPC) — когда результат нужен немедленно;
  • fire-and-forget через очередь — «принято, сделаем»;
  • request/reply через очередь с идентификатором корреляции — асинхронный запрос, которому всё же нужен ответ;
  • pub/sub — «произошло событие, кто хочет — реагирует»;
  • сага — цепочка шагов с компенсациями между сервисами;
  • webhooks и polling — для внешних систем, у которых нет своих очередей.

Очередь как шина данных

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

Выбор брокера: RabbitMQ или Kafka

Когда встаёт вопрос «а не Kafka ли?», отвечаю так. Это разные инструменты для разных задач. RabbitMQ — умная маршрутизация (exchanges с разными типами обмена), сложные схемы взаимодействия, гарантии доставки; это task-ориентированный брокер, ему естественно обрабатывать задачи и RPC-подобные обмены. Kafka — лог событий: высокий throughput, партиционирование, возможность перечитать историю с начала (replay), хранение событий (retention) и целая экосистема вокруг. Для потока событий и аналитики — Kafka; для задач с гарантиями и маршрутизацией — RabbitMQ. На практике в системах с обоими классами задач часто используют оба: RabbitMQ для задач, Kafka для событий. В нашей архитектуре задачи живут в RabbitMQ — это соответствует классу решаемых задач.

Контракты и версионирование

Интеграции — это контракты, а контракты версионируются. Схема сообщения (JSON Schema или Avro), обратная совместимость — поля добавляем, но не удаляем; ломающие изменения — только через новую версию контракта и миграцию потребителей. Без этого «расширили поле» превращается в «сломали потребителя» в самый неподходящий момент.

Обработка недоступности внешних систем

Внешние системы падают — это норма, а не авария. Поэтому на вызовы ставятся таймауты, для упавших сервисов — circuit breaker (не долбить упавший сервис бесконечными запросами), для пулов — изоляция (bulkhead). Ретраи с экспоненциальной паузой — только для идемпотентных вызовов, иначе повторная попытка превращается в двойную операцию. И отдельно — безопасность каналов: шифрование между сервисами и брокером, авторизация на брокере, секреты не в сообщениях, санитизация данных, приходящих извне.

Мониторинг живучести каналов

Шина данных требует собственного мониторинга. Health-чеки каналов и брокера, метрики очередей (глубина, возраст, скорость), алерты на задержку и накопление. И главное — процедуры реагирования: когда сработал алерт, что делаем. Резервный брокер и failover, перераспределение нагрузки между воркерами, circuit breaker на потребителях. Канал обмена — это инженерный объект с SLA, а не «что-то между сервисами».

6. Грабли и подводные камни: что мы набили, чтобы вы не набили

Асинхронная архитектура платит за свои преимущества набором ловушек. Собрал самые болезненные — те, что всплывают не в первый месяц, а когда система уже «работает».

Идемпотентность обработчиков. Повторная обработка задачи не должна создавать дубли документов. Брокер доставляет сообщения как минимум один раз: обработчик упал после выполнения, но до подтверждения — задача придёт снова. Это не баг, это свойство. Защита одна — идемпотентность и статусы в базе.

Таймауты внутри воркера ≠ таймауты в UI. Перенося операцию в фон, мы переносим проблему, а не решаем её. Если воркер тоже падает, нужны retry и очередь недоставленных — иначе «асинхронно» превращается в «потерянно с задержкой».

Poison message. Одно битое сообщение зацикливает воркера: упал → retry → упал. Решение — счётчик попыток и очередь недоставленных. Важно не путать временную ошибку (ретраим) с битой (в DLQ).

Retry storm. Ретраи без экспоненциальной паузы и джиттера превращают частичный сбой внешнего сервиса в лавину запросов. Это «вежливый» способ положить сервис, который ещё жив.

Thundering herd. После падения или при старте N воркеров все одновременно хватают задачи — удар по базе и внешним системам. Лечится лимитами параллелизма и настройкой prefetch.

Claim без heartbeat. Воркер забрал задачу и умер — задача навсегда висит в processing. Обязательны TTL возврата и heartbeat: задача должна возвращаться в очередь, если обработчик не подаёт признаков жизни.

Запись и публикация неатомарно. Записали в базу, но не опубликовали — или опубликовали, но не записали. Классический рассинхрон базы и очереди, решается паттерном Outbox: событие пишется в той же транзакции, что и бизнес-данные.

Порядок и параллелизм. Параллельные воркеры ломают порядок обработки; зависимые документы (один генерируется из другого) начинают конфликтовать. Решение — ключ упорядочивания или ограничение параллелизма по ключу задачи.

Побочные эффекты при повторе. Retry не должен повторять уведомления пользователю, записи во внешние системы, повторные интеграции. Идемпотентный ключ на каждый побочный эффект.

DLQ без мониторинга. Очередь недоставленных молча копится — это тихая потеря задач. Алерт на рост DLQ обязателен, разбор DLQ — регулярная процедура, а не «когда-нибудь».

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

Раздувание сообщений. Класть в сообщение большие документы и бинарные данные — деградировать очередь. Решение — Claim Check: в очереди ссылка, данные в хранилище.

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

Рост очереди без лимитов. При сбое потребителя очередь растёт бесконечно — диск и память заканчиваются. TTL сообщений, лимиты глубины, политики переполнения.

Дубли на входе. Пользователь дважды нажал «сгенерировать» — две задачи. Дедупликация на входе по idempotency key из контекста запроса.

7. Метрики результата: как понять, что всё работает

Асинхронная система требует асинхронного контроля: пока задача в очереди, «всё работает» — это не факт, а гипотеза. Метрики превращают гипотезу в факт.

Прямые метрики очередей

Глубина очереди, возраст сообщений (lag), скорость обработки, время обработки, число ошибок, размер очереди недоставленных. Это базовый пульс: если глубина растёт при стабильной скорости потребления — потребление отстаёт.

Косвенные сигналы проблем

Самое ценное — косвенные параметры, которые сигнализируют о проблеме до того, как она стала инцидентом. Рост p95/p99 времени обработки. Рост числа повторных попыток. Задачи в статусе processing дольше нормы — зависшие. Деградация внешних сервисов по latency и error rate. Глубина очереди растёт при стабильной скорости потребления. И — часто недооценённый сигнал — рост повторных действий пользователей: люди не нажимают дважды без причины, и причина обычно на нашей стороне.

Контроль выполнения по времени

Помимо дашбордов в реальном времени, нужны периодические проверки: раз в сутки и раз в неделю — выгрузка задач за период, анализ незавершённых, старых и упавших. Это «сверка с действительностью»: что должно было выполниться против того, что выполнилось (reconciliation). И SLO на обработку — у задачи есть норматив времени выполнения, и нарушение норматива — это алерт, а не статистика.

Механизмы контроля (что чаще всего работает)

Дашборды с алертами по порогам (Prometheus/Grafana и аналоги). Heartbeat воркеров и детекция «зависших». Ретраи с паузой, DLQ с отдельным мониторингом. Журнал статусов задач — аудит-трейл, по которому можно ответить «что случилось с документом» за минуту, а не за день. И регулярные отчёты по очередям — ежедневный и еженедельный. Именно эти механизмы контроля и мониторинга генерации документов сделали процесс предсказуемым: документ либо готов, либо у него есть статус и срок.

8. Выводы и критерии выбора

Гибридная синхронно-асинхронная архитектура — не компромисс между «плохо синхронно» и «сложно асинхронно», а отдельная схема с понятными гарантиями. Правило выбора простое: синхронно — то, что пользователь ждёт и что должно быть быстрым; асинхронно — всё тяжёлое, долгое и зависящее от внешних систем. UI ждёт «решение принято», а не «работа выполнена».

Экономика решения

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

Сравнение подходов: что выбрать

Критерий Синхронно Асинхронно (очередь) Гибрид
Внедрение Низкое: минимум нового кода Высокое: брокер, статусы, DLQ Среднее: две парадигмы
Поддержка Низкая: мало движущихся частей Высокая: мониторинг, on-call Средне-высокая
Сопровождение Простое Дорогое: брокер, техдолг Среднее: дисциплина границы
Риски Таймауты при росте Потери задач, дубли Размытая граница
Когда выбирать ≤100–200 мс, не растёт с данными Тяжёлое, долгое, внешнезависимое Пользователь ждёт часть результата

Чек-лист внедрения

Карта операций по времени → решение о границе sync/async → очередь с retry и мониторингом → идемпотентные воркеры → метрики очередей. Пять шагов, каждый из которых проверяется отдельно. Если после внедрения метрика не сдвинулась — вы строили не то.

На платформе этот путь дал измеримый результат: меньше таймаутов в веб-интерфейсе, гарантированное выполнение фоновых операций и генерация документов даже при частичных отказах системы — при 100% росте пиковой нагрузки без увеличения времени отклика критических операций. Это не «красивая архитектура», это бизнес-метрики, которые можно показать цифрами.

Обсудить вашу задачу

Проектируете обработку тяжёлых операций или боретесь с таймаутами? Проведу architecture review: где sync, где async, где очереди. Отвечаю в течение 24–48 часов.