Перейти к основному содержимому

Email-рассылка как распределённая система

Разработчику Архитектору

На бумаге сервис рассылки выглядит как три сущности: список адресов, кампания, кнопка Send. На практике доставка почты — это асинхронная интеграция с внешними MTA (Gmail, Outlook, корпоративный Exchange), где ошибки приходят с задержкой, повторная отправка опасна, а «успех» в API провайдера ещё не значит попадание во входящие.

Эта глава собирает типичный продакшн-контур в одном месте: для system design, для code review и как мост между SMTP в коде и картой system design. Сквозной пример в формате собеседования — ниже в 143.

Откуда кейс

Такой контур встречается в транзакционной почте (сброс пароля, чеки), в newsletter-платформах и в open-source вроде Camp (Go). Продукты различаются UI и фичами, а инженерные задачи доставки — одни и те же.


Ложный CRUD и реальная граница системы

Слой, который видит продуктЧто происходит в инфраструктуре
Сохранить подписчиков в БДВалидация, double opt-in, согласие на обработку данных
Создать кампанию и нажать SendПостановка N×M задач в очередь, лимиты, state machine
«Письмо отправлено» в UIПринято ESP/SMTP; доставка и открытие — отдельные события
Статистика открытийОтдельные HTTP endpoints, боты, блокировщики трекеров

Пока рассылка идёт на десяток адресов через один SMTP-ящик, хватает скрипта. После выхода в прод с тысячами получателей и разными доменами @gmail.com / @company.com появляются очередь, пул воркеров, слой доставки (адаптер к SES, SendGrid, Mailgun, Postmark или своему MTA) и конечный автомат на каждое письмо или пару «кампания + получатель».


Типовая архитектура

API принимает «запустить кампанию», фиксирует намерение в БД и не шлёт письма в HTTP-запросе. Outbox (паттерн) гарантирует, что запись о кампании и задача в очереди появятся вместе. Воркеры масштабируются отдельно; их узкое место — лимиты ESP и репутация домена, а не CPU API.

Скелет «API → primary → очередь → воркеры» — тот же, что в типовом контуре system design и §4 очередей.


Конечный автомат сообщения

Статус хранят на уровне отправки одному получателю (или одного message id), а не только на уровне кампании «в целом».

СтатусСмысл для продукта
pending / sendingМожно показывать «в очереди»; при падении воркера задача возвращается в очередь
acceptedПровайдер принял; во «входящие» письмо может не попасть (спам, DMARC)
bounced_hardАдрес недействителен — больше не слать без ручной проверки
bounced_softВременный отказ — ограниченный retry
suppressedГлобальный запрет на адрес (bounce, complaint, ручной unsubscribe)
complainedЖалоба «спам» — хуже hard bounce для репутации домена

Кампания в UI — агрегат: sent = count(delivered) + count(accepted), failed = count(suppressed) + count(bounced_hard).


Bounces и suppression list

Bounce — уведомление о недоставке: SMTP-ответ при отправке или asynchronous bounce (отдельное письмо от MTA получателя на ваш bounce-адрес / webhook ESP).

ТипТипичные причиныПолитика
Hard bounceНесуществующий ящик, домен не принимает почтуСразу в suppression; retry бессмысленен
Soft bounceПочтовый ящик переполнен, greylisting, временный 4xxExponential backoff, cap попыток (например 3–5 за 72 ч)
ComplaintПользователь нажал «Спам» у провайдераНемедленный suppress; расследование контента и частоты

Suppression list — отдельное хранилище (таблица или Redis set) email-адресов и доменов, куда ни одна кампания больше не ставит задачу. Проверка выполняется до постановки в очередь и перед вызовом delivery layer.

Повторная отправка на hard bounce

Каждая попытка бьёт по репутации отправляющего домена (sender score). ESP могут ограничить или заблокировать аккаунт. Suppression — часть устойчивости, а не «удобство CRM».


Retry policy

Retry относится к инженерии устойчивости (design/2136), а не к циклу for вокруг sendmail.

СитуацияRetry?Заметка
Таймаут к ESP, 5xx провайдераДаС jitter и max attempts
Soft bounceДаС увеличением интервала
Hard bounce, complaintНетТолько suppress
Успешный accepted, потом duplicate workerНет повторной отправкиНужна идемпотентность (ниже)
Rate limit 421 / 450 от GmailДаС глобальным throttle по домену получателя

Повторять имеет смысл только идемпотентные шаги (design/213): «поставить задачу», «отметить accepted», но не «отправить письмо без ключа дедупликации».


Идемпотентность и сбой воркера

Типичный инцидент: воркер отправил письмо через ESP, упал до commit статуса в БД; при рестарте очередь отдаёт задачу снова — пользователь получает дубликат.

Защита:

  1. Уникальный ключ отправки(campaign_id, recipient_id) или message_id (UUID), передаётся в ESP как idempotency key / custom header, где API это поддерживает.
  2. Состояние «sending» с lease — только один воркер держит lock на строку; по TTL lock снимается для recovery.
  3. At-least-once очередь (брокеры) + идемпотентный consumer: повторная доставка задачи не создаёт второе письмо, если статус уже accepted или выше.

