Перейти к содержимому

System design

Раунд system design проверяет не знание конкретной технологии, а умение превратить расплывчатую задачу в схему с цифрами и честно назвать её слабые места. Здесь — порядок решения, приёмы масштабирования, выбор уровня согласованности, место API Gateway и разбор типовой задачи про массовые уведомления.

Перед этой темой: Очереди и Kafka, Архитектурные стили.

Junior+

Как подходить к задаче system design на собеседовании?

Заголовок раздела «Как подходить к задаче system design на собеседовании?»
Ответ

Что это

System design — задача спроектировать систему целиком: компоненты, данные и связи между ними. Формулировка нарочно расплывчата: «спроектируйте чат» или «сервис коротких ссылок».

Как пользоваться

Порядок для интервью на 45 минут:

  1. Требования (5–10 мин). Что система делает и для скольких пользователей.
  2. Оценка нагрузки (3–5 мин). Сколько запросов в секунду и данных в год.
  3. API и модель данных (5 мин). Основные эндпоинты и таблицы.
  4. Схема (10–15 мин). Клиент, балансировщик, сервисы, база, кеш, очередь.
  5. Узкие места (10–15 мин). Что сломается под нагрузкой и как это исправить.
  6. Итоги (3–5 мин). Короткий пересказ решения и его слабых мест.

Частая ошибка

Молча рисовать схему. Интервьюер оценивает рассуждения, поэтому каждое решение проговаривают вслух.

Как устроено

  1. Функциональные требования. 3–5 главных сценариев: «создать короткую ссылку», «перейти по ней». Остальное явно выносят за рамки.
  2. Нефункциональные требования. Число пользователей, соотношение чтения и записи, допустимая задержка (p99), доступность, согласованность, срок хранения.
  3. Оценка нагрузки. RPS в среднем и в пике, объём данных за год, исходящий трафик. Точность — до порядка.
  4. API. 2–4 эндпоинта с параметрами и ответом: POST /links, GET /{code}.
  5. Модель данных. Сущности, ключи и шаблоны доступа — по ним выбирают хранилище.
  6. Высокоуровневая схема. Путь запроса от клиента до базы и обратно.
  7. Узкие места. Горячие ключи, единственная база, синхронные вызовы внешних систем.

Ловушки

  • Начать с технологий («возьмём Kafka»), а не с требований.
  • Строить под миллиард пользователей, когда их тысяча: интервьюер спросит, зачем.
  • Застрять в одном компоненте и не показать систему целиком.

Правило

Каждый блок на схеме объясняется требованием или цифрой из оценки.

Что на самом деле оценивают

  • Работу с неопределённостью. Допущения фиксируют вслух и пишут на доске: «10 млн пользователей в день, чтение к записи 100:1».
  • Вывод из цифр. 2 тыс. RPS на чтение выдержит одна база с кешем, 200 тыс. — уже нет. Архитектура следует из оценки, а не из моды.
  • Глубину по запросу. Интервьюер сам выбирает, где копать: хранилище, отказы, согласованность. Готовиться надо к любому направлению.

Цена

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

Компромиссы

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

Как проверить в проде

Заканчивают наблюдаемостью: какие метрики и SLO покажут, что система справляется (p99, глубина очередей, доля ошибок), и что сломается первым при росте нагрузки в 10 раз.

Итог: на senior-уровне оценивают не схему, а цепочку «требование → цифра → решение → цена» и умение её защитить.

Оценка на салфетке: сервис коротких ссылок
Допущения: 100 млн новых ссылок в месяц, чтение к записи 100:1,
~500 байт на ссылку, пик = 5 × среднее. Сутки ≈ 86 400 с ≈ 10^5 с.
1. Запись: 100 млн / 30 / 86 400 с ≈ 40 RPS, пик ≈ 200 RPS
2. Чтение: 40 RPS × 100 ≈ 4 000 RPS, пик ≈ 20 000 RPS
3. Данные: 100 млн × 500 байт ≈ 50 ГБ в месяц ≈ 600 ГБ в год
4. Кеш: 20% ссылок за месяц
100 млн × 0,2 × 500 байт ≈ 10 ГБ — помещается в память одного узла
5. Вывод: запись выдержит одна база, чтение — кеш перед ней;
за 5 лет ≈ 3 ТБ — к этому моменту подумать о шардировании
Middle+
Ответ

