Сервис уведомлений — это система, отвечающая за доставку своевременной и релевантной информации пользователям через различные каналы, такие как SMS, электронная почта, push-уведомления и внутриигровые/внутриприложенные сообщения.
Пример: После того как пользователь совершает онлайн-покупку, сервис уведомлений может отправить ему электронное письмо с подтверждением заказа, SMS, когда платеж будет обработан, и push-уведомление, когда посылка будет отправлена.
В этой статье мы узнаем, как спроектировать масштабируемый сервис уведомлений, который сможет обрабатывать миллионы уведомлений в день и обеспечивать высокую доступность.
1. Сбор Требований
Прежде чем углубиться в проектирование, давайте определим функциональные и нефункциональные требования.
1.1 Функциональные требования
- Поддержка нескольких каналов: Система должна поддерживать отправку уведомлений через различные каналы, включая электронную почту, SMS, push-уведомления и внутриприложенные сообщения.
- Несколько типов уведомлений: Поддержка транзакционных (например, подтверждение заказа), промо-уведомлений (например, специальные предложения) и системных оповещений (например, сброс пароля).
- Отложенная отправка: Поддержка планирования уведомлений для отправки в будущем.
- Ограничение частоты (Rate Limiting): Обеспечение получения пользователями ограниченного количества промо-сообщений в день для предотвращения спама.
- Механизм повторных попыток: Обработка сбоев при доставке уведомлений с повторными попытками при необходимости (например, при неудачной отправке SMS или email).
1.2 Нефункциональные требования
- Масштабируемость: Система должна обрабатывать миллионы уведомлений в минуту, поддерживая миллионы одновременных пользователей.
- Высокая доступность: Обеспечение минимального времени простоя, чтобы уведомления доставлялись даже в случае сбоев.
- Надежность: Гарантия доставки уведомлений как минимум один раз (at-least-once) с возможностью семантики ровно один раз (exactly-once) для определенных сценариев использования.
- Низкая задержка: Уведомления должны отправляться максимально быстро для обеспечения своевременной доставки.
2. Оценка Масштаба
Прежде чем приступить к проектированию, давайте оценим масштаб, чтобы лучше обосновать архитектурные решения.
- Пользователи: Предположим, что система обслуживает 50 миллионов активных пользователей в день.
- Уведомлений на пользователя: В среднем каждый пользователь получает 5 уведомлений в день.
- Пиковая нагрузка: Предположим, что в пиковое время поступает 1 миллион уведомлений в течение 1 минуты (например, во время распродаж).
Это означает, что система должна обрабатывать:
- Уведомлений в день: 50 миллионов × 5 = 250 миллионов уведомлений/день
- Пик уведомлений в секунду: 1 миллион / 60 ≈ ~17 000 уведомлений/сек
Требования к Хранилищу
Предполагая средний размер уведомления и данных пользователя в 1 КБ:
- Хранилище для данных пользователей: 50 миллионов × 1 КБ = 50 ГБ
- Ежедневное хранилище для уведомлений: 50 миллионов × 5 × 1 КБ = 250 ГБ
3. Высокоуровневый Дизайн
На высоком уровне наша система будет состоять из следующих компонентов:
1. Сервис Уведомлений (Notification Service)
Сервис уведомлений является точкой входа для всех запросов на отправку уведомлений, поступающих как от внешних приложений, так и от внутренних систем. Он предоставляет API, которые различные клиенты могут вызывать для инициации уведомлений.
Это могут быть запросы на отправку транзакционных уведомлений (например, письма для сброса пароля), промо-уведомлений (например, предложения со скидками) или системных оповещений (например, предупреждения о простое).
Каждый запрос проверяется на наличие всей необходимой информации, такой как ID получателя, тип уведомления, содержимое сообщения и каналы, через которые должно быть отправлено уведомление (email, SMS и т.д.).
Для уведомлений, которые должны быть отправлены в будущем, Сервис Уведомлений взаимодействует с Сервисом Планировщика.
После обработки запроса Сервис Уведомлений помещает уведомления в Очередь Уведомлений (например, Kafka или RabbitMQ).
2. Сервис Предпочтений Пользователя (User Preference Service)
Сервис Предпочтений Пользователя позволяет пользователям контролировать, как они получают уведомления.
Он хранит и извлекает индивидуальные предпочтения пользователей относительно получения уведомлений через различные каналы.
Сервис отслеживает, на какие типы уведомлений пользователи явно подписались или отписались.
- Пример: Пользователи могут отказаться от маркетингового или промо-контента.
Чтобы не перегружать пользователей уведомлениями, Сервис Предпочтений Пользователя применяет ограничения на частоту для определенных типов уведомлений, особенно промо-сообщений.
- Пример: Пользователь может получать не более 2 промо-уведомлений в день.
3. Сервис Планировщик (Scheduler Service)
Сервис Планировщик отвечает за хранение и отслеживание запланированных уведомлений — уведомлений, которые должны быть отправлены в определенное время в будущем.
Они могут включать напоминания, рекламные кампании или другие чувствительные ко времени уведомления, которые не отправляются немедленно, но должны быть инициированы по заранее определенному расписанию.
- Пример: Промо-сообщение может быть запланировано для отправки на следующей неделе.
Когда наступает запланированное время, Сервис Планировщик извлекает уведомление из своего хранилища и отправляет его в Очередь Уведомлений.
4. Очередь Уведомлений (Notification Queue)
Очередь Уведомлений действует как буфер между Сервисом Уведомлений и Процессорами Каналов.
Разделяя отправку запроса на уведомление от его фактической доставки, очередь позволяет системе масштабироваться гораздо более эффективно, особенно в периоды высокого трафика.
Система очередей предоставляет гарантии доставки сообщений. В зависимости от сценария использования, она может быть настроена на:
- Доставку как минимум один раз (At-least-once): Гарантирует, что каждое уведомление будет отправлено хотя бы один раз, даже если это приведет к дублированию сообщений в редких случаях.
- Доставку ровно один раз (Exactly-once): Гарантирует, что каждое уведомление будет доставлено ровно один раз, предотвращая дублирование и сохраняя надежность.
5. Процессоры Каналов (Channel Processors)
Процессоры Каналов отвечают за извлечение уведомлений из Очереди Уведомлений и их доставку пользователям через определенные каналы, такие как электронная почта, SMS, push-уведомления и внутриприложенные уведомления.
Разделяя Сервис Уведомлений и фактическую доставку, Процессоры Каналов обеспечивают независимое масштабирование и асинхронную обработку уведомлений.
Такая настройка позволяет каждому процессору сосредоточиться на своем назначенном канале, обеспечивая надежную доставку со встроенными механизмами повторных попыток и эффективной обработкой сбоев.
6. База Данных / Хранилище
Уровень Базы Данных / Хранилища управляет большими объемами данных, включая содержимое уведомлений, предпочтения пользователей, запланированные уведомления, журналы доставки и метаданные.
Системе требуется комбинация решений для хранения данных для удовлетворения различных потребностей:
- Транзакционные данные: Реляционная база данных, такая как PostgreSQL или MySQL, для хранения структурированных данных, таких как журналы уведомлений и статусы доставки.
- Предпочтения пользователей: NoSQL базы данных (например, DynamoDB, MongoDB) для хранения больших объемов пользовательских данных, таких как предпочтения и ограничения на частоту.
- Blob-хранилище: Для уведомлений, содержащих большие вложения (например, письма с изображениями или PDF), могут использоваться Amazon S3 или аналогичные сервисы.
4. Детальный Дизайн
Шаг 1: Создание Запроса на Уведомление
Внешняя система (например, платформа электронной коммерции, генератор системных оповещений или маркетинговая система) генерирует запрос на уведомление.
Пример запроса:
json
{
"requestId": "abc123",
"timestamp": "2024-09-17T14:00:00Z",
"notificationType": "transactional",
"channels": ["email", "sms", "push"],
"recipient": {
"userId": "user789",
"email": "user@example.com"
},
"message": {
"subject": "Подтверждение заказа",
"body": "Спасибо за ваш заказ! Ваш заказ #123456 подтвержден.",
"attachments": ["https://example.com/invoice123456.pdf"],
"smsText": "Спасибо за заказ! Заказ #123456 подтвержден.",
"pushNotification": {
"title": "Заказ подтвержден",
"body": "Ваш заказ #123456 подтвержден. Проверьте вашу электронную почту для получения подробностей.",
"icon": "https://example.com/icon.png",
"action": {
"type": "viewOrder",
"url": "https://example.com/order/123456"
}
}
},
"schedule": {
"sendAt": "2024-09-17T15:00:00Z"
},
"metadata": {
"priority": "high",
"retries": 3
}
}
Шаг 2: Прием Запроса Сервисом Уведомлений
Сервис Уведомлений (через API Шлюз / Балансировщик Нагрузки) получает запрос на уведомление.
Запрос аутентифицируется и проверяется, чтобы убедиться, что он поступил из авторизованного источника и содержит всю необходимую информацию (получатель, сообщение, каналы и т.д.), которая корректна и полна.
Шаг 3: Получение Предпочтений Пользователя
Сервис Уведомлений запрашивает Сервис Предпочтений Пользователя для получения:
- Предпочтительных каналов уведомлений (например, некоторые пользователи могут предпочитать email для промо-сообщений, но SMS для критических оповещений).
- Настроек подписки/отписки: Обеспечивает соблюдение предпочтений пользователя, например, не отправлять маркетинговые письма, если пользователь отписался.
- Ограничений на частоту: Гарантирует, что пользователь не превысит установленные лимиты уведомлений (например, максимум 3 промо-SMS в день).
Пример ответа от Сервиса Предпочтений Пользователя:
json
{
"userId": "user789",
"preferences": {
"channels": {
"transactional": ["email", "push"],
"promotional": ["sms"],
"systemAlert": ["push", "sms"]
},
"doNotDisturb": {
"enabled": true,
"startTime": "22:00",
"endTime": "08:00",
"timezone": "America/New_York"
},
"dailyLimits": {
"promotionalLimit": 2,
"promotionalSentToday": 1
},
"optOut": {
"email": false,
"sms": false,
"push": false
},
"preferredTimeForDelivery": {
"enabled": true,
"startTime": "09:00",
"endTime": "21:00",
"timezone": "America/New_York"
}
}
}
Шаг 4: Планирование (При Необходимости)
Если уведомление запланировано для будущей отправки (например, напоминание на завтра или маркетинговое письмо на следующей неделе), Сервис Уведомлений отправляет уведомление в Сервис Планировщик, который сохраняет уведомление вместе с запланированным временем отправки в базе данных на основе времени или NoSQL базе данных, которая позволяет эффективно выполнять запросы на основе времени.
Таблица scheduled_notifications секционируется по полю scheduled_time, чтобы система могла эффективно извлекать только те уведомления, которые попадают в соответствующий временной диапазон, а не сканировать всю таблицу.
Сервис Планировщик постоянно опрашивает хранилище на предмет уведомлений, готовых к отправке.
- Пример: Каждую минуту (или с более детальным интервалом) сервис запрашивает уведомления, которые должны быть доставлены в следующем временном окне (например, в течение следующих 1–5 минут).
Когда наступает запланированное время, Сервис Планировщик забирает уведомление и отправляет его в Очередь Уведомлений.
Шаг 5: Создание и Форматирование Сообщения
Основываясь на предпочтениях пользователя и запросе, Сервис Уведомлений использует шаблоны (при необходимости) для динамической генерации и форматирования сообщения для каждого канала:
Пример сообщения (Email):
json
{
"messageId": "msg12345",
"timestamp": "2024-09-17T14:00:00Z",
"notificationType": "transactional",
"channel": "email",
"recipient": {
"userId": "user789",
"email": "user@example.com"
},
"messageContent": {
"subject": "Подтверждение заказа - Заказ #123456",
"body": {
"html": "<html><body><h1>Спасибо за ваш заказ!</h1><p>Ваш заказ #123456 подтвержден. Вы можете отслеживать свой заказ <a href='https://example.com/track'>здесь</a>.</p></body></html>",
"plainText": "Спасибо за ваш заказ! Ваш заказ #123456 подтвержден. Отслеживайте заказ здесь: https://example.com/track"
},
"attachments": [
{
"url": "https://example.com/invoice123456.pdf",
"fileName": "invoice123456.pdf",
"mimeType": "application/pdf"
}
]
},
"metadata": {
"priority": "high",
"retries": 3,
"sentBy": "OrderService",
"templateId": "orderConfirmationTemplate",
"requestId": "req9876"
}
}
Шаг 6: Постановка Уведомления в Очередь
После того как Сервис Уведомлений создал и отформатировал сообщения для требуемых каналов, он помещает каждое сообщение в соответствующий топик в Системе Очереди Уведомлений (например, Kafka, RabbitMQ, AWS SQS).
Для каждого канала (email, SMS, push и т.д.) выделен свой собственный топик, что гарантирует независимую обработку сообщений соответствующими Процессорами Каналов.
- Пример: Если уведомление необходимо отправить по email, SMS и push, Сервис Уведомлений генерирует три сообщения, каждое адаптировано под соответствующий канал.
Email-сообщение помещается в Email Топик.
SMS-сообщение помещается в SMS Топик.
Push-сообщение помещается в Push Топик.
Эти топики позволяют каждому Процессору Каналов сосредоточиться на обработке сообщений, относящихся к его каналу, что снижает сложность и повышает эффективность обработки.
Каждое сообщение содержит полезную нагрузку уведомления, информацию, специфичную для канала, и метаданные (такие как приоритет и количество попыток).
Шаг 7: Обработка Сообщений, Специфичных для Канала
Очередь Уведомлений хранит сообщения до тех пор, пока соответствующие Процессоры Каналов не извлекут их для обработки.
Каждый процессор канала выступает в роли потребителя очереди и отвечает за обработку своих собственных сообщений:
- Email Процессор извлекает из Email Топика.
- SMS Процессор извлекает из SMS Топика.
- Push Процессор извлекает из Push Топика.
- In-app Процессор извлекает из In-app Топика.
Шаг 8: Отправка Уведомления
Каждый Процессор Канала обрабатывает доставку уведомления через указанный канал:
- Email Процессор:
Подключается к провайдеру электронной почты (например, SendGrid, Mailgun, Amazon SES).
Отправляет письмо, следуя предпочтениям пользователя (например, HTML или простой текст).
Обрабатывает ошибки, такие как отскоки (bounces) или недействительные адреса электронной почты. - SMS Процессор:
Подключается к SMS-провайдеру (например, Twilio, Nexmo).
Отправляет SMS с необходимыми корректировками форматирования для соблюдения лимитов символов или региональных требований.
Обрабатывает проблемы, такие как недействительные номера телефонов или сетевые ошибки. - Push Процессор:
Использует сервисы, такие как Firebase Cloud Messaging (FCM) для Android или Apple Push Notification Service (APNs) для iOS.
Отправляет push-уведомление, включая любые метаданные (например, действия или иконки, специфичные для приложения).
Обрабатывает сбои, такие как просроченные токены устройств или офлайн-устройства. - In-App Процессор:
Отправляет внутриприложенное уведомление через WebSockets или длинный опрос (long polling) к активной сессии пользователя.
Форматирует сообщение для отображения в пользовательском интерфейсе приложения, соблюдая правила отображения, специфичные для приложения.
Шаг 9: Мониторинг и Подтверждение Доставки
Каждый Процессор Канала ожидает подтверждения от внешнего провайдера:
- Успех: Сообщение доставлено.
- Сбой: Доставка сообщения не удалась (например, сетевые проблемы, неверные адреса).
Процессоры Каналов записывают статус каждого уведомления в таблицу notification_logs для будущих справок, аудита и отчетности.
5. Решение Проблем с Производительностью
5.1 Обработка Сбоев и Повторные Попытки
Если доставка уведомления не удалась из-за временной проблемы (например, простой стороннего провайдера), Процессор Канала попытается отправить уведомление повторно.
Обычно используется стратегия экспоненциальной задержки (exponential backoff) , когда каждая повторная попытка выполняется через прогрессивно увеличивающиеся интервалы.
Если уведомление осталось недоставленным после установленного числа попыток, оно перемещается в Очередь Мертвых Писем (Dead Letter Queue - DLQ) для дальнейшей обработки.
Администраторы могут вручную просматривать и обрабатывать сообщения в DLQ по мере необходимости.
5.2 Масштабируемость
Горизонтальное Масштабирование
Система должна быть спроектирована для горизонтального масштабирования, что означает возможность масштабирования компонентов путем добавления новых экземпляров по мере увеличения нагрузки.
- Сервис Уведомлений: По мере роста объема запросов можно развернуть дополнительные экземпляры для управления возросшей нагрузкой входящих уведомлений.
- Очередь Уведомлений: Распределенные системы очередей, такие как Kafka или RabbitMQ, естественным образом масштабируются и могут обрабатывать большие нагрузки за счет распределения очереди по нескольким узлам.
- Процессоры Каналов: Каждый процессор (email, SMS и т.д.) должен быть горизонтально масштабируемым для обработки больших объемов уведомлений.
Шардирование и Секционирование
Для эффективной обработки больших наборов данных, особенно данных пользователей и журналов уведомлений, используются шардирование и секционирование для распределения нагрузки по нескольким базам данных или географическим регионам:
- Шардирование на основе пользователей: Распределение пользователей по разным базам данных или регионам на основе географического местоположения или ID пользователя для балансировки нагрузки.
- Секционирование на основе времени: Организация журналов уведомлений в разделы на основе времени (например, ежедневно или ежемесячно) для улучшения производительности запросов и управления большими объемами исторических данных.
Кэширование
Внедрение кэширования с использованием таких решений, как Redis или Memcached, для хранения часто запрашиваемых данных, таких как предпочтения пользователей.
Кэширование снижает нагрузку на базу данных и улучшает время отклика для уведомлений реального времени за счет избегания повторных обращений к базе данных.
5.3 Надежность
Для обеспечения высокой доступности данные (например, предпочтения пользователей, журналы) должны реплицироваться в нескольких центрах обработки данных или регионах. Это гарантирует, что даже в случае отказа одного региона данные будут доступны в другом.
- Мульти-зональная репликация (Multi-AZ): Хранение данных в нескольких зонах доступности для обеспечения избыточности.
Балансировщик нагрузки должен использоваться для распределения входящего трафика равномерно между экземплярами Сервиса Уведомлений, гарантируя, что ни один экземпляр не станет узким местом.
5.4 Мониторинг и Логирование
Для обеспечения бесперебойной работы в масштабе система должна иметь:
- Централизованное логирование: Использование таких инструментов, как ELK Stack или Prometheus/Grafana, для сбора журналов из различных компонентов и мониторинга состояния системы.
- Оповещения: Настройка оповещений о сбоях (например, когда уровень отказов при доставке уведомлений превышает пороговое значение).
- Метрики: Отслеживание таких метрик, как процент успешных и неудачных доставок, задержка доставки и пропускная способность для каждого канала.
5.5 Безопасность
Внедрите надежную аутентификацию (например, OAuth 2.0) для всех входящих запросов к сервису уведомлений. Используйте управление доступом на основе ролей (RBAC) для ограничения доступа к критически важным сервисам.
Защитите сервис от злоупотреблений, внедрив ограничение частоты запросов (rate limiting) на шлюзе API для предотвращения DoS-атак.
5.6 Архивирование Старых Данных
Поскольку система уведомлений со временем обрабатывает большие объемы данных, важно реализовать стратегию архивирования старых данных.
Архивирование включает перемещение устаревших или редко используемых данных (например, старых журналов доставки, содержимого уведомлений и истории пользователей) из основного хранилища в более дешевое долгосрочное решение для хранения.
Заключение
Спасибо, что прочитали!
Если вы нашли эту статью полезной, поставьте лайк ❤️ и подумайте о подписке, чтобы получать больше подобного контента каждую неделю.
Если у вас есть вопросы или предложения, оставьте комментарий.
Страховка на собеседовании и поиск работы
Знание есть, но стресс мешает ?
Сообщество для прокачки карьеры и поиска работы в IT:
Платформа - https://itcareergym.tech/#home
Подпишись на - https://t.me/IT_Interview_Partner_Bot
Подпишись на - https://t.me/LyakhovEugene
Курс System Design Website - https://stepik.org/a/279467
C4 + AI: документируем архитектуру быстрее и понятнее - https://stepik.org/a/295262