Транзакционный outbox связывает «кампания запущена» и «задачи созданы»; без него возможны кампании без писем или письма без записи в БД.


SPF, DKIM, DMARC — «тихая» недоставка

Ошибки аутентификации редко ломают ваш API. Письмо уходит, а попадает в спам или отбрасывается на стороне получателя.

МеханизмГде настраиваетсяЧто проверяет получатель
SPFDNS TXT у домена отправителяРазрешён ли этот IP/хост слать от имени домена
DKIMDNS TXT + подпись заголовков письмаЦелостность письма и соответствие домену
DMARCDNS TXTПолитика при несовпадении SPF/DKIM (none / quarantine / reject) + отчёты

Записи TXT для SPF/DKIM — в настройке DNS; порты submission — в справочнике почты.

Практика для разработчика

На staging проверяйте заголовки Authentication-Results, отчёты DMARC в почте администратора и тестовые ящики (mail-tester, встроенные postmaster-консоли Gmail/Outlook). Метрика «accepted от ESP» без мониторинга spam rate обманывает дашборд.


Throttling и домен получателя

Лимиты бывают на трёх уровнях:

УровеньПримерРеакция
Ваш API / воркеры1000 задач/сToken bucket на enqueue
ESP (SES, SendGrid)N писем/сек на аккаунтОчередь + backoff
Почтовый провайдер получателяGmail: плавный ramp для нового доменаPer-domain лимитер (gmail.com, outlook.com)

Один глобальный sleep(1) между письмами не спасает: домен small-corp.ru может принимать быстро, а Gmail — нет. Часто вводят несколько очередей или rate-limit keys по MX-домену получателя.

Связь с rate limiting в архитектуре и троттлингом в администрировании.


Webhooks от ESP

Провайдеры шлют HTTP POST о событиях: delivered, bounced, complained, opened (если включено на их стороне). Это тот же паттерн, что входящие webhooks:

  • проверка подписи (HMAC shared secret);
  • идемпотентность по event_id провайдера;
  • быстрый 200 OK и обработка в фоне (очередь), чтобы не истекал timeout повторов у ESP;
  • защита от replay (timestamp + nonce).

Обработчик обновляет state machine и suppression list; без него soft/hard bounce из реального мира не попадут в вашу БД.


Open tracking, клики и отписка

ФункцияРеализацияРиски
Open trackingПиксель 1×1 на вашем HTTPSБлокировщики, превью почтовых клиентов, завышенные «открытия»
Click trackingРедирект через ваш доменСломанные ссылки при падении трекера; нужен fallback
UnsubscribeЗаголовок List-Unsubscribe + URL + опционально mailto:Юридические требования (CAN-SPAM, GDPR); отписка должна работать без логина по подписанному токену

Эндпоинты трекинга и отписки — отдельная поверхность атаки (подбор токенов, DDoS). Нужны rate limit, короткий TTL токена, логирование без PII в открытом виде.

Транзакционные письма (сброс пароля) обычно без marketing open-tracker; маркетинговые — с явным согласием в коммуникациях и GDPR.


Слой доставки — свой SMTP или ESP

ПодходКогда уместенЧто вы всё равно пишете сами
ESP (SES, SendGrid, Mailgun, Postmark)Почти всегда в продОчередь, state machine, webhooks, suppression
Свой MTA (Postfix + IP, warmup)Крупный объём, своя доставляемостьРепутация IP, bounce mailbox, FBL, мониторинг blacklists
smtplib в приложенииПрототип, внутренние уведомленияThrottling, retry, bounces — вручную

В Python-главе разобраны SMTP-коды и bulk-отправка; для продакшн-объёма там же указан переход на очередь и ESP с готовыми bounces.


Наблюдаемость

Метрики, без которых рассылку слепо эксплуатировать:

МетрикаЗачем
Глубина очереди / age of oldest messageЗадержка кампании, нехватка воркеров
Отправок/сек по MX-доменуПоймать ban от Gmail
Доля hard/soft bounce, complaint rateРепутация домена
Retry count, DLQ sizeПолитика устойчивости
Расхождение accepted vs webhook deliveredПроблемы auth / спам

Алерты — на рост complaint rate и DLQ, а не только на 5xx API.


Чек-лист для system design interview

Краткий порядок ответа (полный каркас — 143):

  1. NFR — писем/день, допустимая задержка кампании, нужна ли exactly-once доставка письма пользователю.
  2. High-level — API + DB + outbox + очередь + workers + ESP; webhooks обратно.
  3. Deep dive — state machine, suppression, идемпотентность, per-domain throttle.
  4. Отказы — падение ESP (circuit breaker, пауза кампании), дубликаты при retry воркера.
  5. Compliance — unsubscribe, хранение согласий, разделение transactional / marketing.

См. также

См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").