Что это

Способ Как Предел
Вертикальное мощнее сервер: больше CPU и памяти самая большая машина, простой при замене
Горизонтальное больше серверов за балансировщиком почти нет, но сервисы должны быть stateless

Зачем stateless

Балансировщик отправляет запросы одного пользователя на разные серверы. Если корзина хранится в памяти сервера A, на сервере B её нет.

Как пользоваться

  • Сессию — в распределённый кеш (Redis) или в токен.
  • Загруженные файлы — в объектное хранилище (Azure Blob Storage, S3).
  • Статику — в CDN, ближе к пользователю.

Частая ошибка

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

Как устроено

Слои по пути запроса:

  1. CDN отдаёт статику и кешируемые ответы из точки присутствия рядом с пользователем.
  2. Балансировщик L4 распределяет TCP-соединения по IP и порту, не заглядывая в HTTP. L7 видит путь, заголовки и cookie, маршрутизирует по URL и завершает TLS.
  3. Stateless-сервисы — N одинаковых экземпляров с автомасштабированием по CPU или RPS.
  4. Кеш в памяти и распределённый — подробно в теме Фоновые задачи и кеширование.
  5. База. Реплики чтения — для запросов, которые терпят отставание; шардирование — когда не справляется запись. Подробно — в теме Проектирование и эксплуатация.
  6. Очередь — для работы, которую можно сделать позже: письма, отчёты, интеграции.

Когда что выбирать

Узкое место Решение
CPU сервисов больше экземпляров
чтение из базы кеш, затем реплики
запись в базу партиционирование, затем шардирование
короткие пики очередь и фоновые обработчики

Ловушки

  • Лаг реплики. Пользователь сохранил профиль и не видит изменений. Своё после записи читают с первичной базы.
  • Соединения к базе. 50 экземпляров × пул на 100 соединений — до 5 000 подключений к одному серверу.

Правило

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

Под капотом

  • Пропускная способность системы равна пропускной способности самого узкого общего ресурса. Общая база, глобальная блокировка или один внешний провайдер ограничивают систему, сколько бы экземпляров ни было.
  • L4 против L7. L4 дешевле и не зависит от протокола, но не видит запросов: долгое HTTP/2- или WebSocket-соединение остаётся на одном сервере. L7 разбирает HTTP, распределяет отдельные запросы и проверяет здоровье по эндпоинту. В Azure это Load Balancer (L4), Application Gateway (L7 в регионе) и Front Door (глобальный L7 с CDN).
  • Ключ шардирования выбирают так, чтобы почти каждая операция жила в одном шарде: магазин, клиент, регион.

Цена

  • Кеш — устаревшие данные, инвалидация и cache stampede при массовом истечении.
  • Реплики — лаг: асинхронная репликация отдаёт старые данные, синхронная увеличивает задержку записи.
  • Очередь — задержка обработки, повторная доставка и требование идемпотентных обработчиков.

Компромиссы

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

Как проверить в проде

У каждого слоя своя метрика: RPS и p99 на экземпляр, hit ratio кеша, лаг реплик, глубина очереди и возраст старейшего сообщения, число соединений к базе. Ёмкость подтверждают нагрузочным тестом до пика, а не во время него.

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

Схема и число экземпляров
Пользователь
→ CDN: статика и кешируемые GET
→ балансировщик L7: TLS, маршрут по пути, проверки здоровья
→ API × N экземпляров: stateless, сессия в Redis
→ Redis: кеш и сессии
→ база: первичная для записи + реплики для чтения
→ очередь → обработчики × M: письма, отчёты, интеграции
Сколько экземпляров API:
пик 6 000 RPS, по нагрузочному тесту один экземпляр держит ~800 RPS при нормальном p99
6 000 / 800 ≈ 8, плюс запас на отказ зоны и деплой → 10–12
Middle+

