MAATRIX / Блог / Порог отказа под нагрузкой: как найти его тестом и не положить продакшен

Порог отказа под нагрузкой: как найти его тестом и не положить продакшен

MAATRIX

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

Почему резкий тест «в лоб» на проде — плохая идея

Соблазн понятен: стенд поднимать долго, а сервер уже стоит настроенный и с реальными данными. Кажется, что можно быстро прогнать ab или wrk прямо на боевой домен ночью, пока трафика немного, и получить честную цифру. На практике это почти всегда даёт либо бесполезный результат, либо инцидент.

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

  • Вы не изолированы от реального трафика. Синтетическая нагрузка накладывается на живую, и непонятно, кто исчерпал пул соединений к базе — тест или обычный пользовательский трафик, случайно совпавший по времени.
  • Общие ресурсы делят тест и прод. Если приложение и соседние сервисы сидят на одном сервере или используют одну managed-базу, тест может уронить не то, что вы тестируете, а что-то совсем другое, зависящее от тех же ресурсов.
  • Резкий пик без разгона не показывает «порог» — он показывает случайную точку, где что-то не выдержало рывка. Деградация под растущей нагрузкой и шок от мгновенного скачка соединений — разные явления: во втором случае вы измерите лимит того, что не пережило резкого старта (например, backlog очереди приёма), а не тот предел, что упирается при органическом росте трафика.

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

Собираем staging, который не соврёт

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

  • Версии и конфигурация приложения — тот же код, та же версия рантайма (PHP/Node/Python), те же настройки пулов соединений, кэшей, таймаутов. Разница в одной директиве worker_connections или размере пула к БД меняет точку отказа.
  • Объём и характер данных. Пустая тестовая база — не тест, а фикция: индексы и план запроса ведут себя иначе на 100 строках и на 10 миллионах. Нужен слепок, близкий по объёму к боевому, с обезличенными персональными данными.
  • Ресурсы сервера. В идеале — идентичная конфигурация CPU/RAM/диска. Если стенд слабее прода, зафиксируйте пропорцию и масштабируйте результат с осторожностью: деградация редко линейна относительно числа ядер.
  • Сетевая топология. Если в проде между балансировщиком, приложением и базой один прыжок в одном дата-центре, а стенд размазан по разным локациям с ощутимым RTT — задержки исказят результат ещё до реального лимита CPU или соединений.
  • Изоляция от боевых зависимостей. Стенд не должен использовать боевую базу, очередь сообщений или SMTP-релей «для простоты» — иначе вы снова тестируете прод, просто через прокладку.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Step load: почему нагрузку наращивают ступенями, а не пиком

Смысл нагрузочного теста «до порога отказа» — не проверить, переживёт ли сервер один резкий скачок, а найти ту точку по нагрузке, после которой система устойчиво деградирует. Для этого нагрузку наращивают ступенями (step load), а не подают сразу максимум:

  1. Baseline. Стартуете с заведомо комфортного уровня — например, 10-20% от ожидаемого пика. Держите его 3-5 минут стабильно, снимаете метрики как точку отсчёта: латентность, ошибки, ресурсы в спокойном режиме.
  2. Ступенчатый разгон. Увеличиваете параллелизм или RPS фиксированным шагом (удвоением или на 25-50% от предыдущего значения), держите каждую ступень стабильно несколько минут, прежде чем идти дальше.
  3. Стабилизация обязательна в обе стороны. Слишком быстрый разгон ловит переходные эффекты — скопление в очереди, прогрев кэшей, паузы сборщика мусора — и путает их с устойчивым пределом. Слишком медленный, растянутый на часы тест наоборот рискует упустить накопление состояния — утечку памяти, исчерпание файловых дескрипторов, разрастание пула соединений, — которое проявляется только при достаточно долгой работе под нагрузкой.
  4. Заранее определите критерий остановки, а не полагайтесь на то, что кто-то заметит проблему по графику вручную: порог по доле ошибок (error rate выше заданного значения устойчиво в течение минуты) или по латентности (p99 выше заданного множителя от baseline). Как только критерий сработал — тест останавливается автоматически, ступень зафиксирована как точка деградации.

