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-потоков.

  1. **Глобальный rate limit по каналу** - APNs: 300 req/s per DeviceToken, FCM: 600 req/s per app - защита от блокировки провайдером
  2. **Per-user per-channel limit** - не более 5 SMS/час, 20 push/час - защита пользователя от спама
  3. **Burst allowance** - critical события могут использовать burst capacity (2x лимит на 60 секунд) при инцидентах безопасности
  4. **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 есть. Какие три наиболее вероятные причины дублирования и как их диагностировать?

Связанные уроки

  • sd-01-intro
Design: Notification Platform

0

1

Войти