MAATRIX / Блог / Антипаттерн: масштабировать раньше, чем найдено узкое место

Антипаттерн: масштабировать раньше, чем найдено узкое место

MAATRIX

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

Как обычно разворачивается этот сценарий

Типичная последовательность событий выглядит так: приходит алерт о росте времени ответа или очереди запросов, дежурный смотрит на CPU веб-серверов, видит загрузку 70-90%, и решение кажется очевидным — добавить ещё пару инстансов приложения или переключиться на план с большим числом ядер. Это делается за 10 минут через панель управления, не требует code review, не нужно ничего чинить в коде — соблазн понятен.

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

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

Почему серверы приложения не лечат медленную БД

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

Если в этот момент добавить ещё серверы приложения, произойдёт следующее:

  • Количество параллельных воркеров вырастет, а значит вырастет и количество одновременных запросов к базе.
  • База, которая уже была узким местом при N подключениях, теперь получает 2N или 3N подключений.
  • Время ответа на отдельный запрос не уменьшится (сам запрос не стал быстрее), а конкуренция за блокировки, буферный пул и I/O диска может даже вырасти.
  • В худшем случае вы упрётесь в лимит подключений СУБД раньше, чем получите видимое улучшение — см. too many connections в MySQL или исчерпание max_connections в PostgreSQL.

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

Это работает и в обратную сторону: если узкое место — это неэффективный код (N+1 запросы, отсутствие кеширования, синхронный вызов там, где нужен асинхронный, лишняя сериализация), то дополнительные серверы приложения просто размножат этот неэффективный код на большее число машин. Каждый инстанс будет так же не оптимален, просто их станет больше — вы купите масштаб неэффективности, а не производительность.

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

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

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

Куда уходят деньги и время

Экономика этого антипаттерна проста и неприятна:

  • Вы платите за ресурсы, которые не устраняют причину. Дополнительные vCPU и RAM на серверах приложения, которые всё это время простаивают в ожидании ответа от БД или диска, не превращаются в скорость для пользователя.
  • Проблема возвращается. Через какое-то время (при росте трафика, объёма данных или просто по нарастающей) симптомы проявляются снова, потому что корневая причина осталась на месте. Приходится снова масштабироваться — и снова это не помогает системно.
  • Стоимость масштабирования нелинейна. Переход на план в 2 раза больше почти никогда не стоит ровно в 2 раза дороже — тарифная сетка провайдеров устроена так, что верхние конфигурации дороже за единицу ресурса. У читателей будет разная тарифная сетка, но сам принцип стоит проверить в своей.
  • Упущенное время. Пока команда наблюдает за метриками после масштабирования и ждёт, поможет оно или нет, реальная причина не расследуется. Это часы или дни, которые можно было потратить на профилирование.

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

Как масштабирование маскирует диагностику

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

  • Симптомы размываются по большему числу узлов. Вместо одного сервера с чёткой картиной (высокий CPU, растущая очередь, конкретные медленные запросы в логе) вы получаете 4-6 серверов, на каждом из которых нагрузка ниже и картина смазаннее. Найти конкретный медленный запрос или конкретный горячий путь в коде становится сложнее — данные для анализа теперь размазаны по нескольким инстансам, и их нужно агрегировать, прежде чем что-то станет видно.
  • Ложное ощущение улучшения. Если после масштабирования пиковая загрузка на каждый отдельный сервер снизилась просто потому, что нагрузка теперь делится на большее число машин, это может создать иллюзию, что проблема решена — хотя суммарное время ответа для пользователя не изменилось или изменилось незначительно.
  • Усложняется трассировка запроса. Запрос теперь проходит через балансировщик к одному из N инстансов, и без сквозного трейсинга (request ID, распределённый трейсинг через OpenTelemetry или аналог) труднее восстановить полный путь конкретного медленного запроса и понять, на каком шаге он застрял.

Итог: масштабирование, предпринятое до диагностики, не только не решает проблему, но и повышает стоимость (по времени и деньгам) её последующего решения.

Правильный порядок: сначала измерение, потом решение

Прежде чем менять план сервера или добавлять инстансы, стоит закрыть три вопроса:

  1. Где именно теряется время? На сети, на диске, в ожидании ответа от БД, в самом коде приложения (сериализация, лишние циклы, синхронные блокирующие вызовы), в ожидании внешнего API?
  2. Это упирается в ресурс (CPU/RAM/I/O) или в конкуренцию (блокировки, лимит соединений, single-threaded участок кода)? Первое лечится масштабированием, второе — почти никогда.
  3. Воспроизводится ли проблема стабильно или это разовый всплеск? Разовые всплески часто не требуют архитектурных изменений вообще, только временного запаса ресурсов.

