English· Español· Deutsch· Nederlands· Français· 日本語· ქართული· 繁體中文· 简体中文· Português· Русский· العربية· हिन्दी· Italiano· 한국어· Polski· Svenska· Türkçe· Українська· Tiếng Việt· Bahasa Indonesia

un

гость
1 / ?
назад к урокам

Два способа принять большую нагрузку

Приветствие

Когда сервис начинает не справляться с нагрузкой, оператору приходится делать выбор. Увеличить мощность существующего сервера (больше CPU, больше RAM, более быстрые диски). Или добавить больше серверов, каждый из которых выполняет ту же работу.

Первый путь называется вертикальным масштабированием (scale up). Второй — горизонтальным масштабированием (scale out).

В этом уроке мы разберём, почему почти каждая современная веб-архитектура выбирает горизонтальное масштабирование и какое свойство рабочей нагрузки делает этот выбор возможным. Ответ кроется в одном слове: состояние (state).

К концу урока вы поймёте:

- Кривые затрат при вертикальном и горизонтальном масштабировании и в каких случаях каждый подход уместен

- Что на практике означают термины «состояние» (stateful) и «без состояния» (stateless), и почему один из этих подходов масштабируется дёшево

- Математика, позволяющая определить размер флота реплик при ожидаемой и пиковой нагрузке

- Правило запаса мощности, которое не даёт слою системы «сломаться» за точкой перегиба в очереди

- Где должно храниться состояние (оно никуда не исчезает) и как вынести его из слоёв, которые требуют масштабирования

Почему горизонтальное масштабирование выигрывает за определённым порогом

Вертикальное масштабирование: один более мощный сервер

Плюсы: простота. Нет изменений в коде. Нет координации. Теперь у того же процесса больше CPU.

Минусы: потолок. У самой большой коммерчески доступной ВМ конечное количество ОЗУ и ядер. Выше этого предела никакие деньги не дадут больше ресурсов. Затраты растут сверхлинейно за пределами оптимальной точки предложения вендора. Сбой на этой одной машине выводит из строя весь сервис.

Горизонтальное масштабирование: много маленьких машин

Плюсы: нет потолка (до тех пор, пока вы готовы платить за машины и координировать их). Вмещательность линейно и предсказуемо растет с количеством реплик. Сбой одной реплики уменьшает пропускную способность на 1/N, а не на 100%.

Минусы: требует, чтобы рабочая нагрузка это поддерживала. Некоторые нагрузки (одна большая база данных, stateful игровой сервер, удерживающий активные сессии) сопротивляются горизонтальному масштабированию. Координация и распределение нагрузки становятся операционными проблемами.

Точка пересечения: любой производственный сервис, который должен пережить сбой любой отдельной машины, должен работать как минимум на двух машинах. Как только вы принимаете две, вы уже выбрали горизонтальное масштабирование. Отсюда вопрос уже не «должны ли мы?», а «насколько дешево мы можем добавить следующую реплику?».

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

Вертикальное и горизонтальное масштабирование: кривая затрат и потолок

Команда запускает сервис на одной ВМ с 8 ЦП, обрабатывая 800 запросов в секунду при загрузке ЦП 60%. Ожидается, что трафик вырастет в 4 раза в течение следующего года. Они обсуждают: арендовать ВМ с 32 ЦП (вертикальное масштабирование) или запустить четыре идентичные ВМ с 8 ЦП за балансировщиком нагрузки (горизонтальное масштабирование). Какой вариант вы рекомендуете и назовите две причины, выходящие за рамки чистой пропускной способности.

Состояние и отсутствие состояния на практике

Состояние никогда не исчезает, оно просто перемещается

Компонент с состоянием (Stateful): хранит информацию, потеря которой изменила бы поведение. База данных, хранящая учётные записи пользователей. Кэш, хранящий токены сессий. Воркер, привязывающий длинное потоковое соединение к конкретному пользователю.

Бессостоятельный компонент (Stateless): не хранит никакой информации, потеря которой имела бы значение. Веб-слой, который читает запрос, обращается к базе данных и записывает ответ. Каждый запрос независим; слой ничего не запоминает между запросами.

Ключевой вывод: состояние никогда не исчезает из системы. Оно перемещается в слой, спроектированный для его хранения (база данных, кластер Redis, объектное хранилище). Тогда слои, обрабатывающие трафик, могут стать бессостоятельными, а бессостоятельные слои масштабируются горизонтально, поскольку любой репликат может ответить на любой запрос.

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

