Кладовочка

О разном, чтобы было под рукой онлайн

System Design: как проектировать масштабируемые системы — полный гайд

System Design — это дисциплина проектирования распределённых систем, которая отвечает на вопрос «как построить приложение, способное выдержать нагрузку в тысячи или миллионы пользователей». Это не про UML и не про рисование красивых диаграмм — это про принятие обоснованных компромиссов между скоростью, надёжностью, стоимостью и сложностью.

Главный принцип System Design: каждое решение — это trade-off. Не бывает «правильной» архитектуры — бывает архитектура, подходящая под ваши требования и ограничения. Задача инженера — сделать эти компромиссы явными, а не навязывать решения, вычитанные из блогов.


О чём эта статья

Эта статья — практический гайд по проектированию систем для разработчиков и архитекторов уровня Middle и выше. Мы разберём:

  • Что такое System Design простыми словами и чем он отличается от обычного проектирования
  • Ключевые концепции: масштабируемость, надёжность, доступность, согласованность — с примерами из жизни
  • Пошаговый фреймворк из 4 шагов, который работает и на собеседовании, и в реальном проекте
  • Базовые компоненты: балансировщик нагрузки, кэш, очередь сообщений, репликация и шардирование
  • Алгоритмы и паттерны: согласованность по CAP, идемпотентность, eventual consistency
  • Типичные ошибки и как их избежать
  • Практические примеры проектирования конкретных систем (Twitter, Uber, Netflix)

Для кого статья: backend-разработчики, которые хотят перейти от «написать код» к «спроектировать систему»; архитекторы, готовящиеся к System Design Interview; тимлиды, которые принимают решения об инфраструктуре.

Источники: материал основан на классических работах — «Designing Data-Intensive Applications» Мартина Клеппмана, курсе «Grokking the System Design Interview», ByteByteGo (Alex Xu), «System Design Interview» Алекс Сюй, плюс опыт инженеров FAANG из открытых лекций и блогов.

Аналитика спроса: «проектирование систем» — 47 508 показов/мес в Яндекс, «system design» — 9 324, «масштабируемые системы» — 450, «проектирование высоконагруженных систем» — 126.


Что такое System Design и зачем он нужен

System Design — это способ мышления, при котором вы разбиваете сложную систему на компоненты и осознанно выбираете между альтернативами. Это не «как нарисовать архитектуру», а «как принять решения, которые не убьют проект через год».

Когда нужен System Design:

Ситуация Что проектируем Типичные вопросы
Стартап с 0 до 1000 пользователей Монолит + одна БД Нужно ли уже шардирование? (Нет)
Продукт вырос до 100K пользователей Разделение read/write Кэш обязателен? (Да)
Глобальный сервис с 10M+ пользователей Multi-region, CDN Eventual consistency допустима?
Собеседование в FAANG Любая система Как объяснить выбор за 45 минут?

Когда System Design НЕ нужен:

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

Главное заблуждение: «System Design — это про высокие нагрузки и большие системы». На самом деле это про осознанные компромиссы. Даже для системы на 100 пользователей можно применить System Design: какой кэш выбрать, синхронно или асинхронно писать в БД, нужна ли очередь. Просто ставки ниже.

Ключевые концепции: что нужно знать перед стартом

Прежде чем проектировать, разберёмся с базовыми терминами. Каждый из них — отдельная большая тема, но в System Design достаточно понимать суть и trade-offs.

Масштабируемость (Scalability)

Способность системы выдерживать рост нагрузки без деградации. Бывает двух типов:

Тип Что значит Когда применять
Вертикальное масштабирование (scale up) Увеличиваем ресурсы одного сервера: больше CPU, RAM, диск Простота, нет изменений в коде. Упираемся в потолок
Горизонтальное масштабирование (scale out) Добавляем новые серверы в кластер Линейный рост, нет потолка. Сложнее в коде

Реальный пример: Twitter хранит 400 млн твитов в день. Вертикальное масштабирование упёрлось бы в потолок на уровне 10–20 самых мощных серверов. Горизонтальное позволяет держать тысячи обычных машин.

graph LR A[1 сервер] -->|Добавляем CPU/RAM| B[Тот же сервер, но мощнее] C[1 сервер] -->|Добавляем ещё сервер| D[Кластер из N серверов]

Надёжность (Reliability)

Способность системы продолжать работу при отказе компонентов. Измеряется в девятках доступности:

Доступность Простой в год Для каких систем
99% («две девятки») 3.65 дня Внутренние инструменты
99.9% («три девятки») 8.76 часов Большинство SaaS
99.99% («четыре девятки») 52.6 минуты Банкинг, платежи
99.999% («пять девяток») 5.26 минут Телеком, авиакомпании

Что важно: каждая дополнительная «девятка» увеличивает сложность системы на порядок. Прежде чем гоняться за 99.999%, посчитайте, во что обойдётся час простоя vs. инфраструктура для его предотвращения.

Доступность (Availability)

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

Дублирование — основной способ повышения доступности. Один сервер = одна точка отказа. Два сервера в режиме active-passive — уже лучше, но failover занимает время. Три сервера в режиме active-active — устойчиво к одновременному падению двух.

Согласованность (Consistency)

Гарантия того, что все клиенты увидят одинаковые данные. Бывает:

  • Strong consistency: после записи все последующие чтения вернут новое значение. Дорого в распределённых системах.
  • Eventual consistency: со временем все реплики придут к одному значению. Дешевле, но возможны «устаревшие» чтения.
  • Causal consistency: причинно-следственные связи сохраняются, но порядок несвязанных операций может меняться.

Пример: вы обновили аватар в VK. Друг видит старую версию ещё 5 секунд — это eventual consistency. Если бы система была strongly consistent, каждое обновление аватара требовало бы синхронной записи на все серверы по всему миру.

CAP-теорема: невозможный треугольник

В распределённой системе во время сетевого сбоя можно гарантировать только два из трёх:

graph TD C[C - Consistency
Согласованность] --> T{Во время сбоя} A[A - Availability
Доступность] --> T P[P - Partition tolerance
Устойчивость к разделению] --> T T --> CAP{Выбор: 2 из 3} CAP --> AP[AP: доступность + устойчивость
Пример: Cassandra, DynamoDB] CAP --> CP[CP: согласованность + устойчивость
Пример: MongoDB, HBase] CAP --> CA[CA: согласованность + доступность
Только без разделений сети]

Уточнение: CAP говорит о поведении во время сетевых разделений. В нормальной жизни можно приближаться к CA, но при partition вы обязаны жертвовать либо C, либо A.

Пошаговый фреймворк System Design

Не существует единого «правильного» фреймворка, но структура из 4 шагов работает и на собеседованиях, и в реальных проектах.

Шаг 1. Требования и ограничения

Прежде чем рисовать диаграммы — определите, что вы вообще строите.

Функциональные требования: что система должна делать. Для Twitter: публикация твитов, лента подписок, поиск.

Нефункциональные требования:

Параметр Вопрос Типичный ответ
Масштаб Сколько пользователей? 100K DAU, 1M MAU
Нагрузка Сколько запросов/сек? 10K reads, 100 writes
Задержка Допустимый p95? <200ms для ленты
Доступность Сколько девяток? 99.9%
Согласованность Strong или eventual? Eventual для ленты
Объём данных Сколько хранить? 100GB, рост 10GB/мес

Back-of-the-envelope расчёты:

Пример для Twitter:

  • 400M твитов/день = 4 600 твитов/сек в среднем, пики до ×10 = 46 000 твитов/сек
  • 100M пользователей × 5 лет × 365 дней × 100 твитов/год = 50 млрд твитов
  • Средний твит 300 байт × 50 млрд = 15 PB за 5 лет

Эти цифры определяют архитектуру: 15 PB не влезет в один сервер, нужно шардирование и S3 для медиа.

Шаг 2. Высокоуровневая архитектура

Нарисуйте «эскиз» системы: какие основные компоненты и как они взаимодействуют.

graph TB U[Пользователь] -->|HTTPS| CDN CDN --> LB[Load Balancer] LB --> API1[API Server 1] LB --> API2[API Server 2] LB --> API3[API Server N] API1 --> Cache[(Cache: Redis)] API2 --> Cache API3 --> Cache API1 --> MQ[Message Queue: Kafka] API2 --> MQ API3 --> MQ MQ --> Worker1[Worker 1] MQ --> Worker2[Worker N] Worker1 --> DB[(Database: PostgreSQL)] Worker2 --> DB DB --> ReadReplica[(Read Replica)] API1 -.->|Read| ReadReplica API2 -.->|Read| ReadReplica

Что здесь важно:

  1. CDN отдаёт статику (картинки, JS, CSS) — снижает нагрузку на бэкенд на 70–90%.
  2. Load Balancer распределяет запросы между серверами API.
  3. Stateless API-серверы — горизонтально масштабируются просто добавлением новых нод.
  4. Кэш перед БД ускоряет частые запросы в 10–100 раз.
  5. Очередь сообщений для асинхронных задач (отправка email, индексация, аналитика).
  6. Read Replica для распределения нагрузки между чтением и записью.

