Экспресс-курс · No. 09

Масштабирование — это не «сделать всё быстрее», а найти ту одну часть, что прогибается под нагрузкой, и расширить её, потом следующую. Большинству систем этого нужно куда меньше, чем боятся их создатели; мастерство — знать, за чем тянуться и когда.

Только суть · Один образ на идею · Измеряй, а не гадай

§ 01

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

Масштабировать значит держать больше, не ломаясь

Маленькое кафе работает нормально, пока не подъезжает туристический автобус — и вдруг один бариста, одна касса, пара столиков не справляются. Масштабирование — это подготовка к автобусу.

Масштабировать — значит держать рост: больше пользователей, запросов, данных, — оставаясь быстрым и живым. Это не про пик изобретательности; это про то, чтобы система не прогнулась, когда нагрузка умножится. И почти любая задача масштабирования сводится к одному: что-то становится той частью, что не поспевает.

Вверх или вширь: вертикально vs горизонтально

Чтобы везти больше, можно купить грузовик побольше — но он лишь до определённого размера — или купить больше грузовиков: предела почти нет, но теперь нужен диспетчер.

Вертикальное масштабирование — это машина побольше: больше CPU, больше RAM. Просто, но есть жёсткий потолок, и остаётся единая точка отказа. Горизонтальное масштабирование — это много машин, работающих вместе: предела почти нет, но нужна координация (балансировка, общее состояние). Вертикальное покупает время; горизонтальное — там, где живёт настоящий масштаб.

Всё — это охота за узким местом

Шоссе быстро ровно настолько, насколько быстр его самый узкий участок, — расширять все прочие полосы бессмысленно, пока не починишь горловину.

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

Большинству систем этого почти не нужно

Ты не строишь шестиполосную автостраду к домику в лесу. Одна хорошая дорога увезёт всех, кто когда-либо приедет.

Один приличный сервер плюс база обслуживают поразительное число пользователей. Преждевременное масштабирование — шардинг, микросервисы, навороченное кэширование до того, как появилась нагрузка, — это стоимость и сложность, которые тебе не нужны. Сначала измерь; масштабируй то, что реально болит, когда оно реально болит.

Масштабирование — это не «сделать всё быстрее». Это найти узкое место, расширить его и повторить.

§ 02

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

Сначала вертикально: коробка побольше

Когда кухня захлёбывается, первый фикс — плита побольше и больше столешницы, а не второй ресторан.

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

Горизонтально: много коробок, одна работа

Одна касса становится десятью — теперь десять клиентов обслуживаются разом, а если одна касса заклинит, остальные девять продолжают.

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

Stateless-серверы: любая коробка обслужит любой запрос

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

Чтобы горизонтальное масштабирование работало, серверы приложения должны быть stateless: они не держат в своей памяти ничего о пользователе между запросами. Данные сессии, загрузки, прогресс — всё это живёт где-то в общем месте (база, кэш, токен на руках у клиента). Тогда любой запрос может попасть на любой сервер, и добавление или потеря коробки ничего не меняют. Безсостоятельность — это то, что делает «просто добавь серверов» реально рабочим.

Вертикальное покупает время; горизонтальное покупает масштаб. И горизонтальное работает, только если серверы сами по себе ничего не помнят.

§ 03

Когда серверов много, что-то должно встать перед ними и решать, кто обработает каждый запрос. Это балансировщик нагрузки — регулировщик масштабированной системы.

Балансировщик: одна входная дверь, много серверов

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

Балансировщик нагрузки (Nginx, HAProxy, облачный балансировщик) — это единая точка входа, которая распределяет входящие запросы по пулу серверов по простым правилам — round-robin или «шли наименее занятому». Клиенты говорят с ним, а не с отдельными серверами, так что машины за ним можно добавлять и убирать незаметно. Это то, что превращает кучу серверов в одну систему.

Health-чеки: не слать на мёртвых

Менеджер очереди замечает, что кассир отошёл, и просто перестаёт отправлять людей к тому окну — никто не ждёт у пустого стола.

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

Бесплатные бонусы: деплой без простоя и failover