Отдельно стоит различать типы нагрузочных тестов: smoke-тест (минимальная нагрузка, проверка что всё работает), load-тест (ожидаемый рабочий пик), stress-тест (поиск порога отказа — то, о чём эта статья), spike-тест (резкий скачок и восстановление после него) и soak-тест (умеренная нагрузка на длительное время, ловит утечки). Step load — инструмент именно для stress-теста: вы целенаправленно идёте выше ожидаемого пика, пока не найдёте предел.

Инструменты: k6, wrk, Apache Bench, Locust

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

ИнструментЯзык сценариевВстроенный step loadРаспределённый режимКогда уместен
Apache Bench (ab)нет (флаги CLI)нетнетБыстрая проверка одного URL, smoke-тест, грубая прикидка «жив ли эндпоинт»
wrkLua (опционально)нет напрямую, но легко обернуть в скриптнет из коробкиМаксимальный сырой RPS от одной машины, точные перцентили латентности (--latency)
k6JavaScriptда, через stages в конфигеда (k6 Cloud или самостоятельная оркестрация нескольких инстансов)Сценарии, близкие к реальному поведению пользователя, автоматические пороги остановки
LocustPythonда, через кастомную логику или встроенные шаги в новых версияхда, штатно (master/worker)Сложные многошаговые сценарии пользователя, наглядный веб-интерфейс в реальном времени

Apache Bench — самый простой вариант, хорош для быстрой sanity-проверки, но у него однопоточная модель и нет сценариев, только один URL и число запросов/параллелизм. Для stress-теста с разгоном по ступеням он неудобен — шаги пришлось бы гонять отдельными вызовами вручную.

wrk — лёгкий, многопоточный, даёт честные перцентили латентности из коробки (флаг --latency). Встроенного step load нет, но его легко получить, вызывая wrk последовательно с разными значениями -c:

#!/bin/bash
for c in 100 200 400 800 1600 3200; do
  echo "=== concurrency $c ==="
  wrk -t8 -c$c -d180s --latency https://staging.example.internal/ \
    | tee -a step-load-results.log
  sleep 30
done

k6 удобен именно тем, что step load встроен в конфигурацию как первоклассная сущность — стадии описываются декларативно, и можно задать пороги, при срабатывании которых тест сам прерывается:

import http from 'k6/http';
import { check } from 'k6';

export const options = {
  stages: [
    { duration: '3m', target: 50 },   // baseline
    { duration: '3m', target: 100 },
    { duration: '3m', target: 200 },
    { duration: '3m', target: 400 },
    { duration: '3m', target: 800 },  // дальше уже ожидаем деградацию
  ],
  thresholds: {
    http_req_failed: ['rate<0.01'],          // ошибок больше 1% — стоп
    http_req_duration: ['p(99)<1500'],       // p99 выше 1.5с — стоп
  },
};

export default function () {
  const res = http.get('https://staging.example.internal/');
  check(res, { 'status is 200': (r) => r.status === 200 });
}

Если порог из thresholds нарушен, k6 помечает прогон как проваленный и (с флагом --abort-on-fail) может прервать выполнение раньше времени — это и есть автоматический предохранитель из раздела про step load.

Locust описывает нагрузку в терминах поведения пользователя — класс с задачами и весами, а не голыми HTTP-запросами:

from locust import HttpUser, task, between

class SiteUser(HttpUser):
    wait_time = between(1, 3)

    @task(3)
    def browse_catalog(self):
        self.client.get("/catalog/")

    @task(1)
    def checkout(self):
        self.client.post("/checkout/", json={"item_id": 42})

Locust хорош, когда важно эмулировать реалистичный микс запросов, а не долбить один URL. Его распределённый режим (master и несколько worker-процессов на разных машинах) полезен, если сам генератор становится узким местом раньше тестируемого сервера.

