Тестовый нагрузочный скрипт три недели бил по боевому серверу
Три недели прод периодически «подвисал» без единой строчки в логах об ошибках — просто иногда отвечал в три-четыре раза медленнее обычного, а потом снова работал нормально. Ни одного алерта, ни одного упавшего деплоя, ни одной жалобы от команды разработки на код. Причина оказалась до обидного простой: коллега тестировал нагрузочный скрипт на новую фичу и по невнимательности указал в конфиге URL боевого сервера вместо тестового стенда. Разбираем, как мы неделю искали проблему не там, что в итоге подсказали логи, и как теперь у нас физически невозможно перепутать прод со стендом.
Содержание
- Симптом: сервис тормозил рывками, но ничего не падало
- Первый круг проверок: код, деплои, база данных
- Что подсказали логи и метрики, когда мы посмотрели внимательнее
- Гипотезы, которые пришлось отбросить
- Настоящая причина: забытый скрипт с продовым URL в конфиге
- Как мы это подтвердили
- Что изменили после разбора
Симптом: сервис тормозил рывками, но ничего не падало
Первые сигналы пришли не от мониторинга, а от службы поддержки: клиенты жаловались, что личный кабинет иногда «подтормаживает» — страницы грузятся не полсекунды, а три-пять. Жалобы были нерегулярными: кто-то писал утром, кто-то вечером, паттерна по времени суток на глаз не читалось. При этом ни один алерт не сработал: наш порог по времени ответа API стоял на 2000 мс для p95, а всплески укладывались в 800-1200 мс — заметно для пользователя, но недостаточно, чтобы разбудить дежурного.
Мы подняли графики Grafana за последние две недели и увидели неровные «зубцы» на p95 времени ответа — не постоянную деградацию, а короткие эпизоды по 5-15 минут, разбросанные по дням без явной системы. CPU веб-серверов в эти моменты действительно подрастал, но не критично — с обычных 25-30% до 55-70%. Диск и память вели себя ровно. Ошибок 5xx не прибавлялось, просто запросы обрабатывались медленнее обычного.
Первая мысль была логичной для такой картины: где-то в коде появился медленный путь, который срабатывает не при каждом запросе, а изредка — например, из-за неудачного индекса или тяжёлого отчёта, который кто-то из пользователей запускает нерегулярно.
Первый круг проверок: код, деплои, база данных
Начали с самого дешёвого способа проверки — посмотрели на историю деплоев. Совпадений по времени не нашли: эпизоды тормозов случались и через несколько дней после последнего релиза, и в дни без единого деплоя. Версию «сломали недавним изменением» отбросили в первый же час.
Дальше проверили медленные запросы к PostgreSQL:
SELECT query, calls, mean_exec_time, max_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;
Ничего подозрительного: тяжёлых запросов, которые внезапно стали бы тяжелее, не появилось. pg_stat_activity в моменты жалоб (мы сопоставили таймстемпы из тикетов поддержки с логами) тоже не показывал скопления долгих транзакций или блокировок.
Проверили фоновые задачи — вдруг где-то запускается тяжёлая джоба по расписанию:
crontab -l -u www-data
systemctl list-timers --all | grep -i app
Нашли только штатные — ротацию логов, ночной бэкап (шёл в 03:00, а тормоза случались и днём), рассылку отчётов раз в неделю по понедельникам (тормоза были не только по понедельникам). Ни один существующий по расписанию процесс не совпадал по времени с жалобами.
К концу первой недели мы перебрали самые очевидные версии — код, деплой, база, крон — и ни одна не подтвердилась. Это неприятный момент в любом разборе: самые дешёвые проверки исчерпаны, а причина не найдена.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверЧто подсказали логи и метрики, когда мы посмотрели внимательнее
На второй неделе решили не гадать по агрегированным графикам, а поднять сырые access-логи nginx за конкретный получасовой интервал одного из подтверждённых эпизодов:
awk -v start="[08/Sep/2026:14:10:00" -v end="[08/Sep/2026:14:25:00" \
'$4 >= start && $4 <= end' /var/log/nginx/access.log | wc -l
Число запросов в этом окне оказалось заметно выше обычного — примерно в полтора-два раза больше, чем в соседних получасовых интервалах без жалоб. Это была первая конкретная зацепка: в моменты тормозов на сервер приходило больше запросов, чем обычно. Дальше посмотрели, откуда именно взялся прирост:
awk -v start="[08/Sep/2026:14:10:00" -v end="[08/Sep/2026:14:25:00" \
'$4 >= start && $4 <= end {print $1}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head -10
Один IP-адрес резко выделялся по количеству запросов — на порядок больше, чем у любого «живого» пользователя за то же время. Посмотрели его User-Agent:
grep "203.0.113.44" /var/log/nginx/access.log | awk -F'"' '{print $6}' | sort -u
User-Agent оказался не браузерным — характерная строка нагрузочного инструмента, а не Chrome или Safari. Стало ясно: тормоза совпадают не с ростом пользовательского трафика, а с эпизодами постороннего автоматизированного трафика с одного адреса.
Гипотезы, которые пришлось отбросить
Прежде чем связать находку с конкретной причиной, проверили несколько версий, куда мог вести этот трафик:
- Целевая DDoS-атака. Отбросили быстро: IP один и тот же на протяжении всех эпизодов за три недели, паттерн запросов повторяющийся и «вежливый» — без попыток обойти защиту, без смены User-Agent, без распределения по множеству адресов. На атаку это не похоже.
- Парсер или бот конкурента. Проверили ASN адреса — он относился не к хостинг-провайдеру и не к облаку, а к диапазону нашей же компании (офисная сеть). Версия с внешним ботом отпала.
- Забытый интеграционный тест в CI. Посмотрели логи CI/CD — в моменты эпизодов пайплайны либо не запускались вовсе, либо это были сборки, не имеющие отношения к нагрузочному тестированию. Не совпало.
- Утечка учётных данных и легитимный, но скомпрометированный аккаунт, который кто-то дёргает скриптом. Проверили — запросы шли не через авторизованные ручки API с токеном пользователя, а напрямую на публичные эндпоинты без сессии. На компрометацию аккаунта не похоже, это выглядело как чей-то собственный скрипт.
Как только выяснилось, что источник — офисная сеть, а не внешний мир, стало понятно, что искать надо внутри компании, а не снаружи.
Настоящая причина: забытый скрипт с продовым URL в конфиге
Дальше — просто вопрос коллегам в чате: «кто-нибудь гоняет нагрузочные тесты с офисной сети?». Ответ пришёлся быстро: один из разработчиков тестировал производительность нового эндпоинта поиска и написал скрипт на k6, который раз в 15-20 минут запускал сценарий с постепенным увеличением параллельных запросов:
// loadtest.js
import http from 'k6/http';
export const options = {
stages: [
{ duration: '2m', target: 20 },
{ duration: '3m', target: 50 },
{ duration: '2m', target: 0 },
],
};
export default function () {
http.get(`${__ENV.TARGET_URL}/api/search?q=test`);
}
Скрипт запускался через systemd таймер на его рабочей машине для регулярной проверки — план был погонять его пару дней на стенде и сравнить производительность до и после оптимизации. Переменная TARGET_URL бралась из локального .env-файла, который он в какой-то момент скопировал с другого проекта, где под этим именем уже лежал адрес боевого API — при копировании файл не переименовали и не проверили содержимое, просто взяли готовый шаблон, чтобы не заводить переменные вручную. Тестовый стенд имеет похожий, но не идентичный домен, и при беглом просмотре конфига разница не бросалась в глаза.
Дальше — типичная история «настроил и забыл»: задача отработала пару раз, результаты устроили, про неё никто больше не вспоминал, а systemd-таймер продолжал запускать её на рабочей станции каждые несколько часов в рабочее время. Из-за плавающего расписания (таймер стартовал не строго по будильнику, а с рандомизацией RandomizedDelaySec, чтобы не создавать пиковую нагрузку на CI-раннере) эпизоды и не попадали в один и тот же час дня — отсюда и ощущение «случайности» тормозов.
Как мы это подтвердили
Формальное подтверждение получили без всякой магии — сопоставили метки времени. Взяли лог запуска systemd-таймера с машины коллеги:
journalctl --user -u loadtest.timer --since "3 weeks ago" | grep Started
Список времени запуска почти один в один совпал со списком эпизодов деградации из Grafana (расхождение в пределах минуты — на старт скрипта и разгон до пиковой нагрузки за первые две ступени сценария). После этого совпадения версия перестала быть гипотезой и стала диагнозом: три недели нагрузочный тест шёл не на изолированном стенде, а на боевом API, конкурируя за CPU и пул соединений к базе с реальными пользователями.
Отдельно проверили, не отражалось ли это на данных — сценарий бил только на GET /api/search, без записи, так что риска порчи данных не было, только паразитная нагрузка на процессор и лишние соединения к базе.
Что изменили после разбора
Сразу после находки отключили таймер и почистили .env-файл, но это устраняло симптом одного конкретного случая, а не саму возможность повторения. Дальше внесли несколько изменений, которые должны сделать такую ошибку физически сложнее:
- Разные учётные данные и разные домены для прода и стенда, которые нельзя перепутать по шаблону. Продовый API теперь недоступен по короткому «человеческому» имени, только по полному защищённому домену, а стенд специально называется с явным префиксом
staging-. - Сетевая изоляция. На фаерволе боевого сервера настроили allow-list по IP для служебных и тестовых обращений — рабочие станции разработчиков без явного добавления в список не могут достучаться до прода вовсе, только до стенда:
sudo ufw allow from 10.20.0.0/24 to any port 443 proto tcp comment "office -> staging only"
sudo ufw deny from 10.20.0.0/24 to any port 443 proto tcp
- Метка синтетического трафика. Все нагрузочные скрипты теперь обязаны слать заголовок
X-Synthetic-Test: <имя-задачи>; на стенде это ни на что не влияет, а на проде nginx такие запросы отклоняет с 403 на уровне конфига — то есть даже если URL снова перепутают, прод сам откажется обслуживать нагрузочный трафик:
if ($http_x_synthetic_test) {
return 403;
}
- Алерт на аномальный источник трафика. Добавили правило в Prometheus/Alertmanager, которое смотрит не только на общий RPS, но и на долю трафика с одного IP выше заданного порога за короткое окно — такой паттерн типичен именно для скрипта, а не для живых пользователей.
- Чек-лист перед запуском нагрузочных тестов. Теперь перед стартом любого нагрузочного сценария в общем чате отправляется сообщение с целевым URL и ожидаемой длительностью — простое социальное правило, но оно окупилось бы в первый же день этого инцидента.
- Отдельный VPS под стенд. Раньше тестовое окружение крутилось на одном сервере с несколькими другими некритичными сервисами, из-за чего разница между «стендом» и «продом» в головах у команды была не такой чёткой. Теперь под тестирование выделен отдельный сервер с собственным доменом и явной пометкой в панели — так меньше шансов перепутать адрес при копировании конфига.
Если у вас стенд и прод до сих пор живут на одном сервере или делят один и тот же диапазон адресов, это стоит поправить в первую очередь — разделение на разные VPS для разработки и тестирования и продовый сервер снимает половину рисков такого рода ещё до всякого allow-list. При выборе размера тестового окружения удобно ориентироваться на то, сколько ресурсов нужно VPS для тестовой среды — с запасом под нагрузочные сценарии, а не впритык.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Почему алерты не сработали, если CPU реально подрастал?
Порог по p95 времени ответа был откалиброван под явную деградацию, а не под кратковременные всплески на 5-15 минут — такие эпизоды укладывались в допустимый диапазон и не считались инцидентом. После разбора добавили отдельный алерт именно на паттерн трафика, а не только на конечное время ответа.
Как быстро отличить нагрузочный скрипт от настоящего роста пользователей по логам?
Смотрите на распределение запросов по IP и User-Agent за подозрительный интервал: живой трафик обычно распределён по многим адресам с разными агентами, а скрипт — это один-два адреса с постоянным, часто повторяющимся паттерном запросов и характерным User-Agent инструмента (k6, wrk, python-requests, ab и подобные не маскируются под браузер, если это специально не сделано).
Достаточно ли просто попросить команду быть внимательнее с конфигами?
На практике нет — рано или поздно кто-то снова скопирует .env по шаблону. Работает связка из технических барьеров (сетевая изоляция, отдельные домены, отклонение синтетического трафика на проде) и лёгких организационных привычек (сообщение в чат перед стартом теста), а не одна лишь просьба быть аккуратнее.
Нужно ли для нагрузочного тестирования такой же по мощности сервер, как прод?
Не обязательно, но стенд должен быть достаточно мощным, чтобы результаты нагрузочного теста были показательны, а не искажены нехваткой ресурсов самого стенда. Разумный ориентир — смотреть на расчёт конфигурации сервера под нагрузку и брать конфигурацию, сопоставимую с продом хотя бы по CPU, если тест как раз про производительность под нагрузкой.
Как понять, что именно грузит сервер, если под рукой нет готовых логов с меткой источника?
Начните с htop/iotop для живой картины и переходите к анализу сетевых соединений и access-логов — подробный порядок действий разобран в статье как узнать, кто нагружает сервер.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →