Вертикальное и горизонтальное масштабирование
Интуиция: кофейня в час пик
Представь кофейню, где утром выстраивается очередь. Можно сделать две вещи:
- Нанять бариста-чемпиона и купить ему кофемашину помощнее. Это вертикальное масштабирование: та же одна точка, только мощнее.
- Поставить ещё три стойки с обычными бариста. Это горизонтальное масштабирование: больше одинаковых исполнителей.
У чемпиона есть потолок: быстрее человека не бывает, а его зарплата растёт быстрее скорости. Если он заболел, кофейня закрыта. Несколько стоек дешевле, переживают болезнь одного бариста и растут почти без предела. Но нужен кто-то у входа, кто направляет гостей к свободной стойке, то есть балансировщик.
Вертикальное 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 упоминай как костыль для легаси.
Схема
Обрати внимание: 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 раскладывает шесть запросов по трём серверам:
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.
- Называть алгоритм без причины. Всегда добавляй, почему выбрал именно его.
Ты только что разобрал фундамент всей главы о масштабировании. Дальше будем строить на нём.