Прежде чем проектировать систему по шагам, полезно договориться о словах. «Надёжная», «масштабируемая», «поддерживаемая» — эти ярлыки вешают на что угодно, обычно без смысла. У каждого из трёх качеств есть точное содержание, и весь системный дизайн — это осознанный размен между ними. Эта статья — про сами понятия; классическая опора здесь — первая глава книги Мартина Клеппмана «Высоконагруженные приложения».
Надёжность: сбой — это не отказ
Первое разграничение, которое сразу наводит порядок в голове: сбой (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.
- Путают сбой и отказ — и пытаются построить систему, в которой «ничего не ломается», вместо системы, которая переживает поломки.
- Борются со всей сложностью подряд. Сложность самой задачи никуда не денется; убирать нужно побочную — ту, что добавила реализация.
Что почитать дальше: метод системного дизайна — как эти понятия превращаются в последовательность шагов; строительные блоки — чем отвечать на выросшую нагрузку; метрики — как считать перцентили в проде. Первоисточник — Мартин Клеппман, «Высоконагруженные приложения», глава 1.