Real-Time Backend
Design: Notification Platform
Один упавший сервис уведомлений в Facebook в 2021 году задержал 500 миллионов сообщений на 40 минут. Как проектируют систему, которая не имеет права падать?
- Slack отправляет более 500 млн уведомлений в день через Push, Email и In-App каналы - все они проходят через единую Notification Platform с централизованной маршрутизацией и дедупликацией
- Uber экономит миллионы долларов в год, маршрутизируя non-critical уведомления с SMS на Push - единая платформа позволяет менять канал без изменения product-сервисов
- Airbnb обнаружили падение APNs delivery rate с 88% до 71% за 10 минут благодаря real-time аналитике на ClickHouse и успели откатить изменение до массового user impact
- Facebook разделили notification queue на три приоритета - critical события о безопасности доставляются за <1с даже когда low-priority очередь переполнена миллионами social-алертов
Архитектура платформы уведомлений
Notification Platform - это не «просто отправить письмо». Это распределённая система, которая ежесекундно принимает события от десятков сервисов, маршрутизирует их по каналам, дедуплицирует, rate-limit'ит и гарантирует доставку. Slack обрабатывает более **500 миллионов уведомлений в день** - и ни одно из них не должно потеряться или задвоиться.
Ключевые компоненты
- **Event Ingestion Layer** - принимает события от product-сервисов через Kafka/Pub-Sub, буферизует пики нагрузки
- **Notification Service** - stateless workers, применяют бизнес-логику: кому, по какому каналу, когда
- **Channel Adapters** - изолированные адаптеры под каждый канал (Push, Email, SMS, In-App); у каждого своя retry-стратегия
- **Preference Store** - Redis-кэш поверх PostgreSQL, хранит настройки пользователя per-channel
- **Dedup Cache** - Redis SET с TTL, не даёт отправить одно уведомление дважды при retry
Airbnb перешли на выделенный Notification Service в 2019 году. До этого каждый из 200+ микросервисов сам вызывал SendGrid - итог: 8 разных шаблонов для «Бронирование подтверждено», нет единой аналитики, нельзя глобально выключить канал. Централизация убрала эти проблемы за одну архитектурную итерацию.
Зачем в Notification Platform нужен отдельный Dedup Cache, если Kafka гарантирует at-least-once доставку?
Multi-channel доставка
У каждого канала свои гарантии, лимиты и стоимость. Push-уведомление через APNs стоит $0, SMS через Twilio - $0.0075 за сообщение. Uber тратит **$15-20 млн в год** только на SMS-верификацию - поэтому каждый канал надо изолировать и оптимизировать отдельно.
Характеристики каналов
- **Push (APNs/FCM)** - задержка <1с, delivery rate ~85% (устройства offline), бесплатно, подходит для real-time алертов
- **Email (SendGrid/SES)** - задержка 1-60с, delivery rate ~98%, стоимость $0.0001/письмо, подходит для digest и транзакционных сообщений
- **SMS (Twilio/Vonage)** - задержка <5с, delivery rate ~99%, $0.0075/SMS, только для критичных (OTP, safety alerts)
- **In-App** - delivery rate 100% (пользователь онлайн), zero cost, но работает только при открытом приложении
- **WebPush** - работает в браузере когда вкладка закрыта, delivery rate ~60%, бесплатно
Slack использует **fanout-стратегию**: одно событие «новое сообщение в канале» разворачивается в N уведомлений по всем участникам, каждое маршрутизируется на нужный channel adapter. При 10 миллионах пользователей онлайн одновременно это генерирует миллиарды fanout-операций в час - именно поэтому fanout вынесен в отдельный, горизонтально масштабируемый сервис.
Почему для SMS-адаптера используется retryPolicy с attempts=1, а не exponential backoff как для Push?
Приоритизация и rate limiting
Без приоритизации push о «лайке на фото» забивает очередь и задерживает алерт «транзакция отклонена». Facebook разделил уведомления на **три класса**: critical (платёжные события, безопасность), high (прямые сообщения), low (социальная активность). В пиковой нагрузке low-класс throttle'ится до нуля.
Token Bucket для per-user rate limiting
Приоритетные очереди реализуются через **отдельные Kafka-топики** per priority, а не через один топик с полем priority. Kafka не поддерживает per-message priority внутри топика - consumer читает offset-последовательно. Три топика: `notif.critical`, `notif.high`, `notif.low` - consumer'ы critical-топика получают больше worker-потоков.
- **Глобальный rate limit по каналу** - APNs: 300 req/s per DeviceToken, FCM: 600 req/s per app - защита от блокировки провайдером
- **Per-user per-channel limit** - не более 5 SMS/час, 20 push/час - защита пользователя от спама
- **Burst allowance** - critical события могут использовать burst capacity (2x лимит на 60 секунд) при инцидентах безопасности
- **Quiet hours** - low/high уведомления не отправляются с 23:00 до 08:00 по часовому поясу пользователя; critical - без ограничений
Почему для приоритетной маршрутизации используют три отдельных Kafka-топика, а не один топик с полем `priority: 'high'`?
Аналитика доставки
Отправить уведомление - половина задачи. Знать, что произошло после - вторая половина. Notification analytics отвечает на вопросы: какой процент push open'ится, на каком канале самый высокий delivery rate, для каких сегментов пользователей SMS работает лучше email. Без этого нельзя оптимизировать ни стоимость, ни UX.
Ключевые метрики
- **Delivery Rate** - % уведомлений, подтверждённых провайдером (APNs returned 200, не device delivered). Норма: Push 85-90%, Email 95-98%, SMS 99%+
- **Open Rate** - % уведомлений, по которым пользователь кликнул. Норма: Push 5-10%, Email 20-30% (varies by category)
- **Opt-out Rate** - % пользователей, выключивших канал после N уведомлений. Рост > 2%/мес = сигнал о спаме
- **P99 Delivery Latency** - время от события до доставки. Critical: < 1с, High: < 5с, Low: < 60с
Airbnb строит аналитику на **ClickHouse** - каждое событие жизненного цикла уведомления (queued, sent, delivered, opened, failed) пишется в append-only таблицу. Dashboard показывает delivery rate в реальном времени. Когда Apple ужесточила правила APNs в 2022, Airbnb увидели падение delivery rate с 88% до 71% за 10 минут - и успели откатить изменение до массового user impact.
Достаточно логировать факт отправки уведомления - если провайдер вернул 200, значит пользователь получил
200 от APNs/FCM означает «принято в очередь», а не «доставлено на устройство». Delivery rate и open rate - отдельные метрики, требующие callback-механизмов и клиентского трекинга
APNs возвращает 200 даже если устройство выключено или приложение удалено. Реальная доставка подтверждается только feedback service или read receipt от клиента. Facebook обнаружили, что у 12% пользователей push-токены протухшие - без аналитики они бы отправляли миллионы «успешных» уведомлений в никуда.
Почему для аналитики уведомлений используют ClickHouse, а не тот же PostgreSQL, что и для preference store?
Итоги
- Notification Platform = Event Ingestion + stateless Notification Service + изолированные Channel Adapters + Preference Store + Dedup Cache; централизация убирает дублирование логики и даёт единую аналитику
- Каждый канал (Push, Email, SMS, In-App) изолируется в отдельный адаптер с собственной retry-стратегией: SMS - attempts=1 (провайдер retry'ит сам), Push - exponential backoff до 3 попыток
- Приоритизация реализуется через отдельные Kafka-топики per priority, а не через поле в одном топике - Kafka sequential per partition не позволяет «вытащить» важное сообщение из середины очереди
- Аналитика строится на event sourcing: каждое состояние (queued, sent, delivered, opened, failed) пишется в ClickHouse; delivery rate ≠ sent rate - реальная доставка подтверждается только feedback/read receipt
Связанные темы
Notification Platform опирается на несколько фундаментальных паттернов распределённых систем:
- Message Queues & Kafka — Event Ingestion Layer и приоритетные очереди строятся на Kafka; понимание partition, offset и consumer groups - обязательная база
- Rate Limiting (Token Bucket) — Per-user per-channel rate limiting реализуется через Token Bucket в Redis - тот же алгоритм используется в API Gateway и защите от DDoS
- Idempotency & Deduplication — Dedup Cache защищает от дублей при at-least-once доставке Kafka; паттерн idempotency key применяется во всех distributed системах с retry
Вопросы для размышления
- Как изменится архитектура, если нужно поддержать scheduled-уведомления (отправить через 24 часа)? Какой компонент добавляется и как он взаимодействует с Kafka?
- При пиковой нагрузке (Black Friday у e-commerce) low-priority очередь растёт быстрее, чем обрабатывается. Какие стратегии помогут - shed load, adaptive throttling или autoscaling workers? Какие trade-offs у каждого?
- Пользователь жалуется: «Получаю одно и то же уведомление дважды». Dedup Cache есть. Какие три наиболее вероятные причины дублирования и как их диагностировать?