Шаг 3. Детальный дизайн ключевых компонентов

Здесь выбираем технологии и обосновываем решения для каждого узкого места.

Какие вопросы задавать:

  • Где будут узкие места? (Обычно — БД и внешние API)
  • Какие данные критичны для согласованности? (Деньги — strong, лайки — eventual)
  • Где можно кэшировать? (Везде, где данные читаются чаще, чем пишутся)
  • Где нужна асинхронность? (Где запрос не блокирует UI)

Шаг 4. Масштабирование и компромиссы

На этом шаге выявляем, что сломается при росте нагрузки в 10× или 100×, и как это предотвратить.

Компонент Узкое место Решение
БД Запись на один мастер Шардирование по user_id
Кэш Холодный кеш, перезагрузка Cache aside + TTL
Очередь Backlog растёт Партиционирование, приоритеты
API N+1 запросов к БД Денормализация, графовая загрузка
Хранилище Файлы не влезают S3 + CDN

Базовые компоненты распределённых систем

Разберём каждый ключевой компонент подробно — что это, когда применять, какие подводные камни.

Балансировщик нагрузки (Load Balancer)

Распределяет входящие запросы между несколькими серверами. Существует в двух формах:

L4 (Transport Layer) — работает на уровне TCP/UDP. Быстро, но не понимает содержимое запроса. Используют: HAProxy, Nginx в режиме stream, AWS NLB.

L7 (Application Layer) — работает на уровне HTTP. Может маршрутизировать по URL, заголовкам, cookies. Используют: Nginx, Envoy, AWS ALB.

Алгоритмы распределения:

Алгоритм Как работает Когда подходит
Round Robin По очереди Одинаковые серверы
Least Connections К наименее нагруженному Долгие соединения
IP Hash По хешу IP Sticky sessions
Weighted Round Robin Пропорционально мощности Разнородные серверы

Подробный разбор — в статье «Балансировщик нагрузки: как работает и зачем нужен».

Кэширование

Самый эффективный способ ускорить систему. Идея: положить часто запрашиваемые данные в быстрое хранилище (Redis, Memcached).

Паттерны кэширования:

flowchart LR A[Запрос] --> B{Кэш hit?} B -->|Да| C[Ответ из кэша] B -->|Нет| D[Чтение из БД] D --> E[Запись в кэш] E --> F[Ответ клиенту]
Паттерн Что делает Trade-off
Cache Aside (Lazy) Код сам проверяет кэш и обновляет при miss Простой, но возможна stale data
Write Through Запись и в кэш, и в БД одновременно Согласованность, но медленнее запись
Write Behind Запись в кэш, БД обновляется асинхронно Быстрая запись, риск потери данных
Read Through Кэш сам загружает из БД при miss Прозрачно для приложения

Подробнее — в статье «Кэширование: паттерны, стратегии инвалидации и подводные камни».

Репликация баз данных

Один сервер БД — это всегда узкое место. Репликация — это создание копий на других серверах.

Типы репликации:

Тип Как работает Плюсы Минусы
Master-Slave 1 мастер пишет, слейвы читают Просто, читаем масштабируем Запись всё равно на одном сервере
Master-Master Несколько мастеров пишут Нет единой точки отказа Конфликты при одновременной записи
Multi-Region Реплики в разных регионах Низкая задержка для пользователей Сложная согласованность

Master-Slave — самый частый выбор. Запись идёт на мастер, чтение — с реплик. Но возникает проблема replication lag: после записи на мастер может пройти 100–500 мс, прежде чем реплика увидит изменение. Если пользователь сразу читает — может получить старое значение.

Решения:

  • Read your writes — пользователь, который только что записал, читает с мастера
  • Monotonic reads — пользователь всегда видит данные не старее, чем видел раньше
  • Consistent prefix reads — если событие A было до B, пользователь видит эту последовательность

Шардирование

Когда одна БД не справляется даже с чтением, данные разбивают на шарды (shards) — независимые подмножества на разных серверах.

Стратегии шардирования:

Стратегия Пример Когда подходит
По диапазону user_id 1–1M → шард 1, 1M–2M → шард 2 Простая, но неравномерная нагрузка
По хешу hash(user_id) % N → шард N Равномерное распределение
По географии европейские пользователи → шард EU GDPR, латентность
По типу данных профили → одна БД, посты → другая Микросервисный подход