Можно ремонтировать по одной кассовой линии за раз, пока остальные обслуживают, — магазин не закрывается.

Раз трафик течёт через балансировщик по взаимозаменяемым серверам, мощные вещи достаются почти даром: деплой без простоя (обновляешь серверы по нескольку за раз), failover, когда один умирает, и быстрый откат. Балансировщик плюс stateless-серверы — это становой хребет, что делает систему и масштабируемой, и устойчивой.

Балансировщик превращает много серверов в один адрес — а умирающую машину в несобытие.

§ 04

Самая быстрая работа — та, которую ты не делаешь. Прежде чем масштабировать машины, делающие работу, сократи, сколько её вообще есть, — кэшами и CDN.

Кэширование: не считай один и тот же ответ дважды

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

Кэш хранит результат дорогой работы — запрос к базе, посчитанную страницу, вызов API, — чтобы следующий запрос получил его мгновенно, а не переделывал. Быстрое хранилище в памяти (Redis, Memcached) перед базой может впитать основную массу чтений. Часто перегруженной базе нужен именно кэш, а не больше серверов.

CDN: обслуживай пользователей из ближнего

Глобальная сеть, которая держит одни и те же товары в локальных складах повсюду, — чтобы клиенты получали их из-за угла, а не везли через океан.

CDN держит копии твоего статического контента — картинки, скрипты, видео, всё чаще и целые страницы — в дата-центрах по всему миру, обслуживая каждого пользователя из ближайшего. Это режет задержку и снимает огромную нагрузку с твоего origin. Для всего глобального и статического CDN делает тяжёлую работу ещё до того, как твои серверы увидят запрос.

Самое сложное — инвалидация

Календарь на холодильнике быстрее, чем проверять телефон, — пока кто-то не поменяет план в телефоне, и теперь холодильник врёт.

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

Самый дешёвый запрос — тот, который ты никогда не обслуживаешь. Кэшируй горячее, отдавай статику с edge и потей над инвалидацией.

§ 05

Рано или поздно база становится узким местом — это та одна часть, которую правда тяжело масштабировать, потому что она держит правду, общую для всех.

База данных — обычно стена

Десять касс, тянущихся в одну-единственную кладовку, — кассиры масштабируются, но одна дверь кладовки становится затором.

Можно крутить сотню stateless-серверов приложения, но они обычно делят одну базу — и она становится пределом. В отличие от серверов приложения, её нельзя просто клонировать, потому что каждая копия должна сходиться в данных. Поэтому масштабирование базы — самая тяжёлая, самая аккуратная часть масштабирования системы — и та, которую стоит откладывать кэшированием как можно дольше.

Read-реплики: копии для чтения

Раздать фотокопии справочника, чтобы многие читали разом, пока единственный мастер-экземпляр — единственный, в котором кто-либо пишет.

Большинство приложений читают намного больше, чем пишут. Read-реплики — это копии базы, обслуживающие запросы на чтение, размазывающие эту нагрузку по машинам, пока записи всё ещё идут в один primary. Это первый и самый лёгкий шаг масштабирования базы — но копии чуть отстают от primary, так что только что записанное значение может не появиться на реплике мгновенно.

Шардинг: дели записи

Один переполненный гроссбух становится несколькими — A–M в одной книге, N–Z в другой, — чтобы два клерка писали разом, не дерясь за одну страницу.

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

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

§ 06

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

Унеси медленную работу с пути запроса

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

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

Очереди впитывают всплески

Водохранилище между разлившейся рекой и городом — паводок наполняет водохранилище, а не топит улицы, и стекает ровным темпом.

Очередь — это ещё и буфер. Когда трафик внезапно подскакивает — запуск, вирусный момент, — работа копится в очереди, а воркеры разгребают её в своём темпе, вместо того чтобы поток обрушил систему. Без этого буфера всплеск, что приходит быстрее, чем ты успеваешь обработать, кладёт всё. Очередь превращает смертельный всплеск в управляемый бэклог.

Развязывай, чтобы одна медленная часть не утопила остальные

Водонепроницаемые отсеки на корабле — если один затопит, переборки держат остальные сухими, а корабль на плаву.

Когда сервисы говорят через очереди и события, а не прямыми блокирующими вызовами, медленный или упавший компонент не замораживает всех, кто его ждёт. Заказ всё равно принимается, даже если сервис писем лежит, — он просто догонит позже. Развязка — это то, что позволяет большой системе деградировать изящно под стрессом, а не падать целиком разом.

Заставляй пользователя ждать только то, что нужно ему сейчас. Всё остальное — за очередь, которая ещё и спасает тебя, когда бьёт всплеск.

§ 07

Хорошо масштабировать — это в основном сдержанность и порядок: измерь настоящее узкое место, поднимайся по дешёвым ступеням первыми и готовься к отказам, которые приносит больше машин.

Измеряй, прежде чем масштабировать

Врач делает анализы, прежде чем оперировать, — резать не тот орган, потому что ты угадал, не поможет никому.

Прежде чем что-то добавлять, найди настоящее узкое место метриками и профилированием — это база, медленный запрос, сеть, CPU? Инженеры регулярно оптимизируют не то, потому что оно просто казалось медленным. Данные говорят, где система реально болит; масштабируй это и больше ничего. (Вот где наблюдаемость оправдывает себя.)

Поднимайся по дешёвой лестнице по порядку

Ты добавляешь утепление и герметизируешь окна, прежде чем покупать вторую печь, — дешёвые фиксы первыми.

Есть порядок, дешёвое и простое первым: коробка побольше, потом кэширование и CDN, потом stateless-серверы за балансировщиком с горизонтальным масштабированием, потом read-реплики, потом очереди для медленной работы — и лишь в самом конце шардинг. Каждая ступень — больше работы и больше сложности. Поднимайся ровно настолько, насколько вынуждает нагрузка.

Больше машин — больше отказов

Одна лампочка редко перегорает за вечер; на стадионе из десяти тысяч несколько всегда не горят. На масштабе что-то всегда сломано.

По мере добавления машин вероятность, что что-то отказывает в любой момент, стремится к достоверности. Поэтому масштабирование и надёжность — один проект: избыточность (никакой единой точки отказа), health-чеки и failover и изящная деградация, чтобы сломанная часть сбрасывала качество, а не убивала систему. Проектируй под отказ, потому что на масштабе он постоянен.

Прежде чем что-либо масштабировать
  • Измерил ли я, где настоящее узкое место, — а не угадал? - Есть ли ступень дешевле — коробка побольше, кэш, CDN — перед этой? - Stateless ли мои серверы приложения, чтобы я мог добавить ещё? - Нагрузка на базу — это чтения (реплики, кэш) или записи (тяжёлая дорога)? - Может ли медленная работа уйти за очередь? - Есть ли у меня вообще эта нагрузка — или я строю под будущее, которое может не прийти?
Признаки, что ты переусложнил
  • Шардинг базы, которую легко уместила бы одна коробка. - Микросервисы и очереди для приложения на сотню пользователей. - Масштабирование части, что не была узким местом, потому что оно казалось медленным. - Слой кэша настолько запутанный, что никто не уверен, когда он протух. - Постройка под миллионы пользователей, которых у тебя нет, и запуск, которого не было.
Признаки, что ты масштабировал хорошо
  • Ты масштабировал измеренное узкое место, и следующее теперь видно. - Серверы stateless; добавить или потерять один — несобытие. - Чтения закэшированы и реплицированы; база дышит. - Медленная работа идёт за очередями, и всплеск становится бэклогом, а не падением. - Нет единой точки отказа — любая одна машина может тихо умереть. - Ты поднялся ровно настолько по лестнице, насколько потребовала нагрузка.

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

Конец экспресс-курса · 7 глав · измеряй, а не гадай

Дальше — глубина: снова «Designing Data-Intensive Applications», книги «System Design Interview» и инженерные блоги компаний, что масштабировались по-настоящему. Но держи ключевую мысль выше паттернов — система сильна ровно настолько, насколько прочна её слабейшая точка под нагрузкой. Найди эту точку, расширь её и не строй остальное, пока не придётся.