System design
Раунд system design проверяет не знание конкретной технологии, а умение превратить расплывчатую задачу в схему с цифрами и честно назвать её слабые места. Здесь — порядок решения, приёмы масштабирования, выбор уровня согласованности, место API Gateway и разбор типовой задачи про массовые уведомления.
Перед этой темой: Очереди и Kafka, Архитектурные стили.
Как подходить к задаче system design на собеседовании?
Заголовок раздела «Как подходить к задаче system design на собеседовании?»Что это
System design — задача спроектировать систему целиком: компоненты, данные и связи между ними. Формулировка нарочно расплывчата: «спроектируйте чат» или «сервис коротких ссылок».
Как пользоваться
Порядок для интервью на 45 минут:
- Требования (5–10 мин). Что система делает и для скольких пользователей.
- Оценка нагрузки (3–5 мин). Сколько запросов в секунду и данных в год.
- API и модель данных (5 мин). Основные эндпоинты и таблицы.
- Схема (10–15 мин). Клиент, балансировщик, сервисы, база, кеш, очередь.
- Узкие места (10–15 мин). Что сломается под нагрузкой и как это исправить.
- Итоги (3–5 мин). Короткий пересказ решения и его слабых мест.
Частая ошибка
Молча рисовать схему. Интервьюер оценивает рассуждения, поэтому каждое решение проговаривают вслух.
Как устроено
- Функциональные требования. 3–5 главных сценариев: «создать короткую ссылку», «перейти по ней». Остальное явно выносят за рамки.
- Нефункциональные требования. Число пользователей, соотношение чтения и записи, допустимая задержка (p99), доступность, согласованность, срок хранения.
- Оценка нагрузки. RPS в среднем и в пике, объём данных за год, исходящий трафик. Точность — до порядка.
- API. 2–4 эндпоинта с параметрами и ответом:
POST /links,GET /{code}. - Модель данных. Сущности, ключи и шаблоны доступа — по ним выбирают хранилище.
- Высокоуровневая схема. Путь запроса от клиента до базы и обратно.
- Узкие места. Горячие ключи, единственная база, синхронные вызовы внешних систем.
Ловушки
- Начать с технологий («возьмём 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 RPS2. Чтение: 40 RPS × 100 ≈ 4 000 RPS, пик ≈ 20 000 RPS3. Данные: 100 млн × 500 байт ≈ 50 ГБ в месяц ≈ 600 ГБ в год4. Кеш: 20% ссылок за месяц 100 млн × 0,2 × 500 байт ≈ 10 ГБ — помещается в память одного узла5. Вывод: запись выдержит одна база, чтение — кеш перед ней; за 5 лет ≈ 3 ТБ — к этому моменту подумать о шардировании- Как оценить RPS по числу пользователей? — Активные пользователи в день × запросов на пользователя / 86 400; пик — в 2–10 раз выше среднего.
- Что делать, если интервьюер не даёт цифр? — Предложить свои допущения вслух и попросить подтвердить.
- Чем функциональные требования отличаются от нефункциональных? — Первые — что система делает, вторые — с какими качествами: задержка, доступность, объём.
- Почему p99, а не средняя задержка? — Среднее прячет медленный хвост, а его видит каждый сотый пользователь.
- Когда остановиться в деталях? — Когда схема работает целиком; глубину дальше выбирает интервьюер.
Как масштабировать веб-систему?
Заголовок раздела «Как масштабировать веб-систему?»Что это
| Способ | Как | Предел |
|---|---|---|
| Вертикальное | мощнее сервер: больше CPU и памяти | самая большая машина, простой при замене |
| Горизонтальное | больше серверов за балансировщиком | почти нет, но сервисы должны быть stateless |
Зачем stateless
Балансировщик отправляет запросы одного пользователя на разные серверы. Если корзина хранится в памяти сервера A, на сервере B её нет.
Как пользоваться
- Сессию — в распределённый кеш (Redis) или в токен.
- Загруженные файлы — в объектное хранилище (Azure Blob Storage, S3).
- Статику — в CDN, ближе к пользователю.
Частая ошибка
Включить sticky sessions вместо выноса состояния. При падении сервера пользователи теряют сессии, а нагрузка распределяется неравномерно.
Как устроено
Слои по пути запроса:
- CDN отдаёт статику и кешируемые ответы из точки присутствия рядом с пользователем.
- Балансировщик L4 распределяет TCP-соединения по IP и порту, не заглядывая в HTTP. L7 видит путь, заголовки и cookie, маршрутизирует по URL и завершает TLS.
- Stateless-сервисы — N одинаковых экземпляров с автомасштабированием по CPU или RPS.
- Кеш в памяти и распределённый — подробно в теме Фоновые задачи и кеширование.
- База. Реплики чтения — для запросов, которые терпят отставание; шардирование — когда не справляется запись. Подробно — в теме Проектирование и эксплуатация.
- Очередь — для работы, которую можно сделать позже: письма, отчёты, интеграции.
Когда что выбирать
| Узкое место | Решение |
|---|---|
| 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- Где хранить сессию при горизонтальном масштабировании? — В распределённом кеше или в самом токене, но не в памяти экземпляра.
- Чем L4-балансировщик отличается от L7? — L4 видит TCP/UDP-соединения, L7 — HTTP-запросы, пути и заголовки.
- Почему автомасштабирование может уронить базу? — Больше экземпляров — больше одновременных запросов и соединений к ней.
- Как прочитать свои данные сразу после записи, если есть реплики? — Читать с первичной базы какое-то время после записи или ждать, пока реплика догонит версию.
- Когда нужна очередь? — Когда работу можно отложить, а пик выше постоянной пропускной способности обработчиков.
Что такое 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 > NDynamo-подобные системы не дают линеаризуемости: мешают параллельные записи, 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 принимает запись, после восстановления сети — разрешение конфликта- Что такое линеаризуемость простыми словами? — Система ведёт себя как одна копия: завершённую запись видят все последующие чтения.
- Можно ли выбрать CA? — Только без сети между узлами, то есть на одном узле; распределённая система разделения не выбирает.
- Как обеспечить read-your-writes при репликах? — Читать с лидера после своей записи или ждать, пока реплика догонит версию записи.
- Что такое last-write-wins и чем он опасен? — Побеждает запись с более поздней меткой времени, параллельные изменения молча теряются.
- Чем PACELC полезнее CAP? — Он описывает и обычный режим: даже без сбоев сильная согласованность стоит задержки.
Зачем нужен 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.
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();- Чем API Gateway отличается от балансировщика нагрузки? — Балансировщик распределяет трафик по экземплярам; шлюз ещё маршрутизирует по API, применяет политики и агрегирует ответы.
- Достаточно ли проверять токен на шлюзе? — Нет, если сервис доступен в обход шлюза; сервисы проверяют токен сами.
- Как сделать лимит общим для всех экземпляров шлюза? — Хранить счётчики в распределённом хранилище (Redis) или взять управляемый шлюз.
- Чем BFF отличается от API Gateway? — BFF обслуживает один тип клиента и принадлежит его команде; шлюз общий и тонкий.
- Нужен ли шлюз для вызовов между сервисами? — Обычно нет: east-west трафик закрывают service mesh или клиентские библиотеки.
Спроектируйте сервис массовых уведомлений (SMS, e-mail, push) с пиковыми кампаниями.
Заголовок раздела «Спроектируйте сервис массовых уведомлений (SMS, e-mail, push) с пиковыми кампаниями.»Что это
Сервис, который рассылает сообщения большому числу получателей: коды подтверждения, статусы заказов, рекламу. Каналы — SMS, e-mail и push.
Как устроено
- API принимает заявку: шаблон, список получателей, время отправки.
- Очередь хранит сообщения, пока их не отправят.
- Отправщики берут сообщения из очереди и передают провайдеру: SMS-шлюзу, почтовому сервису, FCM или APNs.
- Статусы — отправлено, доставлено, ошибка — сохраняются в базу и попадают в отчёт.
Частая ошибка
Отправлять прямо в обработчике HTTP-запроса. Кампания на 100 тысяч получателей не уложится в таймаут, а сбой провайдера потеряет часть сообщений.
Как устроено
Путь сообщения:
- Приём и валидация. Формат номеров и адресов, лимиты клиента, существование шаблона.
- Разворачивание. Фоновая задача превращает кампанию в отдельные сообщения с подставленными данными получателя.
- Очереди с приоритетами. Транзакционные сообщения (коды, статусы заказов) и маркетинговые — в разных очередях. Код подтверждения не ждёт миллион рекламных SMS.
- Отправка. Пул отправщиков на канал и провайдера с ограничением скорости.
- Статусы. Провайдер подтверждает приём сразу, а доставку — позже, колбэком или delivery receipt. Сообщение проходит путь
Queued → Sent → DeliveredилиFailed.
Ловушки
- Повторы. Таймаут ответа провайдера не значит, что сообщение не ушло. Повтор без проверки — двойная SMS.
- Часовые пояса. Реклама «в 10:00» — по времени получателя, а не сервера; ночью маркетинг не шлют.
- Длина SMS. Кириллица кодируется в UCS-2: 70 символов в одном SMS, в составном — 67 на сегмент. Стоимость и лимиты провайдера обычно считают в сегментах.
Правило
У каждого сообщения стабильный id от создания до финального статуса: по нему дедупликация, повторы и сверка статусов.
Под капотом
- Campaign API — приём, валидация, расписание, идемпотентный ключ заявки.
- Планировщик разворачивает кампанию пачками с учётом часового пояса получателя и тихих часов.
- Очереди по каналу и приоритету. Гарантии доставки и outbox — в теме Очереди и Kafka.
- Отправщики держат token bucket на каждого провайдера: TPS по договору и число SMPP-сессий. На ответ
ESME_RTHROTTLEDскорость снижают. - Маршрутизатор провайдеров — веса, стоимость по направлению, failover по доле ошибок через circuit breaker; паттерны устойчивости — в Стилях интеграции.
- Приёмник статусов принимает 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, ошибки по провайдеру
Узкие места: лимит провайдеров, запись статусов, разворачивание больших кампаний- Как не отправить одно SMS дважды при таймауте провайдера? — Стабильный id сообщения, проверка статуса перед повтором, дедупликация у отправщика; при at-least-once дубль можно сделать редким, но не невозможным.
- Как код подтверждения пройдёт во время большой кампании? — Отдельная очередь и зарезервированная доля лимита провайдера под транзакционный трафик.
- Что делать, если провайдер начал отвечать ошибками? — Circuit breaker на провайдера и переключение на запасного с учётом стоимости и риска дублей.
- Как учесть часовые пояса? — Хранить пояс получателя и разворачивать кампанию волнами по его локальному времени.
- Что делать с сообщениями без статуса доставки? — После срока жизни сообщения (validity period) помечать статус как неизвестный и сверять с отчётами провайдера.