Системный дизайн: как подходить к задаче

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

Листайте стрелками ← →, свайпом или кнопками внизу. В схемах нажимайте кнопки и двигайте ползунки.

45 минут: что оценивают и как их потратить

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

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

Требования определяют архитектуру

  • Функциональные — что система делает: создать короткую ссылку, перейти по ней, посмотреть статистику. Возьмите 3–4 главных сценария, остальное явно вынесите за рамки.
  • Нефункциональные — какой она должна быть: масштаб, задержка, доступность, согласованность, долговечность. Именно они превращают «сервис и база» в настоящую архитектуру.

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

Проверьте себя

Задача: «Спроектируйте ленту новостей, как в Twitter». У вас есть время на один уточняющий вопрос. Какой сильнее всего изменит архитектуру?

Число пользователей меняет количество серверов, но не устройство схемы. А максимум подписчиков решает, как строить ленту. Если у всех до тысячи подписчиков, пост при публикации раскладывают по лентам подписчиков (fan-out on write), и чтение ленты — один запрос. Автору с 50 млн подписчиков пришлось бы сделать 50 млн записей на каждый пост, поэтому для «звёзд» ленту собирают при чтении. На практике — гибрид.

Числа для оценок в уме

Время и объёмы
сутки86 400 с ≈ 10⁵ с
год≈ 3·10⁷ с
1 млн событий в сутки≈ 12 в секунду
1 млрд в сутки≈ 12 тыс. в секунду
2¹⁰ / 2²⁰ / 2³⁰ / 2⁴⁰≈ тысяча / миллион / миллиард / триллион — КБ, МБ, ГБ, ТБ
UUID, timestamp, int6416 / 8 / 8 байт
строка в таблице100 Б – 1 КБ с индексами
пик к среднемуобычно ×2–×3, распродажи и события — ×10
Что держит один узел (порядок величины)
сервис на Go/Java, 16 ядер10–50 тыс. rps на простом эндпоинте, 1–5 тыс. с логикой и походами в базу
PostgreSQLчтения по индексу — десятки тыс./с, запись — 5–20 тыс. транзакций/с, комфортно до единиц ТБ
Redis~100 тыс. операций/с на ядро, ответ < 1 мс, объём — сколько RAM
Kafkaброкер — сотни МБ/с; партиция — единицы–десятки МБ/с
сервер32–128 ядер, 256 ГБ – 1 ТБ RAM, сеть 10–25 Гбит/с (1–3 ГБ/с)
NVMe SSD / HDDсотни тысяч IOPS и 3–7 ГБ/с / 100–200 IOPS

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

Оценка на салфетке

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

  • Запись: DAU × действия ÷ 10⁵ с, пик — ×2–3.
  • Чтения: запись × соотношение чтение/запись.
  • Хранение: записи в сутки × размер × 365 × годы × 3 копии.

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

Задержки, которые нужно помнить

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

Как устроена иерархия памяти внутри процессора — в презентации про процессор. Цифры типичны для серверов в 2020-х.

обращение к RAM100 нс
сжать 1 КБ (Snappy/LZ4)2 мкс
прочитать 1 МБ из RAM50 мкс
случайное чтение NVMe20–100 мкс
круг по сети в зоне0,1–0,5 мс
GET из Redis с сетью0,2–1 мс
запрос в Postgres по индексу0,5–2 мс
между зонами одного региона1–2 мс
поиск на HDD5–10 мс
Москва ↔ Франкфурт~40 мс
Европа ↔ США (восток)~80 мс
новое TLS-соединение через океан~250 мс

Полоса — логарифмическая шкала: каждый шаг вправо — примерно ×10.

Доступность: сколько стоит каждая девятка

Доступность — доля времени (или запросов), когда система отвечает правильно. Каждая девятка уменьшает допустимый простой в 10 раз — и обычно в разы увеличивает стоимость.

Двигайте ползунок. Обратите внимание, где кончаются ручные действия: при четырёх девятках дежурный не успевает даже открыть ноутбук.

Зависимости умножают доступность

  • Последовательно — запрос нужен каждому компоненту: доступности перемножаются. Пять компонентов по 99,9% дают 99,5%.
  • Параллельно — хватит любой из n копий: недоступность перемножается, 1 − (1 − a)ⁿ. Две копии по 99% дают 99,99%.
  • Мягкая зависимость — без неё система деградирует, но отвечает: кэш упал — читаем из базы, рекомендации не пришли — показываем популярное.