Примеры

- Веб-процесс на Python, который читает запросы, обращается к Postgres и возвращает JSON: бессостоятельный. Состояние находится в Postgres.

- Веб-процесс на Python, который хранит пользовательские корзины покупок в локальной памяти: состоятельный. Завершение процесса приведёт к потере корзин.

- Сервер WebSocket, поддерживающий открытые соединения с пользователями чата: состояние в смысле соединений. Если процесс будет завершён, соединения будут разорваны; клиентам необходимо переподключиться. Часто такие системы всё же масштабируются горизонтально при тщательном подходе (липкие сессии, согласованное хеширование).

- Кэш Redis перед Postgres: состояние для содержимого кэша, но это допустимо, если промахи кэша приемлемы. Сбой реплики означает промах кэша, а не потерю данных.

Проектирование для горизонтального масштабирования = вынос состояния из слоя, который нужно масштабировать.

Аудит подозрительного слоя

Команда запускает API рекомендаций на 6 виртуальных машинах (VM) за обратным прокси. Приложение: считывает ID пользователя из запроса, извлекает недавнюю активность пользователя из Postgres, запускает алгоритм скоринга, возвращает список рекомендуемых элементов. Два нестандартных поведения:

- Приложение хранит кэш «недавняя активность пользователя» в памяти процесса, который заполняется при первом запросе для пользователя и переиспользуется при последующих запросах.

- Приложение использует липкие сессии: как только пользователь обращается к VM №3, все его последующие запросы направляются на VM №3 (прокси настроен на липкую маршрутизацию по cookie).

Определите, какое из этих двух поведений делает слой состоятельным, и объясните, что сломалось бы, если бы команда попыталась масштабировать с 6 VM до 12. Затем предложите переработку, которая позволит слою масштабироваться горизонтально без потери преимуществ пользовательского кэша.

Формула реплик

Самая простая формула пропускной способности

Когда тир становится stateless, его масштабирование сводится к арифметике. Вам нужно достаточное количество реплик, чтобы нагрузка в стационарном режиме приходила и уходила с одинаковой скоростью, с запасом на пиковые нагрузки.

Формула:

replicas = ⌈ (peak_load × surge_factor) / per_replica_capacity ⌉ + headroom

Где:

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

- surge_factor: множитель, покрывающий кратковременные всплески выше пиковой нагрузки (обычно 1,5x–2x для предсказуемого трафика, 3x и более для вирусного/непредсказуемого)

- per_replica_capacity: количество запросов в секунду, которое одна реплика обрабатывает с приемлемой задержкой и загрузкой (обычно измеряется при 70% загрузки CPU, а не при насыщении)

- headroom: дополнительные реплики, чтобы несколько отказов реплик не привели к сбою всего тира (обычно 1–2 реплики для небольших кластеров, 10–20% для более крупных)

Разобраный пример: бэкенд обрабатывает 100 запросов/с при загрузке CPU 70% на один реплику. Пиковая нагрузка составляет 600 запросов/с. Ожидается периодический рост нагрузки в 2 раза. Необходимо обеспечить работоспособность при отказе 2 реплик.

replicas = ⌈ (600 × 2) / 100 ⌉ + 2 = 12 + 2 = 14 реплик

Правило 80%

Пропускная способность одной реплики — это не точка насыщения. Пропускную способность следует измерять при загрузке CPU 70–80%, а не при 100%.

После достижения примерно 80% загрузки кривые очередей резко возрастают: очередь, которая обрабатывалась за 10 мс при загрузке 60%, обрабатывается за 80 мс при загрузке 90%. Сначала ломается не пропускная способность, а задержка. (В сопутствующем уроке geometry_of_stateless_horizontal_scaling эта кривая выводится математически.)

Автомасштабирование против статического провижининга

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

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

Важное замечание об автоскейлинге: время холодного запуска имеет значение. Если на запуск нового репликата уходит 2 минуты, автоскейлинг не сможет отреагировать на всплеск нагрузки длительностью 30 секунд. Зрелые системы автоскейлинга поддерживают «тёплый» пул заранее провижнированных репликатов, находящийся чуть ниже порога масштабирования вверх.

Формула расчёта размера репликата с примером

Расчёт размера флота для нового сервиса

