Саги под нагрузкой: как получить цифры до и после
Оптимизация саги редко ускоряет один запрос. Она меняет время удержания блокировок, число повторов и стоимость компенсаций. Разбираем, что измерять до изменений, какие узкие места искать и как доказать результат цифрами.

Сага, которая обрабатывает заказ за 200 мс в спокойном тесте, может занимать секунды под нагрузкой: один сервис ждёт таймаут, другой повторяет команду, третий запускает компенсацию. Поэтому сравнивать только время HTTP-ответа опасно. В этой статье — методика замеров «до и после» для саг на Apache Kafka 3.x и PostgreSQL 16, без привязки к конкретному фреймворку и с акцентом на то, что ломается первым.
Сначала измерьте не запрос, а жизненный цикл саги
У саги есть как минимум два времени. Первое — задержка ответа клиенту. Второе — время от принятия бизнес-команды до конечного состояния: completed, failed или compensated. Оптимизация первого показателя может ухудшить второй, если сервис начал отвечать сразу, но оставил длинный хвост событий.
Для каждого экземпляра саги нужен сквозной идентификатор. Он должен проходить через команду, события, записи о состоянии и компенсационные действия. Без него вы увидите задержку Kafka отдельно, запросы PostgreSQL отдельно, а связь между ними придётся восстанавливать вручную.
Минимальный набор метрик
- время бизнес-операции: от старта саги до терминального состояния, отдельно p50, p95 и p99;
- время каждого шага и компенсации, включая ожидание очереди;
- доля успешных саг, ошибок, повторов и компенсаций;
- возраст самого старого необработанного сообщения и размер очереди;
- количество запросов к базе на одну сагу, время ожидания блокировок и число откатов;
- нагрузка на CPU, память, соединения с базой и брокером.
p95 означает значение, быстрее которого завершаются 95% операций. Оставшиеся 5% важны именно для саг: в них обычно попадают таймауты, повторы и компенсации.
Цифра «сага завершилась за 80 мс» ничего не значит, пока не указано, сколько из этих 80 мс пришлось на ожидание очереди, блокировку базы и повторную доставку.
Как поставить честный эксперимент
Сравнение до и после имеет смысл только при одинаковых условиях. Иначе вы сравниваете не оптимизацию, а два разных запуска.
- Зафиксируйте сценарий. Запишите размер заказа, число позиций, число участников саги, долю успешных и ошибочных операций, размер сообщения и уровень параллельности. Отдельно задайте сценарий с отказом одного из шагов.
- Прогрейте систему. Первый запуск может включать создание соединений, загрузку кода и заполнение кешей. Не смешивайте его с рабочими измерениями.
- Снимите базовую линию. Сохраните p50, p95, p99, пропускную способность, долю компенсаций и задержку очереди. Для PostgreSQL добавьте время ожидания блокировок, для Kafka — задержку потребителей.
- Измените одну группу причин. Например, сначала только идемпотентность и outbox, затем только лимит параллелизма. Иначе вы не узнаете, что именно дало эффект.
- Повторите тот же прогон. Используйте то же железо, объём данных, число потребителей и профиль ошибок. Сравнивайте не только среднее, но и хвост p95/p99.
Результат считается пригодным, если вы можете ответить на три вопроса: что изменилось в коде или конфигурации, какой показатель должен был измениться и не выросла ли цена компенсации.
Что обычно даёт прирост
1. Уберите длинные транзакции из границ саги
Локальная транзакция участника должна защищать запись конкретного сервиса, а не удерживать соединение и блокировки, пока сага ждёт другой сервис. Команда записывается локально, фиксируется, а дальнейшая доставка выполняется отдельно. Это сокращает время блокировок, но добавляет требования к повторной обработке и согласованности.
Сравнивайте не только длительность саги, но и максимальное время блокировки строки. Если p99 саги почти не изменился, а блокировки сократились, вы всё равно получили важный результат: система стала устойчивее к конкуренции.
2. Сделайте доставку повторяемой
Сбой после записи в базу, но до публикации события создаёт рассинхронизацию. Для этого применяют transactional outbox: событие и изменение состояния записываются в одной локальной транзакции, а отдельный процесс доставляет событие в брокер. Такой подход добавляет запись и чтение из таблицы outbox, зато уменьшает риск потерянного события.
Измеряйте возраст необработанных записей outbox и долю повторных публикаций. Если очередь растёт, ускорение базы само по себе не решает проблему: ограничением может быть брокер, сеть или число потребителей.
3. Ограничьте параллелизм, а не увеличивайте его вслепую
Больше потребителей не всегда означает больше пропускной способности. Они могут конкурировать за одни строки, исчерпывать пул соединений PostgreSQL или перегружать сервис, который выполняет компенсацию.
Подбирайте параллелизм по измерениям. Увеличивайте его ступенчато и фиксируйте пропускную способность, p99, ошибки и ожидание блокировок. Точка, после которой throughput почти не растёт, а p99 растёт, — кандидат на рабочий лимит.
4. Отделите обычный поток от компенсаций
Компенсация — не откат распределённой транзакции. Это новая бизнес-операция, которая тоже может завершиться ошибкой и потребовать повтора или ручного вмешательства. Если отправить её в ту же очередь без приоритета, всплеск отказов способен задержать обычные заказы.
Практичный вариант — отдельная очередь или отдельный лимит потребителей для компенсаций. Цена решения — дополнительная эксплуатация и необходимость следить за двумя потоками. В замерах смотрите, как быстро очищается очередь компенсаций после искусственного отказа.
| Подход | Что меняет | Что измерять | Цена |
|---|---|---|---|
| Короткие локальные транзакции | Уменьшает удержание блокировок | Время блокировок, p99, откаты | Больше промежуточных состояний |
| Transactional outbox | Связывает запись состояния и событие | Задержка outbox, повторы доставки | Таблица, воркер и очистка данных |
| Ограниченный параллелизм | Снижает конкуренцию за ресурсы | Throughput, p99, пул соединений | Возможное снижение пиковой скорости |
| Отдельный поток компенсаций | Изолирует ошибки возврата | Возраст очереди компенсаций, время восстановления | Две очереди и отдельные алерты |
Как показать эффект в отчёте
Таблица «до/после» должна содержать условия эксперимента, а не только красивые проценты. Если реальных замеров ещё нет, не подставляйте примерные значения. Заполните их после прогона.
| Показатель | До | После | Как интерпретировать |
|---|---|---|---|
| p95 полного цикла | измерение | измерение | Главный показатель хвоста задержки |
| Доля компенсаций | измерение | измерение | Не должна расти из-за новых таймаутов |
| Возраст outbox | измерение | измерение | Показывает отставание доставки |
| Ожидание блокировок | измерение | измерение | Помогает отделить базу от брокера |
| Время восстановления после отказа | измерение | измерение | Важнее среднего времени в аварийном сценарии |
Для относительного изменения используйте формулу: (значение до − значение после) / значение до × 100%. Для ошибок и компенсаций знак нужно трактовать отдельно: снижение задержки хорошо, но снижение числа компенсаций может означать и потерю событий, если наблюдаемость неполная.
Где оптимизация ломается
Повторы превращаются в дубли
Повтор команды безопасен только при идемпотентной обработке. Участник должен распознавать уже применённую операцию по устойчивому идентификатору, а не по времени получения сообщения. Иначе повторное списание, резервирование или отправка уведомления станет бизнес-ошибкой.
Компенсация не является симметричным откатом
Если товар уже передан перевозчику, простого «отменить резерв» недостаточно. Нужен отдельный процесс возврата, а иногда — ручная очередь. В модели состояний заранее выделите окончательные и промежуточные статусы, иначе мониторинг будет считать зависшую сагу успешной.
Таймауты скрывают медленный сервис
Короткий таймаут уменьшает время ожидания одного шага, но увеличивает число повторов и компенсаций. Длинный таймаут держит ресурсы и ухудшает хвост задержки. Подбирайте его по распределению фактических задержек конкретного участника, а не по среднему значению.
Метрики агрегированы слишком грубо
Средняя длительность может улучшиться, пока p99 ухудшается в несколько раз. Разделяйте метрики по типу саги, версии схемы события, результату шага и причине отказа. Полный идентификатор саги храните в трассировке и логах, но не добавляйте его как высококардинальный label в метрику Prometheus.
Следующий шаг после замеров
Сначала установите базовую линию на одном сценарии и одном участнике саги. Затем добавьте трассировку от команды до терминального события, алерт на возраст outbox и отдельный алерт на зависшие компенсации. После каждого изменения прогоняйте штатный и отказоустойчивый сценарий, а в проде наблюдайте p99 полного цикла, а не только ответ API.
Материал ориентирован на Apache Kafka 3.x и PostgreSQL 16. Названия таблиц, клиентов и параметров зависят от выбранного фреймворка; перед внедрением сверяйте их с документацией конкретной версии.
IT-архитектор / SQL-оптимизатор: Поиск N+1, оптимизация запросов, code review, поиск багов.
Спросить за 20 ₽Источники: Saga pattern — microservices.io, Saga design pattern — Microsoft Azure Architecture Center
Частые вопросы
Нужно ли всегда использовать саги вместо распределённых транзакций?
Какой показатель считать главным при оптимизации?
Что делать, если после увеличения числа потребителей система стала медленнее?
Можно ли считать компенсацию обычным откатом транзакции?
Что делать, если после оптимизации выросло число повторов?
Подойдут ли эти рекомендации для другой версии Kafka или PostgreSQL?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.