Добавляйте копии и делайте зависимости мягкими. Формула верна только для независимых отказов: копии в одной зоне, с одним конфигом и одним релизом падают вместе.

SLI, SLO, SLA и бюджет ошибок

  • SLI — что измеряем: доля успешных запросов, доля ответов быстрее 300 мс.
  • SLO — внутренняя цель: 99,9% успешных за 30 дней.
  • SLA — обещание клиенту с компенсацией, всегда слабее SLO: например 99,5%.
  • Бюджет ошибок = 1 − SLO. Пока он есть, можно рисковать и выкатывать. Кончился — заморозка фич, чиним надёжность.

Сервис получает 1 000 запросов в секунду. Выберите SLO и устраивайте инциденты.

Перцентили, а не среднее

p99 = 120 мс значит: 99% запросов быстрее 120 мс, а каждый сотый — медленнее. Среднее прячет хвост: редкие паузы GC почти не двигают его, но именно их видит пользователь.

Медленные запросы часто достаются самым ценным клиентам: у них больше данных. Добавьте паузы GC и промахи кэша и посмотрите, что сдвинется.

Хвост растёт при веерных запросах

Если страница ждёт N параллельных вызовов, она медленная, когда медленный хоть один: P = 1 − (1 − p)ⁿ. Редкий для одного сервиса хвост становится нормой для пользователя.

Лечение: меньше вызовов на запрос, таймауты с частичным ответом, хеджированные запросы — копия запроса на другую реплику, если первая не ответила за p95 (подробнее — в «The Tail at Scale», Dean и Barroso). Нажимайте «Запрос» или прогоните тысячу.

Проверьте себя

Страница собирается из 100 параллельных вызовов к разным сервисам. У каждого p99 = 50 мс, вызовы независимы. Какая доля открытий страницы займёт больше 50 мс?

1 − 0,99¹⁰⁰ ≈ 0,634. Каждый вызов быстрый с вероятностью 0,99, и все сто должны быть быстрыми. Поэтому для сервиса, в который ходят веером, целятся в p99,9 и p99,99, а не в p99.

Закон Литтла

Среднее число запросов в системе = пропускная способность × время ответа: L = λ·W. Верно для любой устойчивой системы — пула соединений, воркеров, очереди.

  • 1 000 rps при 200 мс — в каждый момент обрабатывается 200 запросов: нужно 200 потоков или соединений.
  • Пул ограничивает пропускную способность: 20 соединений к базе при запросе 5 мс — не больше 4 000 запросов/с.

Двигайте ползунки. Попробуйте замедлить запрос к базе с 5 до 50 мс — так выглядит инцидент «база стала медленнее, и упал весь сервис».

Очереди: почему 90% загрузки — это много

Запросы приходят неравномерно. Даже если сервер в среднем успевает, в моменты скопления образуется очередь. Для простейшей модели (M/M/1) среднее время ответа T = S / (1 − ρ), где S — время обработки, ρ — загрузка.

50% загрузки — ответ вдвое дольше обработки, 90% — в 10 раз, 99% — в 100. Поэтому мощности планируют с запасом и держат загрузку на 50–70%. Двигайте ползунок загрузки и смотрите на очередь.

Проверьте себя

Сервис загружен на 80%. Обработка запроса — 10 мс, среднее время ответа с очередью — 50 мс. Трафик вырос на 10%, загрузка стала 88%. Каким станет среднее время ответа?

T = S / (1 − ρ): при 80% это 10 / 0,2 = 50 мс, при 88% — 10 / 0,12 ≈ 83 мс. Плюс 10% трафика дали плюс 66% к задержке. Чем ближе к 100%, тем круче кривая: ещё 10% — и будет уже 10 / 0,03 ≈ 330 мс.

От простой схемы к углублению

  • Сначала самая простая схема, которая выполняет функциональные требования. Пройдите по ней путь записи и путь чтения.
  • Потом каждый компонент добавляйте ради конкретного требования или числа из оценки — и говорите это вслух.
  • Углубляйтесь в 1–2 самых сложных места. В конце — где сломается при ×10 и какие компромиссы приняты.