Ваша команда планирует запустить API метаданных видео. Бенчмарки показывают, что один репликат обрабатывает 250 запросов/с при загрузке CPU 70% и задержке p99 50 мс. Маркетинг прогнозирует пиковую нагрузку в 4 000 запросов/с в часы пикового спроса. Планируемое промо-событие может кратковременно увеличить нагрузку в 3 раза от пика. Вы хотите, чтобы сервис выдерживал одновременный отказ 3 репликатов, не превышая 80% загрузки на оставшихся.

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

Холодный старт, медленное завершение и другие реальные граничные случаи

У реальных флотов есть реальные граничные случаи

Формула предполагает, что реплики появляются мгновенно, сразу принимают трафик и мгновенно прекращают его обработку. В продакшене ни одно из этих условий не выполняется.

Холодный старт: новой реплике необходимо загрузить ОС, запустить процесс, загрузить конфигурацию, разогреть кэши и пройти проверки работоспособности. Это может занять от 5 секунд (перезапуск контейнера) до 5 минут (полная загрузка ВМ + загрузка образа). Автомасштабирование не может реагировать на всплески нагрузки, короче этой задержки.

Медленное завершение: реплике, удаляемой из пула, требуется время, чтобы завершить текущие запросы перед остановкой. Иначе пользователи увидят обрывки ответов. Обратные прокси поддерживают режим drain (прекращение приёма новых запросов, завершение активных), но это занимает от секунд до минут.

Тёплый пул: в производственных средах поддерживается пул заранее provisioned, но простаивающих реплик, готовых принять трафик по сигналу. Это небольшая постоянная плата за быстрое реагирование на всплески нагрузки.

Слив соединений (draining) против немедленного завершения: важно корректное завершение работы. SIGTERM, инициирующий слив соединений, занимает больше времени, чем SIGKILL, но не прерывает пользовательские запросы.

Окно проверки работоспособности: только что запущенная реплика может пройти первую проверку работоспособности до того, как пул соединений с базой данных станет «тёплым»; тогда прокси начинает отправлять реальный трафик, и первые десяток запросов выполняются медленно. Настройте проверки так, чтобы они тестировали реальный путь обработки, а не только факт запуска процесса.

Постепенное накопление состояния (stickiness creep): даже формально безсостоятельные уровни со временем приобретают состояние (кэши CDN, кэши DNS-резолверов, пулы соединений). Будьте настороже, если «идентичные реплики» тем не менее ведут себя по-разному.

Тёплый пул или реактивное автомасштабирование?

Ваш API метаданных видео (тот же самый, что и в предыдущем вопросе, рассчитанный на 51 реплику для стабильного пика + всплесков) испытывает 30-секундный всплеск нагрузки в 5 раз выше обычного каждый раз, когда загружается новый вирусный видеоролик. Автомасштабирование в настоящее время занимает 90 секунд, чтобы добавить новую реплику с нуля (загрузка образа + прогрев). В течение этого 90-секундного интервала задержка резко возрастает, и некоторые запросы завершаются по таймауту.

Предложите решение. Выберите: (a) оставить реактивное автомасштабирование, но настроить его иначе, (b) provisioned тёплый пул простаивающих реплик, или (c) статически provisioned ресурсы под пиковую нагрузку в 5 раз постоянно. Обоснуйте свой выбор и назовите одну стоимость, которую влекли бы за собой два других варианта.

Проектирование безсостоянного уровня в условиях ограничений

Синтез

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

Примените все четыре.

Спроектируйте бэкенд-слой для feed.example.com, API социальной ленты. Ограничения: пропускная способность на реплику 200 запросов/с при загрузке CPU 70%; ожидаемая пиковая нагрузка 1500 запросов/с; коэффициент всплеска 2,5x (периодические трендовые новости); выживание при одновременном отказе 2 реплик; время холодного запуска 60 секунд; всплески могут длиться 45 секунд; бюджет позволяет иметь некоторый резерв мощности, но не постоянное provisioning в 2,5 раза.

Рассчитайте размер постоянного флота (покажите расчет), выберите стратегию обработки всплесков (теплый пул / реактивное масштабирование / оба) и определите один элемент состояния на пользователя, который приложение, вероятно, хранит, и куда он должен переместиться, чтобы слой оставался без состояния.

Куда приведет этот курс дальше

К чему ведёт этот курс

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

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

Сопутствующий урок: geometry_of_stateless_horizontal_scaling выводит кривую очередей, применяет закон Литтла к флоту реплик и раскрывает геометрический смысл «колена» при 80% загрузке.

Отличная работа. Вперёд.