Что такое CAP, PACELC и eventual consistency и как это влияет на проектирование?

Заголовок раздела «Что такое CAP, PACELC и eventual consistency и как это влияет на проектирование?»
Ответ

Что это

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

  • Согласованность (C) — каждое чтение видит последнюю запись.
  • Доступность (A) — каждый работающий узел отвечает на запрос.
  • Устойчивость к разделению (P) — система работает, когда сеть между узлами разорвана.

Как это проявляется

Связь между дата-центрами пропала. Узел может отказать в запросе и сохранить C или ответить тем, что знает, и сохранить A — возможно, со старыми данными.

Частая ошибка

Говорить «выбираем CA». Сеть рвётся независимо от нашего желания, поэтому распределённая система выбирает только между C и A на время разрыва.

Как устроено

  • CAP. C здесь — линеаризуемость: система ведёт себя как одна копия, и завершённую запись видят все последующие чтения. A — каждый запрос к работающему узлу получает ответ.
  • PACELC. Если разделение (P) — выбор между A и C; иначе (E) — между задержкой (L) и C. По Abadi, Dynamo, Cassandra и Riak по умолчанию PA/EL, а HBase и VoltDB — PC/EC.
  • Eventual consistency. Реплики сходятся, если новые записи прекратились. Срок схождения не гарантирован.
  • Read-your-writes. Автор сразу видит свои изменения, даже если другие пока нет.
  • Кворум. N реплик, запись ждёт W подтверждений, чтение опрашивает R реплик. При W + R > N чтение задевает хотя бы одну реплику с последней записью.

Когда что выбирать

Операция Выбор
Платёж, списание остатка линеаризуемость: запись через лидера или транзакцию, при разделении — отказ
Корзина доступность: принять добавление всегда, конфликты слить
Лента, счётчик просмотров eventual, автору — read-your-writes

Ловушки

  • Считать eventual «почти мгновенной». Под нагрузкой лаг реплики — секунды и больше.
  • Ждать линеаризуемости от кворума. При параллельных записях и сбоях посреди записи её нет.

Правило

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

Под капотом

  • Доказательство Gilbert и Lynch касается одного объекта в асинхронной сети. О транзакциях, задержках и частичной доступности оно ничего не говорит.
  • Kleppmann в «Designing Data-Intensive Applications» советует не делить базы на CP и AP: определения слишком узкие, а реальные системы настраиваются по-разному для разных операций.
  • Abadi подчёркивает: без разделений CAP не мешает дать и ACID, и высокую доступность. Слабую согласованность в нормальной работе выбирают ради задержки, то есть по ELC.
  • Даже при W + R > N Dynamo-подобные системы не дают линеаризуемости: мешают параллельные записи, sloppy quorum и частично применённые записи.

Цена

  • Линеаризуемость требует координации: запись ждёт лидера или большинства реплик. Между регионами это десятки миллисекунд на каждую операцию.
  • Eventual consistency перекладывает работу на приложение: разрешение конфликтов (last-write-wins молча теряет данные, слияние сложнее), идемпотентность, компенсации.

Компромиссы

Сессионные гарантии — read-your-writes, monotonic reads — дешёвая середина: пользователь видит согласованную картину своих действий без глобальной координации. В Azure Cosmos DB это уровень Session, он по умолчанию; всего уровней пять: Strong, Bounded staleness, Session, Consistent prefix, Eventual.

Как проверить в проде

Лаг реплик и время схождения — метрики с алертами. Аномалии ищут тестами с внесением сбоев в духе Jepsen: разделяют сеть и проверяют историю операций на нарушения гарантий.

Итог: согласованность — спектр, а не переключатель; её выбирают по операции, платя задержкой за сильные гарантии и сложностью приложения за слабые.