Главная проблема шардирования — cross-shard запросы: «показать всех друзей пользователя, у которых есть общие группы». В одной БД это JOIN, в шардированной — scatter-gather запрос по всем шардам. Дорого и медленно.

Решения:

  • Денормализация: дублировать данные, которые часто нужны вместе
  • Глобальные таблицы: маленькие справочники (страны, категории) на каждом шарде
  • CQRS: разделить запросы и записи, чтобы заточить каждую часть под свои нужды

Очереди сообщений

Позволяют асинхронно обрабатывать задачи. Вместо «отправить email и ждать 5 секунд» — «положить в очередь, отдать клиенту ответ, отправить email в фоне».

Когда нужны:

  • Долгая обработка (видео-транскодирование, генерация отчётов)
  • Сглаживание пиков (всплеск заказов в Чёрную пятницу)
  • Декуплинг компонентов (сервис A не ждёт ответа сервиса B)
  • Гарантия доставки (не потерять событие при падении)

Сравнение Kafka vs RabbitMQ:

Параметр Kafka RabbitMQ
Модель Распределённый лог Очередь с подтверждениями
Пропускная способность Очень высокая (млн сообщений/сек) Средняя (сотни тысяч)
Порядок сообщений В рамках партиции В рамках очереди
Retention Дни/недели по умолчанию До подтверждения
Когда выбирать Event sourcing, стриминг, аналитика Классические задачи очередей

Пример 1: проектирование Twitter-подобной системы

Задача: спроектировать сервис микроблогов (твиты, лента подписок, поиск).

Шаг 1. Требования

  • 100M DAU, 1B твитов/день
  • Чтение ленты — 200K запросов/сек, запись — 5K твитов/сек
  • Лента должна грузиться за <300ms (p95)
  • Eventual consistency для ленты допустима (читатель увидит твит через 1–2 сек)

Шаг 2. Высокоуровневая архитектура

graph TB User[Пользователь] -->|Tweet, чтение ленты| LB[Load Balancer] LB --> API[Stateless API servers] API -->|Чтение| Cache[(Redis: ленты)] API -->|Запись| MQ[Kafka] MQ --> Fanout[Fanout Service
Раздача по подписчикам] Fanout --> TimelineDB[(Timeline DB: Cassandra)] Fanout --> MQ2[Kafka] MQ2 --> Index[Search Service
Elasticsearch] TimelineDB --> Cache

Шаг 3. Ключевые решения

Fan-out на запись vs Fan-out на чтение:

Подход Как работает Trade-off
Fan-out на запись (Push) При публикации твита он копируется в ленты всех подписчиков Быстрое чтение, медленная запись, много места
Fan-out на чтение (Pull) При чтении ленты собираем твиты всех подписок Быстрая запись, медленное чтение, актуальные данные
Гибрид (как Twitter) Знаменитостям — pull, остальным — push Баланс

Хранение твитов:

  • Свежие твиты (последние 7 дней) — в Cassandra, быстрый доступ по user_id и tweet_id
  • Старые твиты — в S3 / HDFS, для архива
  • Медиа (картинки, видео) — в S3 с CDN на CloudFront или аналоге

Генерация ID:

Twitter использует Snowflake — 64-битный ID, в котором закодированы:

  • Timestamp (41 бит) — миллисекунды с эпохи
  • Datacenter ID (5 бит)
  • Worker ID (5 бит)
  • Sequence (12 бит) — номер в пределах миллисекунды

Это даёт упорядоченные по времени ID без единого центра генерации — каждый сервер может создавать ID независимо.

Шаг 4. Масштабирование

Что сломается при ×10 нагрузки Как решать
Fanout не успевает Разделить по user_id, увеличить партиции Kafka
Timeline DB переполняется TTL на старые записи, выгрузка в архив
Поиск тормозит Шардирование Elasticsearch по языкам и гео

Пример 2: проектирование Uber-подобной системы (гео-поиск)

Задача: найти ближайших водителей для пассажира. Ключевая сложность — геопространственный поиск в реальном времени.

Алгоритмы гео-поиска

Алгоритм Идея Точность Скорость
Простой круг SELECT WHERE lat BETWEEN... AND lng BETWEEN... Низкая на полюсах Очень быстро
Геохеш (Geohash) Строка фиксированной длины, кодирующая область Средняя Быстро
Quadtree Рекурсивное деление карты на 4 части Высокая Быстро
S2 Geometry (Google) Кривые Гильберта на сфере Очень высокая Быстро

Geohash — самый популярный выбор. Разбивает карту на сетку, кодирует координаты в строку. Чем длиннее строка — тем точнее область. Поиск соседей — строки с общим префиксом.

