Разработка
System Design: как бы вы отправили 1 миллион уведомлений?
Большинство инженеров сосредотачивается на том, как быстро отправить уведомления. При проектировании крупных систем важнее отправлять их безопасно. Главная задача не в том, чтобы передать миллион сообщений. Она в том, чтобы этот миллион сообщений не вывел из строя всю платформу.
Чёрная пятница. Маркетинговая команда подготовила рекламную кампанию. Ровно в полночь она хочет отправить уведомления одному миллиону пользователей. Задача кажется простой.
Пока вы не понимаете, что мгновенная отправка миллиона уведомлений может перегрузить серверы приложения, поставщиков уведомлений, базы данных и даже внешние API.
Большинство инженеров сразу скажут:
Используйте Kafka.
Это хорошая отправная точка.
Но что произойдёт, если:
- потребители Kafka не будут успевать обрабатывать сообщения;
- Firebase начнёт ограничивать частоту запросов;
- рабочий процесс завершится с ошибкой посреди обработки;
- уведомления нужно персонализировать;
- следующая кампания будет рассчитана не на миллион, а на 100 миллионов пользователей;
- маркетингу внезапно понадобится отслеживание статусов доставки.
Разберём собеседование по системному проектированию, в ходе которого рассматриваются все эти вопросы.
Задача
Аадвик: Представим, что сегодня Чёрная пятница. Маркетинговая команда хочет немедленно отправить миллион push-уведомлений. Как бы вы спроектировали такую систему?
Сара: Прежде всего я бы отказалась от синхронной обработки. Распространённая ошибка выглядит так:
for(User user : users) {
notificationService.send(user);
}
И всё это выполняется внутри обработчика запроса. При таком подходе доставка уведомлений напрямую связана с сервером приложения. Если попытаться синхронно отправить миллион уведомлений:
- потоки обработки запросов будут заблокированы;
- потребление памяти резко вырастет;
- загрузка процессора подскочит;
- увеличится количество превышений времени ожидания;
- приложение может стать недоступным.
Аадвик: Какой подход вы бы выбрали?
Сара: Я бы разделила создание уведомлений и их фактическую отправку. Приложение должно только создавать задания на отправку уведомлений и помещать их в очередь. Рабочие службы смогут обрабатывать их асинхронно.
Аадвик: Покажите архитектуру.
Сара:

API остаётся лёгким. Его ответственность заканчивается в тот момент, когда запрос на отправку уведомлений надёжно сохранён в очереди.
Аадвик: Почему это лучше?
Сара: Потому что очереди поглощают резкие всплески нагрузки. Представим, что миллион уведомлений поступил за пять секунд. Без очереди:

Приложению пришлось бы сразу обработать весь миллион запросов.
Это создало бы огромный скачок нагрузки.
С очередью:

Очередь работает как буфер. Рабочие службы отправляют уведомления с контролируемой скоростью.
Аадвик: А если маркетинг хочет, чтобы уведомления были доставлены немедленно? Разве очередь не замедлит отправку?
Сара: Не обязательно. Очередь не делает доставку медленнее. Она делает её управляемой.
Например:
1,000 Workers 100 Notifications / Second = 100,000 Notifications / Second
Миллион уведомлений всё равно можно доставить за несколько секунд.
Разница в том, что нагрузка распределяется между множеством рабочих служб, а не обрушивается целиком на слой приложения.
Первая ловушка
Аадвик: Допустим, у нас есть 100 воркеров. Каждый обрабатывает 100 уведомлений в секунду. Всё работает хорошо. Но затем Firebase начинает ограничивать частоту запросов. Что произойдёт?
Сара: Теперь мы столкнулись с ограничением внешней системы. Производительность определяется уже не нашими серверами, а поставщиком уведомлений.
Аадвик: Как вы будете с этим справляться?
Сара: Я бы добавила ограничение скорости отправки на уровне воркеров.

Даже если в очереди находится миллион сообщений, рабочие процессы должны отправлять их только с той скоростью, которую способен обработать поставщик.
Аадвик: Но тогда в очереди возникнет задолженность по обработке?
Сара: Да. И это совершенно нормально. Очереди как раз и существуют для того, чтобы накапливать необработанные сообщения. Растущая очередь обычно намного безопаснее, чем перегрузка внешней зависимости.
Масштабирование рабочих служб
Аадвик: Допустим, глубина очереди сильно увеличилась. Как бы вы масштабировали систему?
Сара: Парк воркеров должен масштабироваться независимо от серверов приложения. Например:

Если в очереди:
10 000 сообщений
можно запустить:
10 рабочих процессов
Если в очереди:
1 000 000 сообщений
можно запустить:
1 000 рабочих процессов
Слой приложения при этом не меняется.
Вторая ловушка
Аадвик: Рабочий процесс успешно отправил уведомление, но завершился с ошибкой до подтверждения обработки сообщения. Очередь доставляет сообщение повторно. Что теперь?
Сара: Это классическая проблема распределённых систем. Уведомление уже могло быть доставлено. Но сообщение поступило на обработку ещё раз. Возникает риск отправить пользователю дубликат.
Аадвик: Как это предотвратить?
Сара: Нужно сделать обработку уведомлений идемпотентной. У каждого уведомления должен быть уникальный идентификатор.
Например:
NOTIF-1001
Перед отправкой выполняется запрос:
INSERT INTO processed_notifications
(notification_id)
VALUES ('NOTIF-1001');
Для поля notification_id должно действовать ограничение уникальности. Если вставка прошла успешно, отправляем уведомление. Если вставка завершилась ошибкой, значит уведомление уже обрабатывалось. Его нужно пропустить.
Аадвик: Знакомый подход.
Сара: Тот же принцип применяется в платёжных системах. Повторные попытки неизбежны. Наша задача — сделать их безопасными.
Пакетная отправка
Аадвик: Допустим, для отправки одного уведомления требуется один запрос к API. Миллион уведомлений означает миллион запросов. Можно ли сделать лучше?
Сара: Конечно. Многие поставщики поддерживают пакетные API .
Вместо схемы:
1 запрос = 1 уведомление
можно использовать:
1 запрос = 500 уведомлений
Тогда:
1 000 000 уведомлений
превращается в:
2 000 запросов
вместо:
1 000 000 запросов
Это значительно уменьшает сетевые накладные расходы.
Очередь необрабатываемых сообщений
Аадвик: Что делать, если уведомление постоянно не удаётся отправить?
Например:
- токен устройства недействителен;
- адрес электронной почты некорректен;
- поставщик отклоняет запрос.
Нужно ли повторять попытки бесконечно?
Сара: Нет. Количество повторных попыток должно быть ограничено. Распространённая схема выглядит так:

Например:
Retry 1 Retry 2 Retry 3 Retry 4 Retry 5
После достижения максимального числа попыток сообщение перемещается в очередь необработанных сообщений — DLQ.
Позднее специалисты смогут изучить причины ошибок и устранить их.
Третья ловушка
Аадвик: А если кампания рассчитана не на миллион, а на 100 миллионов пользователей? Вы всё равно поместите 100 миллионов сообщений непосредственно в Kafka?
Сара: Скорее всего, нет. При таком масштабе я бы разделила описание кампании и создание отдельных заданий на отправку.
Вместо немедленного создания всех сообщений лучше использовать генератор:
Генератор может создавать задания на отправку постепенно. Это позволит не заполнять Kafka сотнями миллионов записей одновременно.
Отслеживание доставки
Аадвик: Маркетинговой команде нужна аналитика. Она хочет знать, сколько уведомлений:
- отправлено;
- доставлено;
- открыто;
- завершилось ошибкой.
Как это реализовать?
Сара: Поставщики уведомлений обычно отправляют обратные вызовы со статусами. Я бы обрабатывала их асинхронно в виде событий.
Так аналитика остаётся отделена от процесса доставки уведомлений.
Завершение собеседования
Аадвик: Подведите итог своей архитектуры.
Сара: Моя система включала бы:
- асинхронную обработку через очередь;
- независимые парки воркеров;
- автоматическое масштабирование по глубине очереди;
- ограничение скорости с учётом возможностей поставщика;
- идемпотентную обработку уведомлений;
- пакетную отправку везде, где она поддерживается;
- очереди повторных попыток с экспоненциальной задержкой;
- очередь необрабатываемых сообщений для постоянных ошибок;
- отслеживание доставки через асинхронные события.
Большинство инженеров сосредотачивается на том, как быстро отправить уведомления. При проектировании крупных систем важнее отправлять их безопасно. Главная задача не в том, чтобы передать миллион сообщений. Она в том, чтобы этот миллион сообщений не вывел из строя всю платформу.
-
Новости4 недели назадВидео и подкасты о мобильной разработке 2026.25
-
Разработка4 недели назадЛучшие практики SwiftUI из агентского навыка Xcode 27
-
Новости4 недели назадAndroid 17 официально вышел
-
Интервью4 недели назадМаркетологи в мобайле: Анастасия Луканова (руководитель направления по развитию рекламного бизнеса RuStore)