Кворумы и поведение при разделении
N = 3 реплики
W = 2, R = 2 → W + R = 4 > 3: чтение задевает реплику с последней записью;
переживает потерю 1 реплики и для чтения, и для записи
W = 3, R = 1 → быстрое чтение, но запись встаёт при потере любой реплики
W = 1, R = 1 → минимальная задержка, eventual: чтение может вернуть старое
Разделение: узел A видит только себя, узлы B и C — друг друга
выбор C: A отказывает в записи (нет кворума), B и C продолжают работать
выбор A: A принимает запись, после восстановления сети — разрешение конфликта
Senior+

Зачем нужен API Gateway и что он должен и не должен делать?

Заголовок раздела «Зачем нужен API Gateway и что он должен и не должен делать?»
Ответ

Что это

API Gateway — сервис, через который проходят все запросы клиентов к системе. Клиент обращается к api.example.com/orders, а шлюз знает, что это сервис заказов.

Зачем

  • Клиент знает один адрес, а не десяток сервисов.
  • Общие задачи — TLS, проверка токена, лимиты запросов, логи — делаются в одном месте.
  • Внутренние сервисы не открыты в интернет.

Как пользоваться

Шлюз настраивают маршрутами «путь → группа экземпляров сервиса»: например, /orders/... → три экземпляра сервиса заказов. Как шлюз и сервисы проверяют JWT — в теме JWT.

Частая ошибка

Писать в шлюзе бизнес-логику: «если сумма заказа больше 1000 — вызвать проверку». Шлюз пропускает и защищает запросы, а решения принимают сервисы.

Как устроено

  • Маршрутизация по пути, хосту или заголовку; балансировка между экземплярами и проверки их здоровья.
  • Аутентификация на краю. Запросы без валидного токена отсекаются до сервисов.
  • Rate limiting и квоты по клиенту или ключу API — защита от одного шумного клиента.
  • Агрегация. Один запрос клиента → несколько вызовов сервисов → один ответ.
  • Сквозная трассировка. Шлюз начинает или продолжает трассу по заголовку traceparent (W3C Trace Context), и запрос видно от входа до базы.
  • Версионирование. /v1 и /v2 ведут в разные версии сервиса; старую снимают, когда по метрикам её перестали вызывать.

Когда что выбирать

Вариант Когда
YARP шлюз как .NET-приложение: своя логика в коде, свой хостинг
Azure API Management API для партнёров: портал разработчика, ключи, квоты, политики без кода
Kong готовый шлюз на плагинах, независимо от стека сервисов

Ловушки

  • Обход шлюза. Шлюз проверил токен, а сервис доверяет заголовку и доступен напрямую — защиты нет.
  • Лимит на экземпляр. Rate limiter ASP.NET Core считает в памяти: на трёх экземплярах шлюза лимит фактически втрое выше.
  • Агрегация. Ответ ждёт самый медленный сервис, а его отказ ломает весь ответ, если не предусмотрен частичный.

Правило

Шлюз знает о протоколе и политиках, но не о предметной области.

Под капотом

  • YARP — библиотека поверх ASP.NET Core. Маршруты и кластеры перезагружаются из конфигурации без рестарта; есть балансировка (по умолчанию PowerOfTwoChoices), активные и пассивные проверки здоровья, transforms. Аутентификация, rate limiting, output caching и таймауты — обычные политики ASP.NET Core, назначенные маршруту.
  • Azure API Management — управляемый шлюз с политиками вроде validate-jwt, rate-limit-by-key и quota и порталом для внешних разработчиков.
  • Kong построен на Nginx и OpenResty, возможности добавляют плагинами.

Цена

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

Компромиссы

  • Тонкий шлюз и BFF. Агрегацию под конкретный клиент выносят в BFF — отдельный бэкенд для мобильного приложения или веба, которым владеет команда этого клиента. Шлюз остаётся общим и тонким.
  • Шлюз и service mesh. Шлюз обслуживает north-south трафик. Вызовы между сервисами (east-west) mesh (Istio, Linkerd) закрывает sidecar-прокси: mTLS, повторы, телеметрия без изменения кода. Гнать внутренние вызовы через центральный шлюз — лишний переход и общая точка отказа.