Практический порядок действий:

  1. Смотрите на систему целиком, а не на один узел: top/htop и vmstat на серверах приложения и БД одновременно, iostat -x 1 для диска, сетевые метрики.
  2. Проверяете, что именно ждут "загруженные" процессы: состояние D (uninterruptible sleep) в выводе ps aux часто указывает на ожидание диска, а не на вычисления.
  3. Смотрите на сторону БД: активные запросы (SHOW PROCESSLIST в MySQL, pg_stat_activity в PostgreSQL), включаете журнал медленных запросов, поднимаете pg_stat_statements или performance_schema, чтобы увидеть, какие запросы съедают больше всего суммарного времени.
  4. Только после того как узкое место локализовано, выбираете конкретное точечное решение — это может быть масштабирование, но так же часто это индекс, кеш, переписанный запрос или изменение архитектуры доступа к данным.

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

Инструменты для локализации узкого места

Набор инструментов зависит от слоя, но вот отправная точка по каждому:

Уровень ОС и сети:

top / htop          — общая картина по CPU и памяти
vmstat 1             — переключения контекста, очередь на CPU
iostat -x 1           — загрузка и латентность диска per-device
ss -s / netstat -an  — число и состояние соединений

Уровень приложения:

  • Профилировщики с флейм-графами: py-spy для Python, async-profiler для Java/Kotlin, встроенный профилировщик node --prof для Node.js, pprof для Go.
  • APM/трейсинг: OpenTelemetry с любым бэкендом, или сервисы вроде Datadog/New Relic — они показывают breakdown времени запроса по слоям (код / БД / внешние вызовы) без ручного профилирования каждого инцидента.
  • Логи с таймингами на уровне запроса — даже простое логирование времени выполнения каждого шага обработчика часто быстро указывает на виновника.

Уровень БД:

-- PostgreSQL: включить и посмотреть самые дорогие запросы
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SELECT query, calls, total_exec_time, mean_exec_time
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

-- разобрать план конкретного медленного запроса
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;
-- MySQL: включить лог медленных запросов
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
-- затем анализировать через mysqldumpslow или pt-query-digest

Если картина показывает, что тормозят конкретные запросы из-за отсутствующего индекса или плохого плана — читайте разбор почему база игнорирует ваш индекс и медленные запросы MySQL: причины и решение: в обоих случаях добавление серверов приложения не изменит план выполнения запроса и не решит проблему.

Когда масштабирование — действительно правильный ответ

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

Характерные признаки, что дело в ресурсах:

  • CPU на серверах приложения устойчиво высокий (state R, не D), при этом запросы к БД быстрые (единицы-десятки миллисекунд по данным pg_stat_statements/slow log).
  • Профилирование показывает, что время тратится на реальные вычисления приложения (сериализация большого объёма данных, шифрование, обработка изображений, инференс модели), а не на ожидание.
  • Рост нагрузки линейно связан с ростом полезного трафика, а не с деградацией из-за внутренней конкуренции за ресурс.

В этом случае горизонтальное масштабирование серверов приложения (или вертикальное — план с большим числом ядер) — прямое и обоснованное решение, потому что вы добавляете ресурс именно туда, где он упирается в потолок. Разница между "масштабировать по интуиции" и "масштабировать по диагностике" не в самом действии, а в том, что ему предшествовало.

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

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

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

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

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

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

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

Сколько времени должна занимать диагностика перед тем, как масштабироваться?

Если инцидент активный и пользователи страдают прямо сейчас, иногда оправданно временно накинуть ресурсов как экстренную меру и параллельно запустить диагностику — это не то же самое, что масштабирование вместо диагностики. Ключевое отличие: временная мера с планом расследования против постоянного решения без расследования вообще. Базовая локализация (ОС/сеть/БД/приложение) обычно занимает от 30 минут до нескольких часов в зависимости от сложности стека.

Можно ли масштабироваться "на всякий случай", не дожидаясь проблемы?

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

Что делать, если узкое место — БД, а переписать запросы быстро нельзя?

Промежуточные меры существуют: кеширование частых запросов, read-реплики под чтение, добавление недостающего индекса (часто быстрее, чем кажется), настройка work_mem/буферного пула. Это тоже "точечное решение по диагнозу", а не слепое масштабирование серверов приложения.

Автомасштабирование — это всегда плохо?

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

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

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

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

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

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