Вертикальное против горизонтального масштабирования
Масштабировать вверх — машина побольше; вширь — машин побольше. Вверх просто, но упирается в потолок железа и остаётся SPOF; вширь эластично и отказоустойчиво, но требует stateless и платит координационный налог, который закон универсальной масштабируемости делает конкретным.
API стартапа крутился на одном мощном сервере БД. Трафик рос — и они делали очевидное: инстанс побольше. Потом ещё больше. Потом самый большой, какой давало облако, — 128 vCPU, дороже в месяц, чем два инженера. Когда и он насытился, «следующего размера» не было, а миграция на шардированный многоузловой дизайн, которую надо было начать два года назад, теперь шла во время аварии. Урок не в том, что вертикальное масштабирование плохо. А в том, что они выбрали путь с потолком, ни разу не спросив, где потолок.
Два направления
Когда упираешься в потолок по ёмкости, ходов ровно два. Понять, чего каждый из них стоит и где каждый ломается — вот что отделяет тушение пожара от решения, о котором не пожалеешь через год.
Вертикальное масштабирование (вверх) — дать одной машине больше ресурсов: больше CPU, RAM, быстрее диски, инстанс крупнее. Приложение почти не меняется; перезагружаешься на железо побольше — и ёмкость растёт. Горизонтальное (вширь) — добавить машины и размазать работу по ним за балансировщиком.
Привлекательность «вверх» — почти нулевые инженерные затраты: никаких распределённых проблем, никаких вопросов согласованности, твой код не знает, что он на коробке побольше. Привлекательность «вширь» — нет врождённого потолка и переживает смерть машины: потерял один узел из пятидесяти — потерял 2% ёмкости, а не сервис.
Реальные системы используют оба: масштабируй БД вверх, пока один primary не станет неуютным, а stateless (без локального состояния) app-слой — вширь свободно. Senior-вопрос никогда не «вверх или вширь?» абстрактно — он «какой ресурс узкое место, есть ли у него потолок, в который я упрусь, и чем каждый путь обходится в режимах отказа?».
Два потолка масштабирования вверх
Вертикальное масштабирование отказывает по двум независимым причинам, и ты ловишь ту, что наступит раньше:
- Потолок железа. Есть самый большой инстанс. Когда ты на нём — всё, коробки больше нет, а срок переархитектуры теперь измеряется кварталами.
- Кривая цены. Топовое железо стоит сверхлинейно: самый большой инстанс часто дороже, чем 2× от вдвое меньшего, ведь ты платишь за редкий многоядерный кремний. Две средних машины часто бьют одну большую и по цене, и по устойчивости.
И под обоими: одна вертикально масштабированная машина — единая точка отказа. Перезагрузка, смерть диска, падение AZ — и у тебя полная авария, потому что нагрузке некуда уйти.
▸Почему это работает
Почему «вширь» «требует» statelessness? Потому что если запрос 1 попал на узел A и записал сессию в память A, запрос 2 от того же пользователя может попасть на узел B, который о нём не слышал. Решение, к которому тянется junior, — sticky sessions (прибить пользователя к узлу), но это возвращает единую точку отказа на каждого пользователя и ломает ребалансировку: умер узел A — все прибитые к нему теряют сессию. Прочное решение — вытолкнуть состояние из app-слоя совсем: в общую БД, распределённый кэш (Redis) или клиент (подписанный токен). Тогда любой узел обслужит любой запрос, а app-слой становится стадом одинаковых одноразовых «коров», которых добавляешь и убиваешь как угодно.
Координационный налог: закон универсальной масштабируемости
Опасный миф горизонтального масштабирования — что пропускная способность растёт линейно с узлами: удвой узлы, удвой ёмкость. Не растёт. Закон универсальной масштабируемости (USL) Нила Гюнтера моделирует реальную кривую двумя штрафами:
N
C(N) = ─────────────────────────
1 + α(N−1) + βN(N−1)
N = число узлов
α = contention (сериализованная работа — закон Амдала: общий лок, единый координатор)
β = coherency (межузловая координация — держать кэши/реплики согласованными)С одним α (contention) пропускная способность подходит к потолку — закон Амдала: 5% серийной доли упирают тебя в 20× сколько узлов ни добавь. Но β (coherency) хуже: он делает кривую ретроградной — после некоторого пика добавление узлов снижает общую пропускную способность, ведь каждый новый узел добавляет больше переговоров, чем работы. Поэтому кластер из 50 узлов может быть медленнее 30-узлового. Масштабирование вширь не бесплатно; ты покупаешь ёмкость координационным налогом, и налог в итоге может превысить ёмкость.
Куда на самом деле переезжает трудность
Масштабировать вширь stateless-слой (web/API-серверы) — почти решённая задача: за балансировщик, автоскейл по CPU, готово. Трудность не исчезает — она переезжает в stateful-слой. Твоя БД, очереди, кэши всё ещё держат состояние, которое должно оставаться корректным между узлами, и вот там живут репликация, шардинг и трейдоффы CAP (следующий юнит про распределение данных). Так что «просто масштабируем горизонтально» легко лишь для той части, что не держит состояния; как только масштабируешь вширь слой данных — ты подписался на распределённые проблемы.
▸Частая ошибка
Классическая и дорогая ошибка: масштабировать вширь сервис, что выглядит stateless, но нет. In-memory кэши, что каждый узел наполняет независимо; загрузки файлов на локальный диск узла; счётчики rate-limit на процесс; cron-задачи, что теперь бегут на каждом узле разом. Каждое работает на одной машине и тонко ломается на многих — дубли писем, несогласованные счётчики, файлы на одном узле. Прежде чем добавить второй узел, проведи аудит каждого куска состояния процесса и реши, где он на самом деле живёт.
Stateless API-слой масштабируется вширь прекрасно, но после ~40 узлов общая пропускная способность перестаёт расти и потом слегка падает. По закону универсальной масштабируемости, какой член доминирует?
Команда хочет масштабировать вширь API-сервер, что хранит сессии в локальной памяти процесса. Какое минимальное корректное изменение перед добавлением второго узла?
Чтобы масштабировать app-слой горизонтально, каждый узел должен быть _______ — не держать локально состояния запроса — чтобы балансировщик мог направить любой запрос на любой узел, а умирающий узел не терял ничего незаменимого.
- 01Назови два потолка вертикального масштабирования плюс его структурную слабость.
- 02Почему горизонтальное масштабирование требует statelessness и каков прочный фикс для stateful app-слоя?
- 03Что USL добавляет сверх закона Амдала?
Вертикальное масштабирование (машина побольше) стоит почти нулевых инженерных усилий, но упирается в потолок железа, сверхлинейную кривую цены и остаётся единой точкой отказа. Горизонтальное (машин побольше) не имеет врождённого потолка и переживает смерть узла — но лишь если работа stateless, то есть состояние сессий/кэша/файлов вынесено в общий стор или клиент (sticky sessions — пластырь, воссоздающий SPOF на пользователя). Вширь тоже не бесплатно: закон универсальной масштабируемости оценивает это через α (contention → потолок Амдала) и β (coherency → ретроградная кривая, где лишние узлы могут снизить пропускную способность). Stateless app-слой масштабируется вширь легко; настоящая трудность переезжает в stateful-слой — БД и кэши — что и разбирает следующий юнит про распределение данных. Теперь, когда увидишь команду, тянущуюся к инстансу побольше, спроси первым делом: этот ресурс stateless? Есть ли потолок, в который упрёмся через год? Эти два вопроса — и есть всё решение.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.