Практическая ситуация: один входящий запрос порождает обращения к 20 зависимостям. Как оценить нагрузку на каждую зависимость?
Нужно считать не только входящий RPS сервиса, а коэффициент расширения запросов: сколько обращений к конкретной зависимости в среднем порождает один входящий запрос. Для зависимости нагрузка оценивается как входной RPS, умноженный на среднее число вызовов к ней с учётом условных веток, попаданий в кэш и повторов.
Такой расчёт показывает реальную нагрузку на downstream-сервисы, базы данных и очереди. Без него сервис может выглядеть способным обработать трафик, одновременно перегружая одну из зависимостей.
По мере декомпозиции систем один пользовательский запрос всё чаще стал проходить через несколько компонентов. Это позволяет разделять ответственность и независимо масштабировать части системы, но добавляет сетевые вызовы и зависимость между производительностью компонентов.
Проблема fan-out возникла как практическое следствие такой композиции: один внешний запрос превращается в множество внутренних. Поэтому оценка нагрузки должна учитывать не только границу публичного API, но и внутренний граф вызовов.
Пусть сервис получает 5000 запросов в секунду. Если каждый запрос обращается к каталогу четыре раза, к профилю один раз и к базе два раза, то нагрузка на эти компоненты уже отличается от 5000 RPS.
Ситуация усложняется, если часть вызовов выполняется только для некоторых запросов, часть обслуживается кэшем, а временные ошибки вызывают повторы. Особенно опасен высокий fan-out на общий ресурс: небольшое увеличение внешнего трафика может непропорционально увеличить нагрузку на него.
Неверная оценка приводит к исчерпанию пулов соединений, росту очередей, тайм-аутам и каскадной деградации. При этом средний RPS внешнего API может оставаться в пределах ожидаемого.
Для каждой зависимости нужно определить среднее число обращений на один входящий запрос. Базовая формула выглядит так:
RPS зависимости = RPS входного сервиса × среднее число вызовов на запрос × коэффициент повторов × доля запросов, доходящих до зависимости.
Например, при 5000 входящих RPS сервис в среднем выполняет 1,5 обращения к профилю, но только 40% запросов доходят до этого этапа. Без повторов профиль получает примерно 3000 RPS. Если среднее число вызовов включает параллельные обращения, они всё равно учитываются как отдельные операции для оценки нагрузки на зависимость.
Расчёт следует выполнять по нескольким сценариям: обычная нагрузка, пик, деградация кэша и отказ зависимости. Для отказного сценария отдельно учитывают retry amplification — увеличение нагрузки из-за повторных вызовов. Повторы должны иметь ограничение по числу попыток, общий дедлайн и обычно использовать экспоненциальную задержку с джиттером.
Важно считать не только средние значения. Для базы данных и ограниченных пулов ресурсов нужны пиковые значения, распределение по ключам и максимальная конкуренция. Если один внешний запрос запускает 20 параллельных операций, это может быть приемлемо при небольшом RPS, но стать узким местом при массовом одновременном запуске.
Снизить fan-out можно несколькими способами: объединить операции, использовать пакетное чтение, кэшировать стабильные данные, предварительно материализовать представление или изменить API зависимости. У каждого решения есть цена: пакетирование усложняет контракт, кэш создаёт риск устаревших данных, а материализация добавляет задержку распространения изменений и расход хранилища.
Нужно проверять расчёт по наблюдаемым метрикам: RPS и latency по каждому downstream-вызову, число вызовов на входящий запрос, долю попаданий в кэш, количество повторов и ошибки. Среднее число вызовов полезно для планирования, но для защиты системы необходимы также лимиты параллелизма и отказоустойчивое поведение при превышении ёмкости.
API страницы заказа получает 8000 RPS. В среднем оно выполняет шесть обращений к внутренним сервисам, но два из них происходят только для 50% запросов. Один из вызовов часто повторяется при тайм-ауте, а кэш снижает нагрузку на каталог примерно на 70%.
Вариант с оценкой только по 8000 RPS не показывает реальную картину. Более корректно рассчитать нагрузку по каждому вызову отдельно: условный вызов при его выполнении половиной запросов создаёт около 4000 исходных RPS, а вызов каталога после кэширования — только 30% от его потенциальной нагрузки. Для сценария тайм-аутов нужно добавить отдельный расчёт с повторами.
Рассматривались три решения. Увеличить число экземпляров всех зависимостей проще всего, но это не устраняет лишние вызовы и может лишь отложить проблему. Объединить данные в один агрегирующий API уменьшает сетевой fan-out для клиента, но переносит сложность в этот API. Материализовать часто читаемое представление дешевле по latency и нагрузке на чтение, но данные становятся не мгновенно актуальными.
Рациональный выбор — сначала измерить fan-out по трассировкам, затем убрать лишние вызовы и добавить ограничение повторов. Для редко меняющихся данных применить кэш, а для критичного пути оставить только необходимые зависимости. В результате расчёт мощности выполняется по реальной нагрузке downstream-компонентов, а не по внешнему RPS.
1. Нужно ли учитывать параллельные вызовы отдельно от последовательных?
Да. Для нагрузки на зависимость параллельный и последовательный вызов обычно одинаково являются операциями и должны попасть в расчёт RPS. Для latency они различаются: последовательные вызовы складывают задержки, а параллельные частично перекрываются, но ограничиваются самой медленной веткой и накладными расходами координации.
2. Как изменится расчёт при попадании в кэш?
Кэш уменьшает нагрузку на источник только на долю успешных попаданий. Если сервис получает 10 000 запросов, делает один логический запрос к каталогу на каждый, а доля промахов кэша составляет 5%, то каталог в среднем получает около 500 запросов в секунду до учёта обновлений и повторов. Нельзя считать кэш абсолютной защитой: одновременное истечение записей, холодный старт или очистка кэша могут временно вернуть нагрузку к исходному уровню.
3. Почему ограничение повторов является частью оценки нагрузки, а не только механизмом отказоустойчивости?
Потому что повтор создаёт новую работу для зависимости. При двух дополнительных попытках один внешний запрос в худшем случае превращается в три внутренних вызова, а при нескольких слоях повторов эффект может перемножаться по цепочке. Поэтому повторы нужно ограничивать единым дедлайном и бюджетом попыток, иначе отказ зависимости способен породить ещё больший поток запросов и вызвать каскадный отказ.