MAATRIX / Блог / Разбор после пика: узким местом оказалась не та система

Разбор после пика: узким местом оказалась не та система

MAATRIX

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

Почему ожидание расходится с реальностью

Перед пиком команда обычно масштабирует то, что видно и понятно: CPU веб-сервера, число процессов приложения, реплики за балансировщиком. Это не ошибка — просто это самый заметный и самый управляемый параметр, тот, который можно поднять кликом в панели или строчкой в docker-compose. Проблема в том, что реальная пропускная способность системы определяется самым узким звеном во всей цепочке запроса, а не самым заметным.

Цепочка обработки запроса выглядит примерно так: балансировщик → веб-сервер/приложение → пул соединений к БД → сама БД → при необходимости внешний сервис (платёжный шлюз, email-провайдер, SMS-агрегатор). Каждое звено масштабируется по-своему и с разной скоростью реакции:

  • Веб-сервер и приложение — масштабируются почти линейно и быстро: добавили CPU или реплику — получили пропорционально больше обработанных запросов.
  • Пул соединений к БД — жёстко ограничен числом, которое требует отдельного администрирования (max_connections в PostgreSQL, размер пула в pgbouncer), и увеличение этого числа не бесплатно — каждое лишнее соединение стоит памяти и снижает эффективность кеша БД.
  • Сама БД по CPU/диску — масштабируется вертикально дороже и медленнее, чем веб-слой, а горизонтально нетривиально для большинства нагрузок на запись.
  • Внешние сервисы — вообще не под вашим контролем: у платёжного шлюза или SMS-агрегатора свои лимиты запросов в секунду и своя очередь на их стороне, о которой вы узнаёте только по ответам с ошибкой.

Готовясь к пику, легко отмасштабировать первое звено — оно самое дешёвое в изменении — и не заметить, что реальный потолок стоит дальше по цепочке. Причём чем лучше отмасштабировано первое звено, тем концентрированнее становится удар по следующему: если раньше веб-сервер сам сглаживал поток запросов к БД тем, что не успевал их отдавать быстрее, то после апгрейда веб-слоя в БД прилетает уже полный, несглаженный трафик пика. Узкое место не исчезает — оно переезжает на шаг дальше. Почему масштабирование без предварительного поиска узкого места часто двигает проблему, а не решает её, разобрано в статье про антипаттерн масштабирования без поиска узкого места.

Кейс: готовили веб-сервер, упёрлись в лимит соединений к БД

Типичный сценарий: перед распродажей команда поднимает тариф VPS под приложением, увеличивает число процессов gunicorn или воркеров Node.js, добавляет пару реплик за балансировщиком. Нагрузочный тест на staging показывает уверенный рост RPS с ростом числа воркеров — вывод напрашивается сам: больше процессов, больше пропускной способности.

В день пика картина другая: CPU веб-серверов держится в комфортных 40-60%, а время ответа всё равно растёт, пользователи жалуются на зависшую корзину. В логах приложения — характерная ошибка:

FATAL: sorry, too many clients already

или, если стоит pgbouncer перед PostgreSQL:

ERROR: server login has been failing, cached rejection

Причина обычно не связана с CPU: max_connections в PostgreSQL — конечное число (точное значение стоит смотреть в своей конфигурации, а не полагаться на общую цифру), и каждый лишний воркер приложения открывает к БД собственные соединения. Если число процессов веб-приложения выросло вдвое-втрое, а пул соединений остался прежним, суммарное число подключений к БД легко превышает лимит — часть запросов получает отказ вместо ответа, независимо от того, насколько свободен CPU.

Проверить состояние пула во время инцидента:

SELECT count(*) FROM pg_stat_activity;
SELECT setting FROM pg_settings WHERE name = 'max_connections';

Если первое число близко ко второму — это узкое место, которое не видно ни на графике CPU веб-сервера, ни в мониторинге, который следит только за "верхним" слоем стека. Подробнее о том, как устроен этот лимит и почему число воркеров приложения напрямую конкурирует с ним за один потолок — в статье сколько соединений PostgreSQL выдерживает до деградации. Правильным решением здесь почти никогда не бывает механическое поднятие max_connections — это снижает эффективность БД при высоких значениях; вместо этого между приложением и БД ставится pgbouncer в режиме transaction pooling, который держит небольшое число реальных соединений к БД и мультиплексирует через них гораздо большее число клиентских запросов.

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

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

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

Кейс: внешний платёжный шлюз как скрытый потолок

Второй сценарий коварнее первого, потому что происходит целиком за пределами вашей инфраструктуры. Весь стек — веб-сервер, приложение, БД — держит нагрузку пика уверенно, метрики зелёные, а конверсия в оплату всё равно проседает именно на шаге оформления заказа.

Причина — в лимитах внешнего платёжного шлюза или другого стороннего API (email-рассылка, SMS-подтверждение, антифрод), которые задаются не вами и обычно не документированы явно в объёме, достаточном для планирования пиковой нагрузки заранее. У шлюза может быть внутренний лимит запросов в секунду на ваш аккаунт: при превышении он либо возвращает код ограничения (429 Too Many Requests), либо — что хуже для диагностики — просто отвечает медленнее, накапливая ваши запросы в очередь на своей стороне.

Отличить эту ситуацию от проблемы на своей стороне помогает конкретный признак: время ответа именно на запрос к внешнему API растёт непропорционально нагрузке на ваш собственный стек, а остальные метрики остаются в норме. Если время ответа стороннего вызова логируется отдельной меткой — это видно сразу; если нет, такую метку стоит завести до следующего пика:

2026-08-28T14:32:07Z order_id=48213 total_ms=4820 db_ms=140 app_ms=95 payment_gateway_ms=4580