# Пример: geohash «qd2w0yr3x» означает точку с точностью ~38м × 19м
# Поиск соседей: расширяем префикс и проверяем 8 ячеек вокруг

Uber использует S2 + свой Ringtree для более точного поиска на сфере с учётом кривизны Земли.

Типичные ошибки System Design

1. Начинать с технологий, а не с требований

«Берём Kafka + Cassandra + Kubernetes, потому что Netflix». Нет. Сначала — что система должна делать, какая нагрузка, какие ограничения. Технологии — следствие, а не причина.

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

2. Single Point of Failure (SPOF)

Один мастер БД. Один брокер сообщений. Один API-сервер. Один DNS-провайдер. Любой из этих элементов — это потенциальный простой всей системы.

Правило: для каждого компонента спросите «что будет, если он упадёт?». Если ответ «всё сломается» — нужно дублирование или автоматический failover.

3. Over-engineering на старте

50 микросервисов для системы с 100 пользователей. Kafka с 100 партициями, когда хватит одной. Distributed transactions, когда можно обойтись eventual consistency.

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

4. Игнорирование компромиссов

«PostgreSQL идеален для всего». Нет. У каждой БД свои сильные стороны. PostgreSQL — отличная OLTP, но для аналитики лучше ClickHouse. Redis быстрый, но данные в RAM — для больших объёмов дорого.

Правило: для каждого решения формулируйте trade-off. «Выбираем Redis, потому что 95% запросов — это GET по ключу, где нужна задержка <10ms; платим за ограниченный объём данных и eventual consistency».

5. Не думать о данных

«Сделаем быстро, потом разберёмся с схемой БД». Через год — миграция на полгода. Денормализация сделана без анализа запросов. Индексы не соответствуют нагрузке.

Правило: схема БД и access patterns проектируются до кода. Для каждой коллекции/таблицы — список основных запросов и их частота.

6. Забыть про мониторинг и observability

Система работает, но никто не знает, насколько хорошо. Падения замечают пользователи. Метрики снимают раз в месяц по жалобам.

Правило: метрики, логи, трейсы — с первого дня. SLO/SLI определяются вместе с системой.

Ресурсы для углублённого изучения

Книги:

  • «Designing Data-Intensive Applications» Мартин Клеппман — библия распределённых систем. Покрывает всё от согласованности до шардирования.
  • «System Design Interview» Алекс Сюй (ByteByteGo) — практические кейсы с диаграммами, идеально для подготовки к собеседованиям.
  • «Software Engineering at Google» — реальные практики FAANG-компании.
  • «Building Microservices» Сэм Ньюман — детальный гайд по микросервисной архитектуре.
  • «Web Scalability for Startup Engineers» Артём Раскрыткин — практичный подход для средних нагрузок.

Курсы и блоги:

  • Grokking the System Design Interview (Educative) — пошаговый разбор 25+ типичных задач.
  • High Scalability (highscalability.com) — реальные архитектуры компаний.
  • ByteByteGo (youtube + блог) — визуальные объяснения от Алекс Сюй.
  • The Pragmatic Engineer Newsletter — кейсы из крупных компаний.

Инструменты для практики:

  • Pragmatic API Design — научитесь проектировать API
  • C4 Model + Structurizr DSL — документирование архитектуры (см. нашу статью о C4)

Заключение

System Design — это не про «правильные ответы». Это про умение видеть компромиссы и обосновывать выбор. Лучшие инженеры не те, кто знает больше технологий, а те, кто может объяснить, почему выбрана именно эта.

Главные принципы, которые стоит запомнить:

  1. Требования прежде архитектуры. Без понимания нагрузки и ограничений любая архитектура — гадание.
  2. Каждое решение — trade-off. Не бывает «правильного» выбора, бывает обоснованный.
  3. CAP-теорема — не теоретическая абстракция. Она определяет, какие гарантии вы можете дать во время сбоев.
  4. Кэш решает 80% проблем с производительностью. Не шардирование, не микросервисы — кэш.
  5. БД — главное узкое место. Сначала оптимизируйте индексы и запросы, потом думайте о репликации.
  6. Мониторинг с первого дня. Без метрик вы не знаете, работает ли ваша система.
  7. Простота побеждает сложность. Начните с монолита. Дробите, когда есть причина, а не мода.

С чего начать прямо сейчас:

System Design — это навык, который нарабатывается практикой. Каждая спроектированная система — даже мысленно, на бумажке — делает вас лучше.


Что дальше

Связанные статьи на сайте: