System design
Раунд system design перевіряє не знання конкретної технології, а вміння перетворити розмиту задачу на схему з цифрами й чесно назвати її слабкі місця. Тут — порядок розв’язання, прийоми масштабування, вибір рівня узгодженості, місце API Gateway і розбір типової задачі про масові сповіщення.
Перед цією темою: Черги й Kafka, Архітектурні стилі.
Як підходити до задачі system design на співбесіді?
Section titled “Як підходити до задачі 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, а не середня затримка? — Середнє ховає повільний хвіст, а його бачить кожен сотий користувач.
- Коли зупинитися в деталях? — Коли схема працює цілком; глибину далі обирає інтерв’юер.
Як масштабувати вебсистему?
Section titled “Як масштабувати вебсистему?”Що це
| Спосіб | Як | Межа |
|---|---|---|
| Вертикальне | потужніший сервер: більше 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 і як це впливає на проєктування?
Section titled “Що таке 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 і що він має й не має робити?
Section titled “Навіщо потрібен 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) з піковими кампаніями.
Section titled “Спроєктуйте сервіс масових сповіщень (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) позначати статус як невідомий і звіряти зі звітами провайдера.