Здесь total_ms почти целиком состоит из payment_gateway_ms — узкое место очевидно, но только потому, что метрика вообще была разбита на составляющие. Без такого разделения запрос выглядел бы просто "медленным", и расследование пришлось бы вести заново, гадая между БД, приложением и внешним вызовом.

Практические меры, если узкое место — внешний сервис:

  • Таймаут на стороне приложения должен быть заметно короче терпения пользователя и не блокировать воркер на всё время ожидания — иначе медленный внешний сервис "съедает" пропускную способность вашего стека.
  • Уточните у провайдера платежей реальный лимит запросов в секунду заранее — часто его можно поднять по заявке, предупредив о планируемой нагрузке.
  • Ретраи с экспоненциальной задержкой, а не мгновенный повторный запрос — иначе он лишь усиливает нагрузку на уже перегруженный шлюз.
  • Очередь на своей стороне для некритичных по времени вызовов (чек на почту, SMS) — убирает лишнюю зависимость от времени ответа из пути пользователя.

Методика: план нагрузочного теста против факта

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

Звено цепочкиОжидание по плануФакт на пикеВывод
CPU веб-серверадо 80%55%запас избыточен
Соединения к БДне оценивали отдельноупёрлись в лимитпропущенное звено в плане
Ответ платёжного шлюзане оцениваливырос в разывнешняя зависимость не учтена
Диск под логидо 70%30%запас избыточен

Строки вида "не оценивали" — самый ценный результат разбора: они прямо показывают, какие звенья выпали из плана нагрузочного тестирования, а не просто "не хватило мощности" в общем виде. Если тест перед пиком гонял синтетический трафик только на HTTP-слой приложения, не создавая реальной нагрузки на БД и эмулируя внешние вызовы моком с нулевой задержкой — сам тест заранее гарантировал, что эти звенья останутся непроверенными.

Второй полезный срез — сопоставление временных меток. Наложите на график время начала деградации (первые 5xx, первые таймауты, первые жалобы) и метрики каждого звена за тот же интервал. Звено, чья метрика "сломалась" раньше остальных и раньше момента появления ошибок у пользователя — и есть настоящее узкое место, а не то, что субъективно казалось наиболее нагруженным по общему впечатлению от дежурства. В Grafana это делается аннотацией на график по времени первого инцидентного алерта и сравнением, где изменение поведения началось раньше всех.

Где искать данные для расследования постфактум

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

Источники, которые стоит проверить в первую очередь:

  1. Метрики системы за период пика — CPU, память, диск, сеть на каждом узле отдельно, а не только агрегированно. Короткая ретенция мониторинга, из-за которой данные пика уже удалены, — тоже важный вывод: горизонт хранения должен быть больше, чем "до следующего планового разбора".
  2. Логи ошибок приложения и БД, отфильтрованные по времени первых жалоб — тексты вида too many clients, connection timeout, 502, 504 называют компонент точнее любой косвенной метрики.
  3. pg_stat_activity и лог медленных запросов PostgreSQL (log_min_duration_statement), если снимок делался во время пика — показывают, какие запросы держали соединения дольше обычного.
  4. Ответы и коды ошибок внешних API, если логируются отдельно, — прямой способ подтвердить или исключить версию про внешний шлюз.
  5. Тикеты поддержки и жалобы с точным временем — часто точнее указывают, когда реально стало плохо, чем усреднённые графики с крупным шагом агрегации.
  6. Журнал пика, если он вёлся (см. статью про откат ресурсов) — там записано, что именно меняли перед пиком, а значит какие звенья затронуты изменениями и могли повести себя иначе, чем обычно.

Если часть данных отсутствует — прямо зафиксируйте это в итогах как отдельный пункт "не было данных, чтобы подтвердить или опровергнуть", а не подгоняйте вывод под неполную картину. Честное "точно не знаем, но подозреваем X" полезнее ложной уверенности — оно хотя бы подсказывает, что замерить в следующий раз.

От разбора к действию: обновляем план и тесты

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

Правки в нагрузочный тест. Если тест не создавал реальной нагрузки на БД или эмулировал внешние вызовы моком с нулевой задержкой — добавьте в сценарий (например, в k6 или Locust) реальные обращения к тестовой БД с тем же профилем запросов, что и в проде, и, если это технически допустимо, обращения к тестовому контуру внешнего сервиса вместо мока — хотя бы для оценки поведения при таймаутах и ошибках.

Правки в план масштабирования. Внесите в чек-лист подготовки к следующему пику строку по каждому пропущенному звену — не только тариф веб-сервера, но и, например, "проверить размер пула pgbouncer относительно числа воркеров" или "уточнить у платёжного провайдера лимит запросов в секунду". Методика перевода бизнес-метрик (число заказов) в технические (запросы в секунду, соединения к БД) становится точнее ровно настолько, насколько разбор поправил список звеньев, которые нужно закладывать в расчёт.

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

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

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

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

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

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

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

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

В чём разница между этой статьёй и статьёй про возврат ресурсов после пика?

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

Нужно ли проводить такой разбор после каждого пика, даже небольшого?

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

Как понять, что узкое место — именно пул соединений к БД, а не сама база по производительности запросов?

Ошибки вида "too many clients" или "server login has been failing" — прямой признак лимита на число соединений. Если же соединения устанавливаются нормально, но конкретные запросы выполняются заметно дольше обычного — это, вероятнее, проблема производительности запросов или блокировок, и расследование начинается с лога медленных запросов, а не с размера пула.

Можно ли заранее протестировать поведение при отказе или лимите внешнего платёжного шлюза?

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

Что делать, если узкое место — во внешнем сервисе, на который вы вообще не можете повлиять?

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

Стоит ли держать записи разбора в том же журнале пика, что и чек-лист отката?

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

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

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

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