Какие метрики смотреть кроме RPS

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

  • Перцентили латентности, а не среднее. Среднее маскирует хвост распределения — именно его чувствуют реальные пользователи. Если p50 держится на комфортных значениях, а p99 растёт кратно — часть запросов уже упирается в ресурс, даже если «в среднем всё хорошо». Смотрите минимум p50/p95/p99, для критичных сервисов ещё и p99.9.
  • Error rate в динамике, а не суммарно. Важен не итог за весь тест, а момент, с которого доля ошибок начала расти — это и есть маркер конкретной ступени, где начался отказ.
  • Насыщение ресурсов параллельно с тестом. RPS и латентность — симптомы, а причина всегда в конкретном ресурсе: CPU, память и своп, соединения в пуле к БД, очередь на диске, пропускная способность сети. Та же методика применима к отдельным слоям — например, для PostgreSQL она разобрана в статье сколько соединений к PostgreSQL до деградации, а для nginx — в статье сколько соединений держит nginx: замер до отказа.
  • Реальная одновременность (concurrency) против отданной нагрузки. По закону Литтла среднее число запросов в системе одновременно равно произведению пропускной способности на время ответа. Если латентность растёт при неизменном RPS — параллельная нагрузка на систему растёт незаметно для RPS, просто запросы дольше «зависают» внутри.

Не полагайтесь на один график. Исчерпание пула соединений обычно даёт резкий скачок ошибок при ровной латентности до этого момента, а упор в CPU — плавный рост латентности без явного обрыва. Совмещайте вывод генератора нагрузки с системными метриками (vmstat, mpstat, ss -s) в одном временном окне, чтобы видеть причину и следствие рядом.

Как зафиксировать точку деградации и не спутать её с обвалом

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

  • Мягкая деградация — латентность растёт, часть запросов отваливается по таймауту, но сервер формально отвечает и не требует перезапуска. Это и есть тот порог, который вы ищете в первую очередь: предел комфортной работы.
  • Жёсткий отказ — connection refused, зависание процесса, OOM-killer, полная недоступность. Доводить тест до этой стадии каждый раз не обязательно, только если вы сознательно проверяете сценарий восстановления после полного падения — а это отдельная, более рискованная задача с заранее продуманным планом отката.

Практический чек-лист, чтобы остановить тест вовремя, а не по факту уже случившегося инцидента на стенде:

  1. Заранее задайте числовые критерии остановки (error rate, p99) в самом инструменте — как в примере с thresholds в k6.
  2. Держите отдельный watchdog-скрипт или мониторинг health-эндпоинта, независимый от генератора нагрузки — если сам генератор перегружен, вы можете пропустить момент отказа.
  3. Настройте алерт человеку при срабатывании порога — даже с автостопом инструмента полезно, чтобы кто-то посмотрел на систему по свежим следам.
  4. Зафиксируйте не только факт деградации, но и то, какой ресурс насытился первым — это ответ на вопрос «почему» порог именно такой.
  5. После остановки дайте системе время на восстановление и проверьте, что она вернулась в норму сама — если нет, это отдельный важный вывод теста.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

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

Сколько раз повторять step load, чтобы доверять результату?

Минимум дважды подряд с одинаковыми параметрами — если результат совпадает в пределах разумной погрешности, ему можно доверять. Разовый прогон легко спутать со случайным совпадением фоновых факторов на стенде.

Можно ли тестировать порог сразу на нескольких эндпоинтах?

Можно и даже правильнее, если реальный трафик именно такой — микс запросов, как в примере с Locust, честнее показывает поведение системы, чем долбёжка одного самого лёгкого URL.

Что делать, если сам генератор нагрузки упирается в свой лимит раньше сервера?

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

Нужно ли тестировать с включённым TLS, если в проде HTTPS?

Да, тем же протоколом, что в проде — TLS-хендшейк заметно нагружает CPU по сравнению с обычным HTTP, и порог по HTTPS почти всегда ниже.

Как часто повторять весь цикл тестирования порога?

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

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →