Сколько запросов вытянет API за реверс-прокси: ищем узкое звено всей цепочки
Клиент спрашивает: «Сколько RPS выдержит наш API?» — и обычно в ответ звучит цифра из документации nginx или бенчмарка фреймворка, к продакшену отношения не имеющая. Реальная цепочка запроса — это клиент, реверс-прокси, приложение и база данных, и её потолок определяется не суммой возможностей каждого звена, а самым слабым из них. Разберём, как найти это звено методически, а не гаданием по логам после очередного падения.
Содержание
Почему сумма пределов звеньев — не предел системы
Частая ошибка — сложить паспортные цифры каждого компонента и решить, что система выдержит минимум из них. На бумаге это выглядит логично: nginx держит десятки тысяч соединений, приложение на условном FastAPI с пулом воркеров отвечает на тысячи запросов в секунду в синтетическом тесте, PostgreSQL с быстрым NVMe и настроенным max_connections тоже показывает солидные цифры в изолированном бенчмарке. Проблема в том, что эти цифры получены в разных условиях, с разной длиной запроса, разным размером ответа и без учёта того, что все три компонента работают одновременно и делят один и тот же CPU, одну и ту же сеть, а иногда и один и тот же сервер.
Реальный предел цепочки — это RPS, при котором первое звено начинает деградировать: расти по латентности нелинейно, отбрасывать соединения или уходить в очередь без возврата. Все звенья после него в этот момент ещё далеки от своего потолка, но общая пропускная способность уже упёрлась. Найти это звено — не значит найти самый медленный компонент вообще, а значит найти тот, который первым перестаёт линейно масштабироваться при росте нагрузки на конкретный сценарий использования.
Здесь же стоит разделить два разных вопроса, которые часто путают: «сколько запросов система обработает без ошибок» и «сколько запросов она обработает с приемлемой латентностью». Это разные пределы, и второй почти всегда наступает раньше первого. Прокси может продолжать принимать соединения и не отдавать 502 ещё долго после того, как p95 латентность выросла в разы — просто потому, что очередь на бэкенде растёт, а не переполняется мгновенно.
Методика: нагрузочное тестирование с изоляцией по этапам
Тестировать всю цепочку сразу — правильный первый шаг, но он даёт только суммарную цифру, не объясняющую, где именно упор. Рабочая методика — движение от целого к частям:
- Базовый прогон через полную цепочку. Нагружаете публичный эндпоинт через реверс-прокси инструментом вроде
wrk,heyилиk6, фиксируете RPS, p50/p95/p99 латентность и долю ошибок при плавном росте нагрузки (не сразу на максимум — ступенями). - Исключение прокси. Тот же сценарий, но нагрузка идёт напрямую на порт приложения, минуя nginx/Caddy/HAProxy — либо с того же хоста через
127.0.0.1, либо временно пробросив порт. Если цифры почти не изменились — прокси не узкое звено на этом профиле нагрузки. Если RPS заметно вырос или латентность упала — часть проблемы в прокси. - Исключение приложения. Заменяете реальный обработчик заглушкой, которая сразу отдаёт фиксированный ответ без похода в БД, и гоняете тот же трафик через прокси. Так вы измеряете потолок связки «прокси + сеть + минимальное приложение» отдельно от бизнес-логики.
- Изоляция БД. Нагружаете базу напрямую — через
pgbenchдля PostgreSQL или аналогичный инструмент для вашей СУБД — запросами, похожими по паттерну на реальные (тот же набор JOIN, та же выборка, тот же объём данных, а неSELECT 1). - Сведение картины. Сопоставляете четыре цифры: полная цепочка, прокси-минус, приложение-заглушка, БД отдельно. Слой, на исключении которого суммарный RPS подскочил сильнее всего, и есть узкое звено — с оговоркой, что после его расширения потолок может тут же упереться в следующий по слабости слой.
Пример команды для базового прогона:
wrk -t8 -c400 -d60s --latency https://api.example.com/v1/orders
Ключевое здесь — --latency, без него вы увидите средний RPS, но не увидите, что p99 уже улетел за секунды, пока среднее ещё выглядит прилично. Ступенчатый рост нагрузки (например, k6 со сценарием ramping-vus) полезнее одного прогона на фиксированной конкурентности: он показывает не только точку отказа, но и форму деградации — плавную или обвальную.
Важная оговорка: цифры, которые вы получите таким тестированием, специфичны для вашего профиля запросов, размера ответа и железа. Не переносите их между проектами и не воспринимайте как универсальный бенчмарк — это ориентир для конкретной системы в конкретный момент.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверМониторинг каждого звена по отдельности: какие метрики смотреть
Нагрузочный тест без метрик по слоям даёт только внешнюю цифру RPS, но не причину. Под каждым звеном нужен свой набор наблюдаемых величин, снимаемых во время того же теста:
| Звено | Что смотреть | Инструмент |
|---|---|---|
| Реверс-прокси | активные соединения, очередь backlog, доля 502/504, upstream_response_time | nginx stub_status, access-лог с $upstream_response_time |
| ОС на прокси/приложении | CPU per-core, контекстные переключения, сетевые сокеты в TIME_WAIT | vmstat, mpstat, ss -s |
| Приложение | размер очереди воркеров, время в очереди до обработки, event loop lag (для однопоточных рантаймов) | встроенные метрики фреймворка, Prometheus-экспортер |
| БД | активные подключения vs max_connections, время ожидания блокировок, cache hit ratio | pg_stat_activity, pg_stat_statements |
| Диск под БД | %util, await, IOPS в моменте пика | iostat -x 1 |
Смотреть эти метрики нужно синхронно с нагрузочным тестом, а не постфактум по агрегированным дашбордам за час — узкое звено может проявляться всплесками по несколько секунд, которые пятиминутные средние в Grafana просто сгладят. Если у вас уже есть Prometheus и Grafana, разумно на время теста снизить scrape_interval для целевых экспортеров до 1–5 секунд — это даёт разрешение, достаточное чтобы увидеть момент, когда очередь на одном из слоёв начинает расти быстрее, чем обрабатывается.
Отдельно стоит следить за тем, что метрика «пиковая» не значит «предельная». Например, если CPU приложения доходит до 70% на пике теста, а RPS уже деградирует — упор не в CPU, и наращивание ядер не поможет. Ищите метрику, которая первой упирается в потолок и держится там, пока латентность растёт, а не ту, что просто выше остальных в моменте.
Реверс-прокси: где у него предел
У прокси предел редко выражается в «максимум N запросов в секунду» — обычно это комбинация из нескольких факторов, каждый из которых можно упереть отдельно:
worker_connectionsи число воркеров. В nginx это верхняя граница одновременных соединений на воркер; при превышении новые соединения встают в очередь на уровне сокета или отбрасываются, в зависимости от настройкиbacklog.- Keepalive-соединения к бэкенду. Без пула постоянных соединений между прокси и приложением каждый запрос — это новое TCP-соединение (а для HTTPS до бэкенда — ещё и новый TLS-хендшейк), что заметно съедает CPU и латентность на высоком RPS. Подробнее разница между работой с keepalive и без него разобрана в статье про keepalive между прокси и приложением.
- Буферизация тела запроса/ответа. При больших телах прокси может писать буфер на диск (
proxy_max_temp_file_size), что превращает чисто сетевую операцию в дисковую под нагрузкой. - Таймауты апстрима. Если приложение отвечает медленнее, чем
proxy_read_timeout, прокси начинает отдавать 504 значительно раньше, чем реально исчерпан ресурс приложения — это выглядит как предел прокси, хотя причина в следующем звене.
Общий путь запроса через прокси — приём соединения, TLS, выбор бэкенда, проксирование заголовков — подробно разобран в статье как работает reverse proxy; если непонятно, на каком именно шаге запрос теряет время, это хорошая отправная точка. А практический замер, сколько соединений реально держит nginx до отказа на конкретном железе, — в статье сколько соединений держит nginx.
Практический момент: сам по себе прокси на современном сервере редко становится узким звеном раньше приложения или БД при типичной REST/JSON-нагрузке. Он чаще упирается первым в специфичных сценариях — очень большое число мелких keepalive-соединений от множества клиентов, TLS-терминация на слабом CPU, или проксирование крупных файлов/стримов.
Приложение и БД: где чаще всего рвётся цепочка
На практике узкое звено чаще находится не в прокси, а в паре «приложение + БД», и здесь важно различать несколько разных механизмов деградации:
Пул подключений к БД меньше пула воркеров приложения. Если у приложения 50 воркеров, а пул подключений к базе — 20, то при 50 одновременных запросах 30 из них будут стоять в очереди на подключение, даже если сама БД не нагружена. Это выглядит как «медленная база», хотя проблема в конфигурации пула на стороне приложения.
max_connections БД становится потолком раньше её реальной вычислительной мощности. Каждое подключение — это память и накладные расходы на стороне сервера БД; при большом числе одновременных клиентов разумнее ставить пулер (PgBouncer для PostgreSQL) между приложением и базой, чем поднимать max_connections бесконтрольно. Как именно растёт деградация при приближении к пределу подключений PostgreSQL, разобрано в статье сколько соединений держит PostgreSQL до деградации.
Медленные запросы под конкурентной нагрузкой ведут себя не так, как под одиночным вызовом. Запрос, который выполняется 20 мс в изоляции, может занять 200 мс под конкурентной нагрузкой из-за блокировок строк, конкуренции за буферный кэш или ожидания на WAL. Изолированный тест приложения-заглушки этого не покажет — нужен именно тест с реальными запросами к БД под нагрузкой, близкой к продакшен-профилю.
Синхронный код в однопоточном приложении. Для рантаймов с event loop (Node.js, asyncio-приложения на Python) один медленный синхронный вызов — например, блокирующий драйвер БД или CPU-тяжёлая сериализация — блокирует обработку остальных запросов в этом воркере. Здесь метрика event loop lag информативнее, чем средняя латентность ответа.
Практический вывод: тестируя приложение и БД в связке, полезно менять не только общую конкурентность нагрузки, но и соотношение «воркеры приложения / подключения к БД / реальный CPU базы» по отдельности — узкое звено часто находится не в абсолютной мощности компонента, а в неверном соотношении лимитов между соседними слоями.
Типичные ошибки: оптимизация не того звена
Самая частая причина, по которой узкое звено не находят месяцами, — оптимизация того компонента, который проще всего оптимизировать, а не того, который реально ограничивает пропускную способность:
- Масштабируют прокси, когда упор в приложении. Добавление воркеров nginx или переход на более быстрый прокси не даст эффекта, если бэкенд и так не успевает отвечать — очередь просто переместится с уровня TCP-бэклога на уровень таймаутов апстрима.
- Увеличивают ресурсы сервера БД, не глядя на пул подключений. Более мощный CPU или память под БД не помогут, если приложение всё равно ограничено количеством одновременных подключений в пуле — узкое звено остаётся конфигурационным, а не аппаратным.
- Судят по средней латентности, а не по хвостам. Среднее в 50 мс может скрывать 5% запросов с латентностью в 3 секунды — именно эти запросы формируют реальный пользовательский опыт под нагрузкой и часто указывают на конкретный проблемный запрос к БД или блокировку.
- Тестируют не тем профилем запросов. Нагрузочный тест с одинаковыми лёгкими GET-запросами не покажет проблему, которая возникает только при смеси чтения и записи или при запросах с большим телом — реальный продакшен-трафик почти никогда не однороден.
- Не учитывают холодный старт и прогрев кэшей. Первые секунды теста после рестарта приложения или БД дают заниженные цифры RPS не из-за реального предела, а из-за прогрева коннекшн-пулов, JIT и кэшей — если не отбросить эту фазу, легко принять её за узкое звено.
- Оптимизируют звено, которое не участвует в целевом сценарии. Если 90% трафика — это чтение по кэшируемым ключам, а тестируют и чинят путь записи, реальная пропускная способность продакшена не меняется.
Практическая проверка перед тем, как что-то оптимизировать: смогли ли вы через изолированное тестирование (раздел выше) показать, что именно этот слой первым начинает деградировать под нагрузкой. Если ответ «предполагаю, но не проверял» — оптимизация с высокой вероятностью уйдёт не туда.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Можно ли определить узкое звено только по продакшен-метрикам, без нагрузочного тестирования?
Частично — если у вас есть детальные метрики по каждому слою за реальные пиковые периоды, можно увидеть, какой слой первым показывает рост латентности. Но без контролируемого теста сложно отделить причину от следствия: рост латентности БД на проде может быть и причиной, и следствием того, что приложение начало ретраить запросы из-за таймаутов на другом слое.
Стоит ли тестировать на проде или обязательно нужен отдельный стенд?
Предпочтительнее отдельный стенд с максимально близким железом и объёмом данных — тестирование на проде рискует реальными пользователями и не даёт чистой изоляции по этапам. Если стенда нет, хотя бы проводите тесты в окно низкой нагрузки и с явным пониманием рисков.
Что делать, если узкое звено — это сеть между приложением и БД, а не сами компоненты?
Такое бывает при разнесении приложения и БД на разные сервера или в разные дата-центры. Проверяется через латентность и потери пакетов между хостами (ping, mtr) под нагрузкой отдельно от нагрузки на сами сервисы — иногда решение не в апгрейде компонентов, а в размещении приложения и БД в одном сегменте сети или даже на одном сервере.
Как часто нужно повторять этот анализ?
При заметном изменении профиля трафика, схемы БД, версии зависимостей или после значимого роста аудитории — узкое звено не статично и может сместиться с прокси на БД и обратно после любого из этих изменений.
Нужно ли сразу закладывать запас по всем слоям, чтобы не искать узкое звено каждый раз?
Разумный запас — 30–50% сверх текущего пикового RPS на каждом слое — снижает частоту авралов, но не отменяет необходимость периодической проверки: рост нагрузки редко распределяется равномерно между слоями, и запас на одном звене может исчерпаться быстрее, чем на других.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →