Когда системе нужно масштабирование
Кризис роста: почему масштабирование начинается не с серверов
Слово «масштабирование» часто появляется в проекте слишком поздно. Пока система работает нормально, о нём не вспоминают.
Затем пользователей становится больше, и проявляются классические симптомы деградации:
- › запросы начинают выполняться медленнее;
- › сервер загружается сильнее;
- › база данных упирается в ограничения.
«Нужно добавить мощности».
Иногда добавление ресурсов — это действительно правильный и экономически оправданный шаг.
Но очень часто — нет. Если система тормозит из-за одного неудачного SQL-запроса, нехватки индексов, неправильного кэширования или блокировок в базе, ещё один сервер не устранит причину.
Масштабирование — это не просто увеличение CPU, памяти или количества серверов.
Это изменение архитектуры таким образом, чтобы система могла выдерживать растущую нагрузку без пропорционального роста проблем.
И начинать здесь нужно не с вопроса «сколько серверов добавить», а с другого:
Рост пользователей — только один из сценариев
Самый очевидный случай линейного роста выглядит привычно:
└── система работает
1 000 пользователей
└── нагрузка растёт
10 000 пользователей
└── появляются ограничения
Но пользователи — далеко не единственный источник нагрузки. Система может начать испытывать проблемы из-за:
- - роста количества данных;
- - увеличения числа операций;
- - появления новых интеграций;
- - более тяжёлых отчётов;
- - фоновых задач;
- - массовых импортов;
- - увеличения количества файлов;
- - роста частоты запросов;
- - новых функций;
- - запуска мобильного приложения или API;
- - подключения новых филиалов;
- - увеличения количества оборудования и датчиков.
Например, количество пользователей практически не изменилось, но предприятие подключило ещё 300 производственных устройств.
Пользователей столько же. А нагрузка на систему выросла в сотни раз.
Поэтому правильнее говорить не о масштабировании «под пользователей», а о масштабировании под нагрузку.
Сначала нужно найти узкое место
Предположим, приложение стало медленным. На первый взгляд можно просто увеличить вычислительные мощности backend-сервера.
Но если проблема находится непосредственно в слое СУБД, то увеличение количества серверов приложения мало что изменит в общей картине.
В таком случае ошибочное масштабирование backend-компонентов создаст деструктивную конфигурацию:
Backend 2 ──┤
Backend 3 ──┼──► Database
Backend 4 ──┘ │
X
Приложений стало больше. База осталась прежней. И теперь несколько серверов одновременно создают ещё больше запросов к перегруженной базе.
Масштабировать нужно не самый заметный компонент, а ограничивающий производительность.
Прежде чем масштабировать, нужно понять причину
Хорошая архитектура начинается с измерений. Нас интересует не только среднее время ответа.
Выглядит отлично на общих графиках мониторинга.
90% запросов → 150 мс
95% запросов → 300 мс
99% запросов → 4 500 мс
У части пользователей система уже практически не работает.
Поэтому при комплексном анализе нагрузки смотрят как минимум на:
- › latency;
- › throughput;
- › количество запросов;
- › CPU;
- › RAM;
- › дисковую подсистему;
- › сетевой трафик;
- › количество соединений;
- › очередь задач;
- › время выполнения запросов;
- › блокировки;
- › ошибки;
- › состояние базы данных.
И очень важно смотреть на показатели во времени. Система может прекрасно работать утром и регулярно деградировать каждый будний день в 18:00. Это уже совершенно другой инженерный сценарий.
Вертикальное масштабирование: сначала сделать сервер мощнее
Самый простой вариант — увеличить ресурсы существующего узла.
Application Server
8 GB RAM
Application Server
64 GB RAM
Это называется вертикальным масштабированием. Иногда такой шаг является лучшим решением. Например, если приложение хорошо работает на одном сервере, а ограничение связано с нехваткой памяти, нет смысла немедленно превращать его в распределённую систему из десяти узлов. Можно просто дать ему больше ресурсов.
Преимущество очевидно: архитектура почти не усложняется.
В инфраструктуре не появляются:
- › балансировщик;
- › распределённые сессии;
- › дополнительные сетевые взаимодействия;
- › синхронизация нескольких экземпляров;
- › новые точки отказа.
Но есть фундаментальное ограничение:
у конкретного сервера есть физический и экономический предел. Нельзя бесконечно увеличивать его мощность.
Когда вертикального масштабирования достаточно
Оно особенно разумно, когда выполняются следующие условия:
- › нагрузка выросла умеренно;
- › приложение пока хорошо помещается на одном узле;
- › система плохо приспособлена к горизонтальному масштабированию;
- › стоимость более мощного сервера приемлема;
- › требуется быстро увеличить запас производительности;
- › усложнение архитектуры принесёт больше риска, чем пользы.
Иногда команда пытается построить распределённую архитектуру слишком рано. Получается система из множества сервисов, брокеров, балансировщиков и реплик, хотя исходную нагрузку спокойно выдержал бы один хорошо настроенный сервер.
Масштабирование само по себе не является архитектурной целью.
Цель — обеспечить требуемую производительность, надёжность и запас роста с разумной сложностью.
Горизонтальное масштабирование: добавляем узлы
Другой подход — не делать один сервер всё мощнее, а добавлять новые экземпляры приложения.
Теперь входящие запросы распределяются между несколькими запущенными экземплярами.
Если один сервер был изначально способен обработать:
то запуск нескольких параллельных экземпляров потенциально позволяет пропорционально увеличить общую производительность.
Но слово «потенциально» здесь принципиально важно.
Три сервера не гарантируют автоматического трикратного ускорения системы.
Почему добавление серверов не всегда помогает
Горизонтальное масштабирование одного слоя без учета смежных компонентов не решает проблему производительности.
Backend 2 ──┤
Backend 3 ──┼──► Database
Backend 4 ──┘
Если база выдерживает только определённую нагрузку, приложение неизбежно упрётся в её ограничения.
Backend 2
Backend 3
│
▼
Storage
X
Дисковая подсистема или сетевое объектное хранилище (Storage) блокируют параллельную обработку файлов.
Backend 2
Backend 3
│
▼
External API
X
Лимиты сторонних интеграций (Rate Limits) или их низкая скорость делают локальные вычислительные узлы неэффективными.
Система всегда имеет ограничивающий ресурс.
Поэтому горизонтальное масштабирование одного слоя может просто перенести проблему на следующий.
Балансировщик нагрузки — не волшебная коробка
Когда появляется несколько экземпляров приложения, возникает вопрос: куда отправлять очередной запрос? Для этого используется балансировщик нагрузки.
Он может распределять запросы по разным алгоритмам и учитывать состояние серверов. Но здесь появляется новая инженерная задача. Что произойдёт, если один экземпляр приложения перестал отвечать? Балансировщик должен перестать направлять ему новые запросы.
Поэтому в системе настраиваются циклы опроса доступности (health checks):
И это уже полноценная часть отказоустойчивой архитектуры, а не просто базовый способ «раздать входящий трафик».
Горизонтальное масштабирование требует stateless-подхода
Один из классических вопросов возникает с пользовательской сессией при работе в кластере.
Предположим линейную последовательность действий пользователя:
Следующий запрос:
Запрос №2 → Server 2 (Ничего не знает о состоянии с Server 1)
В изоляции узлов получается критический разрыв контекста:
└── session
Server 2
└── ничего
Поэтому при горизонтальном масштабировании состояние обычно выносят из конкретного экземпляра приложения на выделенный распределенный слой.
Целевая архитектурная схема организации Shared State:
В зависимости от задачи это может быть отдельное хранилище сессий, распределенный кэш, база данных или другой централизованный механизм.
Любой экземпляр приложения должен иметь возможность обработать запрос без зависимости от локального состояния другого экземпляра.
Именно это условие делает горизонтальное масштабирование существенно сложнее простого добавления серверов.
Кэширование: иногда масштабировать вообще ничего не нужно
Есть ещё один инструмент, который часто даёт больший эффект, чем увеличение инфраструктуры — кэширование.
Представим классический повторяющийся запрос:
Каждый раз приложение обращается напрямую к базе:
Application → Database → result
Если данные меняются редко, нет никакого инженерного смысла выполнять одну и ту же тяжёлую операцию тысячи раз. Можно сохранить полученный результат.
Теперь значительная часть входящих запросов вообще не доходит до базы данных, разгружая всю систему.
Но кэш тоже создаёт архитектурные проблемы
Самый известный вопрос инвалидации: когда именно данные в кэше устаревают?
Рассмотрим классический пример рассинхронизации слоёв:
Cache: price = 1 000 ₽
Cache: price = 1 000 ₽
Пользователь получает устаревшую информацию.
Поэтому кэширование — это не просто директивная задача «поставить Redis рядом».
Перед проектированием кэш-слоя нужно чётко определить:
- › что именно можно кэшировать;
- › на какое конкретно время;
- › кто инициирует обновление кэша;
- › что происходит непосредственно при изменении данных;
- › что происходит в сценарии полной недоступности кэш-хранилища;
- › допустима ли вообще устаревшая информация для бизнес-логики.
Иногда несколько миллисекунд выигрыша от использования кэша совершенно не стоят той архитектурной сложности, которую он добавляет системе.
Самая недооценённая проблема — база данных
В типичной бизнес-системе база данных очень часто становится одним из первых серьёзных ограничений.
Приложение можно масштабировать почти бесконечно. Но база данных при этом остаётся одной.
В этот момент нужно исследовать не только количество доступного CPU.
Проблема непроизводительного поведения может находиться в:
- - плохих SQL-запросах;
- - отсутствии индексов;
- - слишком больших таблицах;
- - блокировках;
- - чрезмерном количестве соединений;
- - тяжёлых JOIN;
- - массовых UPDATE;
- - неэффективной пагинации;
- - неправильной структуре данных;
- - синхронных фоновых операциях.
Иногда точечная оптимизация всего одного запроса даёт значительно больший эффект, чем автоматическое добавление нескольких физических серверов.
Пример: масштабирование, которое не потребовалось
Представим страницу аналитики. Пользователь открывает её, а система выполняет ресурсоёмкую операцию на уровне СУБД:
├── 8 JOIN
├── 5 агрегатов
└── 20 млн строк
└── Время выполнения: 4 секунды
«Страница медленная. Увеличиваем сервер».
Запрос выполняется со стандартными мощностями:
Сервер становится мощнее, но радикальных изменений нет:
Проблема осталась на прежнем уровне. Если исправить сам SQL-запрос, добавить правильные индексы и изменить способ формирования отчёта, итоговое время обработки может уменьшиться гораздо сильнее.
Получается важный инженерный принцип:
Масштабирование ни в коем случае не должно заменять оптимизацию.
Очереди позволяют отделить быстрые операции от тяжёлых
Не каждую задачу нужно выполнять непосредственно во время пользовательского запроса. Представим загрузку документа.
Пользователь вынужден ждать завершения всей цепочки. При росте нагрузки серверы начинают одновременно обрабатывать множество тяжёлых задач, парализуя систему.
Асинхронный подход позволяет полностью разделить процесс:
Теперь количество обработчиков можно изменять абсолютно независимо.
Если документов стало в пять раз больше, не обязательно масштабировать весь backend. Можно увеличить только количество обработчиков:
↓
Queue
↓
Worker × 10
Это уже масштабирование отдельного типа нагрузки.
И часто именно такой подход оказывается правильнее, чем попытка сделать всё монолитное приложение быстрее.
Разделение нагрузки между сервисами
По мере роста системы разные операции начинают иметь совершенно разный профиль нагрузки.
Например, структура единого API-интерфейса:
├── Пользовательские запросы
├── Отчёты
├── Файлы
├── Уведомления
└── Импорт данных
Если всё выполняется одним процессом, тяжёлая операция может повлиять на остальные.
Разделение позволяет масштабировать компоненты независимо:
├── Workers × 10
├── Reports × 3
└── Notifications × 2
Теперь ресурсы распределяются в строгом соответствии с реальной нагрузкой.
Масштабирование — это ещё и разделение типов нагрузки
Очень часто проблема появляется не потому, что запросов стало больше, а потому что разные типы запросов мешают друг другу.
Если аналитика выполняется на тех же вычислительных ресурсах, что и текущие пользовательские операции, один тяжелый вечерний отчёт может замедлить или полностью остановить весь сервис.
Поэтому зрелая архитектура может предусматривать выделение изолированных ресурсов:
Это уже не просто абстрактная задача «дать системе больше CPU».
Это осознанное разделение нагрузки по её прямому назначению.
Как понять, что система действительно подошла к пределу
Нет универсального значения: «CPU выше 70% — пора масштабировать». Такой критерий слишком примитивен.
Например, сервер может стабильно работать при 85% CPU, если latency остаётся в допустимых пределах. И наоборот: CPU = 40%, а пользователи получают ошибки из-за исчерпания пула соединений с базой.
Поэтому сигналом к масштабированию становится не одна цифра, а совокупность признаков:
Нагрузка стабильно приближается к пределу
If a resource regularly operates near its practical maximum, the headroom for growth becomes too narrow.
Растёт latency
Особенно если увеличение времени ответа происходит синхронно с ростом общей нагрузки.
Растут очереди
Если новые задачи постоянно накапливаются быстрее, чем обрабатываются, система уже не успевает за входящим потоком.
Появляются ошибки под нагрузкой
peak load → 4% errors
Это уже серьёзный сигнал.
Пики становятся регулярными
Если система каждый день испытывает перегрузку в определённый период, архитектура должна учитывать этот профиль нагрузки.
Рост бизнеса начинает требовать больше ресурсов
Даже если система пока работает нормально, прогнозируемый рост может сделать масштабирование заранее разумным.
Нужно учитывать не только среднюю нагрузку
У бизнеса редко бывает идеально равномерный поток.
Типичное распределение активности в течение суток выглядит так:
низкая нагрузка
08:00 ──────── 10:00
рост
10:00 ──────── 17:00
высокая
17:00 ──────── 19:00
пик
19:00 ──────── 00:00
снижение
Если проектировать инфраструктуру только под среднее значение, во время пика система гарантированно не справится с входящим потоком.
Если проектировать всё жестко под максимальный пик, большую часть времени дорогостоящие ресурсы будут простаивать.
Для преодоления этого противоречия применяют динамическое автоматическое масштабирование (Auto-scaling):
Это позволяет гибко подстраивать количество выделенных ресурсов под текущую рыночную ситуацию.
Но автоматическое масштабирование эффективно только тогда, когда сама внутренняя архитектура действительно технологически умеет динамически добавлять и бесшовно удалять активные экземпляры.
Масштабирование может быть не только вверх, но и вниз
О масштабировании часто говорят исключительно как о росте. Но зрелая инфраструктура должна уметь уменьшать ресурсы, когда нагрузка снижается.
Workers × 2
Workers × 12
Если система нативно поддерживает такую эластичную модель, компания больше не обязана постоянно оплачивать максимальную конфигурацию инфраструктуры.
Но здесь снова возникают жесткие архитектурные ограничения.
Если состояние хранится локально, если холодный запуск экземпляра занимает несколько минут или если приложение не умеет корректно завершать работу (Graceful Shutdown), автоматическое уменьшение ресурсов гарантированно приведёт к деградации и проблемам.
Есть ещё один предел — сама архитектура приложения
Рано или поздно физические и экономические ограничения упираются в саму структуру кодовой базы.
Представим классическую монолитную систему, где разнородные подсистемы работают в рамках одного процесса:
├── API
├── Reports
├── Billing
├── Files
├── Notifications
└── Background Jobs
Всё работает в одном процессе. Увеличивать его можно довольно долго, но однажды возникает критическая диспропорция требований подсистем:
API требуют: 8 GB RAM
Workers требуют: 16 CPU
А масштабировать всё вместе становится экономически невыгодно. Если добавить ещё один экземпляр монолита, вместе с легковесным API мы получим ещё одну копию тяжёлого отчётного механизма, нерационально расходующую память.
Тогда возникает вопрос о декомпозиции нагрузки. При этом совершенно не обязательно сразу переходить к десяткам микросервисов. Достаточно крупноблочного разделения:
Такая структура уже позволяет полностью независимо масштабировать отдельные функциональные компоненты в соответствии с их реальным профилем потребления ресурсов.
Но дробить систему только ради масштабирования тоже опасно
Есть противоположная ошибка, когда усложнение архитектуры опережает реальные бизнес-потребности.
Команда заранее делит приложение на множество мелких независимых сервисов:
├── Service B
├── Service C
├── Service D
├── Service E
├── Service F
├── Service G
└── Service H
А потом на практике выясняется, что реальная нагрузка совсем небольшая. В итоге вместо одной простой и понятной системы компания получает сложную распределённую архитектуру со своими специфическими накладными расходами.
В инфраструктуре неизбежно появляются:
- › постоянные сетевые вызовы;
- › распределённые очереди;
- › сложный сквозной мониторинг;
- › распределённая трассировка запросов;
- › деплой десятков изолированных компонентов;
- › каскадные дополнительные отказы;
- › высокая сложность комплексного тестирования.
Горизонтальное масштабирование не является самоцелью.
Если один хорошо спроектированный сервис полностью удовлетворяет текущим и прогнозируемым требованиям, это может быть лучшим архитектурным решением.
Когда масштабирование становится обязательным
Есть несколько критических ситуаций, когда дальнейшее откладывание изменений архитектуры становится опасным для бизнеса.
Система упирается в физический предел:
RAM → максимум
Disk → максимум
Нагрузка продолжает расти, а сервер физически невозможно расширить дальше. Вертикальный путь масштабирования полностью исчерпан.
Опережение производительности одного узла
Если бизнес планирует кратное увеличение объёма операций в короткие сроки, распределенную архитектуру необходимо подготовить и протестировать заранее.
Требования к доступности становятся выше
Когда отказ единственного сервера полностью парализует бизнес-процессы, появляется критическая необходимость в нескольких экземплярах приложения:
├──► Service
Server 2 ──┘
Теперь авария на одном узле не означает автоматическую остановку всей системы.
Разные типы нагрузки мешают друг другу
Классическая ситуация: тяжелые аналитические отчёты полностью перегружают пользовательский API. В этот момент становится необходимо аппаратно разделить вычислительные ресурсы.
Появление изолированных точек роста
Когда API, фоновые фоновые задачи и аналитика растут с разной скоростью и требуют принципиально разных ресурсов, единое линейное масштабирование монолита становится экономически неэффективным.
А когда масштабирование пока не нужно
Это тоже важный вопрос, предостерегающий от преждевременного усложнения инфраструктуры.
Если ключевые метрики системы находятся в зелёной зоне:
Errors → низкие
CPU → запас
RAM → запас
Queue → отсутствует
Database → справляется
Traffic → прогнозируемый
нет абсолютно никакого смысла искусственно усложнять текущую архитектуру только потому, что «так делают большие системы».
Лучше иметь простой монолитный сервис с понятными ограничениями, чем распределённую избыточную систему, которую никто в команде не умеет нормально сопровождать.
Любое инженерное решение должно строго соответствовать реальной текущей нагрузке и фактическим требованиям бизнеса.
Практический путь масштабирования
На практике разумно двигаться последовательно, проходя контрольные этапы развития архитектуры.
Порядок не является абсолютной догмой. Ситуации бывают разными, и архитектурный ответ всегда индивидуален.
Иногда специфика системы такова, что сразу необходим полноценный кластер.
Иногда для решения проблемы достаточно добавить один индекс в базе данных.
Иногда острая точечная проблема эффективно решается внедрением очереди задач.
Иногда самым быстрым и правильным решением оказывается просто более мощный сервер.
Главное — никогда не перепрыгивать через этап полноценной диагностики.
Пример архитектуры, которая растёт вместе с нагрузкой
Развитие масштабируемой системы можно представить как последовательность эволюционных этапов.
│
▼
Application
│
▼
Database
Для небольшого проекта этой базовой конфигурации может быть более чем достаточно.
│
▼
Application
│
▼
Database
Ресурсы вычислений и хранения уже изолированы друг от друга, поведение системы становится предсказуемее.
│
┌───────┼───────┐
▼ ▼ ▼
App App App
└───────┼───────┘
▼
Database
Введение балансировки позволяет распределять горизонтальный входящий трафик на слой stateless-приложений.
│
Users → LB → App ┼── Database
│
└── Queue
│
┌────┼────┐
▼ ▼ ▼
Worker Worker Worker
Снижение нагрузки на СУБД с помощью кэш-слоя и полной изоляции тяжелых фоновых процессов через очереди.
│
┌───────────┼───────────┐
▼ ▼ ▼
API Reports Workers
×8 ×2 ×12
│ │ │
└───────────┼───────────┘
│
отдельные ресурсы
по типу нагрузки
Декомпозиция монолита. Полное разделение вычислительных пулов под конкретный профиль утилизации железа.
На каждом архитектурном этапе система становится способна выдерживать следующий, более высокий класс нагрузки.
Но переходить на каждый последующий сложный уровень строго стоит тогда, когда появляется соответствующая обоснованная потребность бизнеса.
Масштабирование нужно планировать до момента отказа
Есть принципиальная разница между аварийным тушением пожаров и упреждающим инженерным планированием.
«Система уже упала, срочно добавляем сервер».
«При такой динамике нагрузки через несколько месяцев текущая архитектура приблизится к пределу, поэтому заранее готовим следующий уровень».
Второй вариант стратегически значительно дешевле и безопаснее для бизнеса.
Для этого полезно смотреть не только на текущее изолированное состояние ресурсов, но и на общий тренд:
Если нагрузка стабильно приближается к критической границе, масштабирование полностью перестаёт быть аварийной реакцией и становится плановой частью регулярного проектирования инфраструктуры.
Главное — масштабировать нужно причину, а не симптом
Система может быть медленной по десяткам причин.
Много пользователей ≠ нужны новые серверы
Каждая ситуация требует точечной локализации источника деградации:
└── Много одинаковых запросов
└── Нет кэша
└── База перегружена
└── OCR выполняется синхронно
└── API занят
└── Database
└── CPU 100%
И здесь сначала необходимо строго оптимизировать сами SQL-запросы или фундаментально изменить логику работы с данными.
И только в изолированных случаях чистая нехватка вычислительных мощностей становится триггером:
└── нагрузка растёт
└── один сервер уже не справляется
И вот тогда действительно нужен второй, третий и все последующие экземпляры.
Итог
Масштабирование начинается не с покупки нового сервера. Оно начинается с понимания того, где именно система перестаёт справляться с нагрузкой.
Позволяет быстро увеличить ресурсы существующего узла и сохранить простую, понятную архитектуру без распределенного состояния.
Позволяет распределить входящую нагрузку между несколькими экземплярами, обеспечивая кратный запас роста и высокую отказоустойчивость.
Но между этими двумя крайними вариантами находится целый набор промежуточных инженерных решений:
Именно поэтому хороший вопрос звучит не так:
«Сколько серверов нам нужно?»
А вот так:
«Какая часть системы является ограничением, как она ведёт себя при росте нагрузки и какой способ устранения этого ограничения даст нам необходимый запас без неоправданного усложнения архитектуры?»
Если на этот вопрос есть измеренный, подтвержденный метриками ответ, то масштабирование становится прозрачной и прогнозируемой инженерной задачей.
Если четкого понимания и численных ответов нет, механическое добавление новых серверов неизбежно превращается в слепую попытку угадать результат.
API, Webhook или очередь сообщений?
Когда API есть, а интеграция всё равно не работает
Как соединить производство и корпоративные системы
Все инженерные материалы и экспертные статьи
Полный алфавитный справочник ИТ-терминов
🔒 Данные и доступ к системам заказчика обрабатываются в соответствии с согласованными требованиями к безопасности и конфиденциальности. Условия доступа, хранения и обработки данных определяются архитектурой проекта и договором.
Сайт носит исключительно информационный характер и не является публичной офертой в соответствии со статьёй 437 Гражданского кодекса РФ. Цены, состав и условия предоставления услуг уточняются при проектировании решения и фиксируются в договоре. Условия технического сопровождения и уровень сервиса могут дополнительно определяться отдельным соглашением (SLA).
Автоматизация бизнеса и ИИ-решения под ключ
Самозанятый Скопец Антон Викторович ИНН 741709260870