Хорошая схема: потоки данных подписаны стрелками, запись и чтение различимы, у хранилищ указан ключ. Нажимайте «Дальше».

API: коротко, но с главным

  • REST/JSON — для внешних клиентов и браузера. gRPC — между сервисами: схема, бинарный формат, стриминг.
  • Пагинация курсором: ?after=<id>&limit=50. OFFSET 100000 заставляет базу прочитать и выбросить 100 тысяч строк, а при вставках страницы съезжают.
  • Ключ идемпотентности для всего, что меняет деньги и состояние: клиент генерирует ключ, сервер запоминает результат и на повтор отвечает тем же.

Сеть может потерять ответ, и клиент не знает, выполнилась ли операция. Повторять обязательно — значит повтор должен быть безопасным. Включайте и выключайте ключ и отправляйте платёж.

Сначала модель данных

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

  • Чат: «последние 50 сообщений чата» → ключ (chat_id, id), шард по chat_id: весь чат на одном узле.
  • Связи многие-ко-многим, транзакции, отчёты — реляционная база. Огромная запись по одному ключу — колоночные KV вроде Cassandra.

Про выбор хранилища, шардирование и репликацию — в третьей части серии.


-- шаблон доступа: последние сообщения чата
create table messages (
  chat_id    bigint      not null,
  id         bigint      not null, -- растёт со временем (Snowflake)
  author_id  bigint      not null,
  body       text        not null,
  created_at timestamptz not null default now(),
  primary key (chat_id, id)
);

-- страница истории — курсором по ключу
select id, author_id, body from messages
 where chat_id = $1 and id < $2
 order by id desc
 limit 50;

Что замечает интервьюер

Сильные сигналы

  • Уточнил требования и записал их, прежде чем рисовать.
  • Числа из оценки повлияли на решения: «12 тыс. записей/с — одна база не справится».
  • Каждый компонент обоснован, у каждого решения назван компромисс.
  • Сам нашёл единые точки отказа и узкие места.

Красные флаги

  • Сразу рисует кубики или называет технологии без причин: «возьмём Kafka».
  • Шардирование и микросервисы для 100 запросов в секунду.
  • «Будет быстро, строго согласованно и всегда доступно» — без компромиссов.
  • Молчит, не следит за временем, спорит с подсказками интервьюера.

Как это применять на практике

  • На интервью: выпишите в углу доски требования и числа — и возвращайтесь к ним при каждом решении.
  • Ёмкость узла измеряют, а не берут из таблиц: нагрузочный тест (k6, wrk, vegeta) до точки, где p99 начинает расти.
  • Латентность — гистограммы, а не средние. Перцентили нельзя усреднять между серверами: считают по объединённой гистограмме.
  • Алерты — по скорости сжигания бюджета ошибок, а не по каждой ошибке.
  • Загрузку держат с запасом: следите за насыщением — очередями, ожиданием пула, run queue.

# p99 по всем экземплярам сервиса (Prometheus)
histogram_quantile(0.99,
  sum by (le) (rate(http_request_duration_seconds_bucket[5m])))

# доля ошибок за час против бюджета SLO 99,9%
sum(rate(http_requests_total{code=~"5.."}[1h]))
  / sum(rate(http_requests_total[1h])) / 0.001

# нагрузочный тест: 2000 rps, смотрим p99
$ vegeta attack -rate=2000 -duration=60s < targets | vegeta report

Второе выражение — скорость сжигания: 1 значит, что бюджета хватит ровно на 30 дней, 14,4 за час — что месячный бюджет кончится за двое суток. На такое будят дежурного.

Главное

  • Интервью проверяет ход мысли: требования → оценки → API и данные → схема → углубление → компромиссы.
  • Нефункциональные требования и числа решают, какая нужна архитектура. Считайте порядками: сутки ≈ 10⁵ секунд.
  • Доступности последовательных зависимостей перемножаются. Копии и мягкие зависимости — способ получить больше девяток, чем у любой части.
  • Смотрите на p99 и выше: при веерных запросах хвост одного сервиса становится нормой для пользователя.
  • L = λ·W ограничивает пулы, а очередь взрывается у 100% загрузки — держите запас.

Каждое решение — компромисс. Назовите его вслух.