Как проверить в проде

  • Метрики шлюза по маршрутам: RPS, p99, доля 4xx и 5xx, отказы лимитера (429).
  • Тест обхода: прямой запрос к сервису мимо шлюза должен получить отказ.

Итог: шлюз окупается, когда централизует политики края, и вредит, когда в нём появляются бизнес-правила; для внутреннего трафика есть mesh, для нужд клиента — BFF.

Шлюз на YARP: версия в пути, лимит и проверки здоровья
Yarp.ReverseProxy
using System.Threading.RateLimiting;
using Yarp.ReverseProxy.Configuration;
using Yarp.ReverseProxy.Transforms;
var builder = WebApplication.CreateBuilder(args);
// 100 запросов в секунду с одного IP — на каждом экземпляре шлюза отдельно
builder.Services.AddRateLimiter(o =>
{
o.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
o.AddPolicy("per-client", ctx => RateLimitPartition.GetFixedWindowLimiter(
ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown",
_ => new FixedWindowRateLimiterOptions { PermitLimit = 100, Window = TimeSpan.FromSeconds(1) }));
});
builder.Services.AddReverseProxy().LoadFromMemory(
[new RouteConfig
{
RouteId = "orders-v1",
ClusterId = "orders",
RateLimiterPolicy = "per-client",
Match = new RouteMatch { Path = "/v1/orders/{**rest}" },
}.WithTransformPathRemovePrefix("/v1")], // сервис получает /orders/...
[new ClusterConfig
{
ClusterId = "orders",
LoadBalancingPolicy = "RoundRobin",
// Каждые 10 с GET /health; больной экземпляр выводится из ротации
HealthCheck = new HealthCheckConfig
{
Active = new ActiveHealthCheckConfig { Enabled = true, Interval = TimeSpan.FromSeconds(10), Path = "/health" },
},
Destinations = new Dictionary<string, DestinationConfig>
{
["orders-1"] = new() { Address = "https://orders-1.internal/" },
["orders-2"] = new() { Address = "https://orders-2.internal/" },
},
}]);
var app = builder.Build();
app.UseRateLimiter(); // лишние запросы получают 429 на шлюзе и не доходят до сервиса
app.MapReverseProxy();
app.Run();
Senior+

Спроектируйте сервис массовых уведомлений (SMS, e-mail, push) с пиковыми кампаниями.

Заголовок раздела «Спроектируйте сервис массовых уведомлений (SMS, e-mail, push) с пиковыми кампаниями.»
Ответ

Что это

Сервис, который рассылает сообщения большому числу получателей: коды подтверждения, статусы заказов, рекламу. Каналы — SMS, e-mail и push.

Как устроено

  1. API принимает заявку: шаблон, список получателей, время отправки.
  2. Очередь хранит сообщения, пока их не отправят.
  3. Отправщики берут сообщения из очереди и передают провайдеру: SMS-шлюзу, почтовому сервису, FCM или APNs.
  4. Статусы — отправлено, доставлено, ошибка — сохраняются в базу и попадают в отчёт.

Частая ошибка

Отправлять прямо в обработчике HTTP-запроса. Кампания на 100 тысяч получателей не уложится в таймаут, а сбой провайдера потеряет часть сообщений.

Как устроено

Путь сообщения:

  1. Приём и валидация. Формат номеров и адресов, лимиты клиента, существование шаблона.
  2. Разворачивание. Фоновая задача превращает кампанию в отдельные сообщения с подставленными данными получателя.
  3. Очереди с приоритетами. Транзакционные сообщения (коды, статусы заказов) и маркетинговые — в разных очередях. Код подтверждения не ждёт миллион рекламных SMS.
  4. Отправка. Пул отправщиков на канал и провайдера с ограничением скорости.
  5. Статусы. Провайдер подтверждает приём сразу, а доставку — позже, колбэком или delivery receipt. Сообщение проходит путь Queued → Sent → Delivered или Failed.

