«Надёжная», «масштабируемая», «поддерживаемая» — эти слова вешают на любую систему, обычно не вкладывая в них ничего конкретного. А ведь у каждого из трёх качеств есть точный смысл, и почти весь системный дизайн — это осознанный размен между ними: усилили одно — обычно чем-то заплатили в другом. Прежде чем проектировать систему по шагам, стоит разобраться, что эти три слова означают на самом деле. Эта статья — про сами понятия, простым языком.
Надёжность: сбой — это ещё не отказ
Первое разграничение, которое сразу наводит порядок в голове: сбой и отказ — это разные вещи.
- Сбой (fault) — когда один компонент отклонился от нормы: умер диск, зависла реплика, человек залил неверный конфиг.
- Отказ (failure) — когда система целиком перестала делать то, что нужно пользователю.
Построить систему, в которой не бывает сбоев, невозможно — детали ломаются всегда. Поэтому задача дизайна не в этом. Задача — не дать отдельному сбою превратиться в отказ всей системы. Диск умер, а реплика продолжает отдавать данные; нода зависла, а балансировщик убрал её из ротации, и пользователь ничего не заметил. Устойчивость строится не из идеальных деталей, а из обычных, поломки которых заранее предусмотрены.
Сбои приходят из трёх мест, и лечится каждый по-своему:
- Железо. Диски, память, питание, сеть. Такие сбои случайны и почти не связаны друг с другом: в большом кластере из 10 000 дисков в среднем один умирает каждый день — это норма, а не ЧП. Лечится избыточностью: RAID, реплики, разнесение по разным зонам (в методе это называют доменами отказов).
- Софт. Ошибка в коде: обработчик падает на определённом входе, процесс потихоньку съедает общий ресурс, один мелкий сбой каскадом валит соседние сервисы. В отличие от железа, такие сбои связанные — одна и та же ошибка срабатывает сразу на всех одинаковых узлах, поэтому резервный сервер не спасает. Лечится изоляцией процессов, тестами и самопроверками (когда система на ходу проверяет, что её данные не разъехались).
- Человек. Исследования крупных сервисов показывают неудобную вещь: чаще всего систему роняют не диски, а люди — операторы, которые что-то не так поменяли в конфигурации. На железо приходится лишь 10–25 % простоев. Лечится это тоже дизайном: делать интерфейсы, где правильное действие — самое простое; давать песочницу на реальных данных; уметь быстро откатываться; выкатывать изменения сначала на маленькую долю пользователей.
Есть контринтуитивный приём: раз сбои всё равно неизбежны — устраивать их нарочно. Например, случайно убивать процессы прямо в бою и смотреть, выживет ли система (так работает Chaos Monkey в Netflix). Это проверяет не столько систему, сколько вашу уверенность: механизмы устойчивости действительно работают, а не просто числятся на схеме.
Масштабируемость: сначала опиши нагрузку
Фраза «система X масштабируемая» сама по себе пустая. Осмысленный вопрос звучит иначе: «если нагрузка вырастет вот так — что мы будем делать?». А чтобы на него ответить, нагрузку надо сначала описать числами — параметрами нагрузки: сколько запросов в секунду на чтение и на запись, каково соотношение чтений к записям, сколько данных в «горячем» наборе, сколько одновременных пользователей. Какой из параметров главный — зависит от конкретной системы.
Классический пример — лента Twitter. Новых публикаций — 4 600 в секунду, а вот запросов «покажи мою ленту» — 300 000 в секунду. Устроить ленту можно двумя способами:
- Собирать при чтении. Твит — это просто одна вставка в общую таблицу. А лента строится запросом «найди всех, на кого я подписан, и собери их свежие твиты». Запись дешёвая, зато чтение дорогое.
- Собирать при записи. У каждого пользователя есть свой готовый «почтовый ящик» ленты. Когда кто-то публикует твит, его сразу раскладывают по ящикам всех подписчиков. Теперь чтение дешёвое (лента уже собрана), зато запись дорогая — один твит человека с миллионом подписчиков превращается в миллион вставок.
Twitter начинал с первого способа, не выдержал нагрузки на чтение и перешёл на второй: раз чтений в 65 раз больше, чем записей, выгоднее платить на записи. Но и это не финал: для знаменитостей с миллионами подписчиков раскладка по ящикам неподъёмна, поэтому их твиты подмешивают уже при чтении — получается гибрид двух подходов. Мораль: главный параметр нагрузки здесь — вовсе не «твитов в секунду», а то, как распределены подписчики по пользователям. Не описав нагрузку в числах, эту архитектуру нельзя ни выбрать, ни объяснить.
Отсюда общий принцип: волшебного «масштабирующего соуса» не бывает. Система под 100 000 мелких запросов в секунду и система под 3 больших запроса в минуту — это разные архитектуры, даже если в байтах они прокачивают одинаково. Хорошая архитектура всегда стоит на предположениях о том, какие операции будут частыми, — то есть на параметрах нагрузки.
Производительность: почему среднее время ответа врёт
Как измерить, «быстро» ли работает система? Для пакетной обработки — пропускной способностью (сколько записей в секунду обработали). Для онлайн-систем — временем ответа. И вот тут первая ловушка: время ответа — это не одно число, а разброс. Один и тот же запрос сегодня выполнится за 20 мс, а завтра за 800 — из-за переключения задач на сервере, потерянного пакета в сети или паузы сборщика мусора.
Считать среднее арифметическое — плохая идея: оно не говорит, скольким пользователям реально не повезло с медленным ответом. Смотреть надо на перцентили — они отвечают на вопрос «а сколько запросов уложилось в такое-то время»:
- Медиана (p50) — половина запросов быстрее этого времени. Это «типичный» запрос.
- p95, p99, p999 — это «хвост»: время, которое видят самые невезучие 5 %, 1 % и 0,1 % запросов.
Хвост важнее, чем кажется, по двум причинам. Во-первых, самые медленные ответы часто достаются самым «тяжёлым» — а значит, самым ценным — пользователям: у них больше всего данных. Amazon формулирует требования к своим сервисам через p999 и подсчитал, что лишние 100 мс задержки снижают продажи примерно на 1 %. Во-вторых, работает усиление хвоста: если одна страница собирается из обращений к 7 разным сервисам, и у каждого свой p99, то вероятность зацепить хотя бы один медленный ответ уже сильно больше, чем 1 %. Чем больше параллельных вызовов, тем чаще один медленный сервис тормозит целую страницу.
Ещё один источник хвоста — блокировка головы очереди: серверу хватает пары медленных запросов, чтобы за ними в очереди застряли быстрые. Поэтому время ответа честнее мерить на стороне клиента, а не сервера — иначе не видно, сколько запрос простоял в очереди.
На перцентилях строят и договорённости об уровне сервиса: SLO (внутренняя цель) и SLA (соглашение с клиентом, у которого за нарушение бывают санкции) формулируют в духе «медиана меньше 200 мс, p99 меньше 1 секунды, доступность 99,9 %». Как перцентили считаются технически (гистограммы, агрегация) — в статье про метрики.
Когда нагрузка описана числами и метрика выбрана, варианты ответа на рост известны: вертикальное масштабирование (взять машину помощнее) против горизонтального (взять много машин поменьше). На практике это прагматичная смесь: сервисы без состояния (stateless) размножаются легко, а базу данных держат на одном узле до последнего — пока стоимость более мощного железа или требования к доступности не заставят её распределять. Подробнее — в строительных блоках.
Сопровождаемость: код читают дольше, чем пишут
Главные деньги софт съедает не на этапе написания, а потом — на сопровождении: правки, эксплуатация, подгонка под новые требования. Три вещи делают систему пригодной для долгой жизни:
- Удобство эксплуатации. Хорошая система делает рутину дежурных простой: понятный мониторинг, ясная связь «сделал X → произойдёт Y», автоматизация, независимость от конкретной машины, разумные настройки по умолчанию с возможностью вмешаться руками.
- Простота. Главный враг — лишняя сложность, то есть та, что порождена не самой задачей, а способом её решения. Признаки: слишком много состояний, модули цепляются друг за друга, повсюду костыли и особые случаи. Главное оружие против неё — абстракция: SQL прячет от вас, как данные лежат на диске и как разруливается одновременный доступ; язык высокого уровня прячет машинный код. Хорошая абстракция убирает сложность за понятный фасад и переиспользуется много раз.
- Возможность развития. Требования обязательно поменяются — вопрос лишь в том, насколько дорого системе даётся каждое изменение. Простые и хорошо абстрагированные системы менять легко, запутанные — мучительно.
Где это применяется
Эти три слова — скелет любого разговора об архитектуре. Обсуждение дизайна, в котором не прозвучали параметры нагрузки, перцентили и поведение при сбоях, — это разговор про квадратики на схеме, а не про систему. Метод системного дизайна начинается ровно с этого: сначала числа и обязательства, потом схема.
Где спотыкаются начинающие:
- Называют систему «масштабируемой» без параметров нагрузки. Масштабируемость — не свойство-галочка, а ответ на вопрос «что мы сделаем, когда вырастет вот этот показатель?».
- Смотрят на среднее время ответа. Среднее прячет хвост, а хвост — это ваши самые активные пользователи. Начинайте с медианы и p99.
- Путают сбой и отказ — и пытаются построить систему, где «ничего не ломается», вместо системы, которая спокойно переживает поломки.
- Борются со всей сложностью сразу. Сложность самой задачи никуда не денется; убирать надо только лишнюю — ту, что добавил способ реализации.
Что почитать дальше: метод системного дизайна — как эти понятия превращаются в последовательность шагов; строительные блоки — чем отвечать на выросшую нагрузку; метрики — как считать перцентили в проде.