Ctrl K
Урок

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

Интуиция: кофейня в час пик

Представь кофейню, где утром выстраивается очередь. Можно сделать две вещи:

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

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

Вертикальное vs горизонтальное

Вертикальное (scale up) Горизонтальное (scale out)
Что делаем больше CPU/RAM/SSD одной машине добавляем машины
Сложность почти ноль, код не меняется нужен stateless-код и балансировщик
Предел самая большая доступная машина почти не ограничен
Цена растёт нелинейно, топовое железо дорогое растёт примерно линейно
Отказоустойчивость одна точка отказа падение одной машины не роняет сервис
Апгрейд часто с даунтаймом по одной машине, без простоя

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

Прикинем числами

Допустим, один инстанс API держит ~2 000 RPS, а в пике приходит 10 000 RPS.

  • Базовая потребность: 10 000 / 2 000 = 5 инстансов
  • Держим загрузку не выше 70%, чтобы был запас: 5 / 0.7 ≈ 8 инстансов
  • Закладываем отказ одной зоны (N+1): 9–10 инстансов

Такой расчёт стоит проговорить вслух. Интервьюер видит, что ты думаешь о пиках и отказах, а не только о среднем.

Stateless: главное условие горизонтали

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

Типичная ловушка — сессия пользователя. Если она лежит в памяти API #1, а следующий запрос попал на API #2, пользователя «разлогинит». Варианты решения:

Способ Как работает Компромисс
Sticky sessions балансировщик закрепляет клиента за одним сервером неравномерная нагрузка; при падении сервера сессии теряются
Общее хранилище (Redis) сессия по session_id лежит в Redis, читать её может любой API +1 сетевой поход; Redis нужно реплицировать
Токен у клиента (JWT) состояние подписано и лежит в самом токене сложно отозвать досрочно; токен растёт

Ответ по умолчанию на собесе: «Сервисы stateless, сессии храню в Redis с TTL, либо использую короткоживущий JWT плюс refresh-токен». Sticky sessions упоминай как костыль для легаси.

Схема

HTTPSсессияКлиентыклиентБалансировщик (L7)LBAPI #1 (stateless)сервисAPI #2 (stateless)сервисAPI #3 (stateless)сервисRedis: сессиикэшPostgreSQLБД
Горизонтальное масштабирование: балансировщик раздаёт запросы одинаковым stateless-инстансам, состояние вынесено в Redis и БД

Обрати внимание: API размножились, а база одна. Рано или поздно узким местом станет она. Её масштабируют вертикально, репликами на чтение и шардированием, но это тема следующих уроков. На собесе честно скажи: «Следующее узкое место — БД».

Балансировщик нагрузки

L4 или L7

  • L4 (транспортный уровень) видит только IP и порт, внутрь трафика не заглядывает. Он очень быстрый и дешёвый, подходит для любого TCP/UDP.
  • L7 (прикладной уровень) читает HTTP: путь, заголовки, cookie. Умеет маршрутизировать /api/* и /static/* в разные сервисы, терминировать TLS, делать sticky по cookie. Он медленнее и тратит больше CPU.

Частая комбинация такая: L4 на входе для сырой пропускной способности, за ним L7 для умной маршрутизации.

Алгоритмы

Алгоритм Суть Когда хорош
Round robin по очереди: 1, 2, 3, 1, 2, 3… запросы похожи, серверы одинаковые
Weighted RR мощным серверам достаётся больше разное железо
Least connections туда, где меньше активных соединений запросы разной длины, WebSocket
Consistent hashing сервер выбирается по хешу ключа (user_id) нужен локальный кэш или привязка к ключу; при добавлении сервера переезжает лишь ~1/N ключей

Вот как round robin раскладывает шесть запросов по трём серверам:

след. запрос ↓ API10
API21
API32
API13
API24
API35
Round robin: запросы идут по кругу, API #1 получает 1-й и 4-й

Health checks

Балансировщик регулярно опрашивает каждый инстанс, например GET /health раз в 5 секунд. После нескольких неудачных проверок подряд он выводит инстанс из ротации, а после нескольких успешных возвращает обратно.

  • Liveness проверяет, жив ли процесс.
  • Readiness проверяет, готов ли инстанс принимать трафик: прогрелся, есть соединение с БД.

Не делай health check «глубоким» до упора. Если /health проверяет БД, то при её секундном сбое балансировщик выкинет все инстансы разом.

Сам балансировщик тоже не должен быть единой точкой отказа. Ставят пару в режиме active-passive с плавающим IP или берут управляемый облачный LB.

Как это звучит на собесе

«Начну с одного сервера и при необходимости подниму ему ресурсы, это проще всего. Для роста и отказоустойчивости делаю API stateless, сессии выношу в Redis. Перед инстансами ставлю L7-балансировщик с least connections и health checks, сам LB дублирую. При 10k RPS пика и 2k на инстанс это около 9–10 машин с запасом. Дальше узким местом станет база».

Частые ошибки

  • Сразу прыгать в «сотни микросервисов», не назвав более простой вертикальный вариант.
  • Масштабировать API горизонтально, но оставить сессии в памяти процесса.
  • Забыть, что балансировщик и база тоже могут стать точкой отказа.
  • Считать по средней нагрузке вместо пиковой и не закладывать N+1.
  • Называть алгоритм без причины. Всегда добавляй, почему выбрал именно его.

Ты только что разобрал фундамент всей главы о масштабировании. Дальше будем строить на нём.