Ловушки

  • Повторы. Таймаут ответа провайдера не значит, что сообщение не ушло. Повтор без проверки — двойная SMS.
  • Часовые пояса. Реклама «в 10:00» — по времени получателя, а не сервера; ночью маркетинг не шлют.
  • Длина SMS. Кириллица кодируется в UCS-2: 70 символов в одном SMS, в составном — 67 на сегмент. Стоимость и лимиты провайдера обычно считают в сегментах.

Правило

У каждого сообщения стабильный id от создания до финального статуса: по нему дедупликация, повторы и сверка статусов.

Под капотом

  1. Campaign API — приём, валидация, расписание, идемпотентный ключ заявки.
  2. Планировщик разворачивает кампанию пачками с учётом часового пояса получателя и тихих часов.
  3. Очереди по каналу и приоритету. Гарантии доставки и outbox — в теме Очереди и Kafka.
  4. Отправщики держат token bucket на каждого провайдера: TPS по договору и число SMPP-сессий. На ответ ESME_RTHROTTLED скорость снижают.
  5. Маршрутизатор провайдеров — веса, стоимость по направлению, failover по доле ошибок через circuit breaker; паттерны устойчивости — в Стилях интеграции.
  6. Приёмник статусов принимает HTTP-колбэки и deliver_sm по SMPP и связывает их с сообщением по id провайдера.

Цена

  • Отписку проверяют прямо перед отправкой: с момента создания кампании могли пройти часы. Для e-mail Gmail с 2024 года требует от массовых отправителей SPF, DKIM, DMARC и отписку в один клик.
  • Колбэк приходит дважды или раньше, чем записан id провайдера, — приёмник идемпотентен и умеет отложить обработку.
  • Failover дублирует сообщение, если первый провайдер на самом деле доставил его.

Компромиссы

Статусы — миллионы записей за кампанию. Горячие данные держат в таблице с партициями по дню и пишут пачками, историю уносят в архив.

Как проверить в проде

Глубина очереди и возраст старейшего сообщения по приоритету, фактический TPS против лимита, доля ошибок и время до доставки по провайдеру, доля сообщений без финального статуса через сутки.

Итог: сервис уведомлений — это управление чужой пропускной способностью: приоритеты, лимиты на провайдера, failover и идемпотентность по id сообщения.

Оценка нагрузки
Допущения: 5 млн получателей, маркетинговая кампания раз в день на всех;
транзакционных SMS 1 млн в сутки; кириллица — обычно 2 сегмента;
2 провайдера по 500 сегментов/с
1. Транзакционные: 1 млн / 86 400 с ≈ 12 сообщений/с, пик ×5 ≈ 60/с
2. Кампания: 5 млн × 2 сегмента = 10 млн сегментов
3. Пропускная способность: 2 × 500 = 1 000 сегментов/с,
из них 100/с — резерв под транзакционные
4. Время кампании: 10 млн / 900 сегментов/с ≈ 11 000 с ≈ 3 часа
→ короче текст (1 сегмент), волны по часовым поясам
или повышение лимита у провайдера
5. Статусы: ~2 события на сообщение × 5 млн ≈ 10 млн записей,
~200 байт каждая ≈ 2 ГБ за кампанию
Схема компонентов
Клиенты: CRM, сервис заказов, авторизация
→ Campaign API: валидация, идемпотентный ключ заявки, расписание
→ Планировщик: разворачивает кампанию пачками по часовому поясу
→ Очереди: sms.transactional | sms.marketing | email | push
→ Отправщики: проверка отписки, дедупликация по id, token bucket на провайдера
→ Маршрутизатор: SMPP-провайдер A | HTTP API провайдера B | почта | FCM | APNs
← Приёмник статусов: deliver_sm (delivery receipt), HTTP-колбэки
→ обновление статуса по id провайдера
→ Хранилище: сообщения и статусы (партиции по дню), отписки
→ Метрики и алерты: глубина очередей, TPS, ошибки по провайдеру
Узкие места: лимит провайдеров, запись статусов, разворачивание больших кампаний