Как мы делаем доставку уведомлений (email, sms, etc) конечному пользователю

Автор: Eugeniy Marilev
Теги: PHP Linux
Дата публикации: 2014-05-07 10:23:09

В чем проблематика доставки (рассылки) уведомлений? Для себя выделил следующие критерии важные для простоты, гибкости и стабильности работы системы доставки:

  • Система должна позволять отправлять уведомления в различные каналы (sms, email, и т.д). Добавление нового канала доставки уведомлений должно быть простым и всегда возможным.
  • Включение/отключение определенного канала должно легко настраиваться для любого уведомления.
  • Важно отмечать время, когда и куда уведомление было доставлено, то есть иметь подробный log.
  • Возможность осуществления фоновой рассылки, если число уведомляемых клиентов за единицу времени несравнимо с ожиданием скорости работы web-интерфейса клиента-отправителя. То есть если я, например выкладываю фотографию в соц. сеть, и при этом всем друзьям, которые не online приходит sms о этом событии. У меня 100 offline друзей. Не буду же я ждать в браузере пока все sms отправятся? Конечно же я хочу сразу же продолжать работу!
  • Возможность осуществления плановой рассылки (в определенные моменты времени). Раз в месяц, раз в неделю, ежедневные рассылки.
  • Возможность отписаться от рассылки со стороны получателя даже на уровне определенного уведомления (не сразу от всей рассылки).
  • Хочется чтобы уведомления доходили как можно быстрее, но если этого не выходит, то должна быть возможность управлять приоритетами.
  • В случае если уведомление по какой-то причине не было доставлено, надо же это попробовать сделать позже, то есть такая возможность должна быть.

Как доставка уведомлений обычно делается?

Обычно при разработке сайтов о всех описанных выше моментах никто не думает... А зачем? Зачем что, если вы реально беспокоитесь о доверии к вашему сервису, об удовлетворенности клиентов тем как все работает, вы просто обязаны думать об этом.

Как правило в сайтах на cms, самописных движках, сайтах на framework'ах используются конструкции вида:

  • mail($to, $subject, $body, ….)
  • MySuperEmailHelper::sendMail($to, $subject, $body, ….)
  • require_once 'SomeSMSServiceAPI.php';  $api = new SomeSMSServiceAPI(...); $api->send($to, $message, …)

Это все конечно прикольно, быстро работает... но что если завтра вам в местах, где оправлялись письма на электронную почту, надо будет еще отправлять sms? Вы будете делать поиск/замену по всему проекту, или же выносить отправку в какой-то отдельных helper и там уже посылать в несколько каналов определенное уведомление? А если контент уведомлений должен отличаться для разных каналов? Так это вообще проблема...

Я попытаюсь предлагаемой системой упразднить все эти костыли, предложить простую для понимания и гибкую модель доставки. Вам понравиться, ведь это просто. Но перед этим еще совсем немного полезной информации.

Почему решение со встроенной в веб-приложение доставкой лучше, чем существующие онлайн сервисы?

Для себя я сделал следующие выводы об онлайн сервисах доставки уведомлений. Вот положительные:

  • Мне не нужно писать код, и если мне надо просто что-то планово отсылать. Может и дороже, но с меньшими запарками я так и сделаю.
  • Я могу редактировать шаблоны уведомлений в админке веб-сервиса.

Отрицательных гораздо больше:

  • Мне постоянно надо платить за сервис определенную сумму за период, или еще хуже, чем больше я отсылаю, тем больше мне надо заплатить.
  • У меня нет подробного лога доставки, я лишь знаю общую статистику о подписчиках и сколько я отправил уведомлений.
  • Мне трудно найти сервис, который бы предоставлял мне отправку уведомлений в разные каналы. Скажем одновременно и электронные письма, и sms-ки.
  • В момент подписки клиента мне нужно пересылать на API этого сервиса HTTP запросы, что замедляет регистрацию на моем сайте.
  • База контактных данных клиентов на сервисе и в БД моего сайта отличаются, и мне трудно следить за их синхронизацией. Например, ваш клиент подписался при регистрации на рассылку от вашего сервиса. Теперь он зашел в профиль и решил изменить свой почтовый адрес в настройках. Поверьте, он ожидает, что и рассылка будет ему тоже приходить на новый адрес...
  • Если уведомлений очень много, и приходят они медленно, вряд-ли кто-то будет добавлять специально для вас еще пару серверов, чтобы ускорить этот процесс.
  • Вы не можете достаточно гибко управлять приоритетами. Например, у вас есть free, business и premuim клиенты на сайте. Вам нужно максимально удовлетворять premium, уже менее важно business, все остальное время пусть занимает отправка free пользователям (задержка получения ими уведомления не критична). Так вот с точки зрения сервисов рассылки и free, и business, и premium — это просто получатели.
  • И наконец, если вам необходимо отправлять так называемые «just now» уведомления, то есть кто-то зарегистрировался, и нам нужно ему отослать письмо с ключем активации его аккаунта, то работать со сторонним веб-сервисом отправки для этого - очень не производительно, и попросту неудобно. Неудобно потому что переменные, подставляемые на веб-сервисе в шаблон уведомления передаете вы. Вам если что-то надо изменять, то шаблон вы будете править на сервисе, а передачу переменных в своем коде. Как по мне это как-то криво...

Подробнее о сущностях цикла доставки

Как правило доставляемые уведомления можно разделить на 2 вида:

  1. Уведомление как реакция на событие в системе.
  2. Плановое уведомление системы.

Для второго типа уведомлений у нас как правило нет четкого шаблона, то есть сам шаблон и определяет уведомление, т.к. никакой зависимости от события системы нет. Для первого же типа уведомлений все немного сложнее: было решено определить в системе сущность «Notification», которая может иметь произвольную структуру. С определенными условиями и шаблонами ее информация может быть доставлена конечному клиенту. То есть, в системе что-то произошло и она может записать, скажем, в БД некий Notification, отлично теперь мы уже знаем, что о чем-то необходимо узнать клиенту.

Вторая же часть цикла это собственно доставка (Delivery), не путать с отправкой. Delivery связана с конкретным Notification. Delivery характеризует получателей, их адреса получения в паре с каналами получения. Проще говоря, один и тот же клиент может иметь номер мобильного телефона и почтовый адрес, значит экземпляр Delivery должен содержать информацию о том, что доставить нужно sms на мобильный номер xxx, и электронное письмо на адрес xxx@xxx.xxx.

Сущность характеризующую способ отправки мы называем каналом доставки (DeliveryChannel). То есть при создании экземпляра Delivery мы заполняем у него некое свойство recipients (получателей). Каждый элемент множества получателей характеризуется типом канала доставки и адресом получателя в этом канале. Уже внутри конкретных реализаций абстракции DeliveryChannel скрывается логика отправки контента уведомления нужным адресатам, то есть здесь, а не где-то по коду приложения и делается вызов функции mail(), sendSMS() и т.п.

А теперь вернемся к сущности Notification (описана немного выше). Уже все сущности определены проще будет понять, что именно внутри конкретного Notification пишется логика формирования контента и темы для отправки, в зависимости от канала, который будет эти данные отправлять. Для плановых уведомлений (уведомления второго типа) есть специальный класс NotificationStatic, который содержит такие свойства как subject и content, то есть они формируются статически снаружи, а не внутри самого Notification.

Цикл доставки в действии

Ну а теперь о самом цикле доставки! При проектировании его мы уделили особое внимание приоритетам доставки, рассмотрели тот факт, что должна быть возможность отложенной отправки. Количество отправляемых в единицу времени уведомлений должно быть лимитировано и контролируемо. Нам же не нужна ситуация, когда отправка писем будет перегружать наш веб-сервер?

Для этого мы сделали скрипт работающий в режиме демона, который читает из БД информацию о Delivery, которые еще не были отправлены. Выборка упорядочивается по приоритетам, записи вынимаются порциями по 50-100 записей, после чего отмечаются специальным статусом, чтобы в следующей итерации они не попадали в выборку.

Хотите попробовать? Можно скачать расширение для yii

Ну что? Надеюсь, что вышесказанное оказалось понятным. Кстати, есть приятная новость — это уже все мы реализовали и это OpenSource, можно скачать на сайте yiiframework.com в разделе extensions. Прямая ссылка здесь.

Преимущества и недостатки описанного подхода

Сама абстракция позволяет нам проще и гибче работать с уведомлениями, но есть и ряд недостатков в этом подходе:

  1. Дополнительная абстракция ведет к множеству дополнительных вызовов функций, созданию переменных, поэтому решение работает, конечно, медленнее на уровне кода, нежели частное решение.
  2. Для работы с данных механизмом нужна СУБД, куда будет записываться информация.
  3. Для отправки sms, email и д.р. Вам нужно будет обзавестись собственным или же арендовать почтовый сервер и sms шлюз.

Как сделать доставку уведомлений распределенной?

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

Есть очень классное и главное универсальное, на мой взгляд, программное решение по распределению очередей. Технология называется AMQP. Прочесть подробнее можно здесь. На языке Erlang есть распределенная реализация для *nix и windows систем навываемая RabbitMQ, которая по сей день остается открытым и бесплатным решением.

Так вот это сервис позволяет создавать очереди, класть в них сообщения. Можно создать из своего веб-приложения очереди, складывать в них сообщения о том, что и куда нужно отправить, но не делать фактическую отправку на этом этапе. На других серверах, по расписанию, а лучше в форме сервиса (демона *nix) читать сообщения из очереди.

То есть, у вас может быть 4 сервера для отправки email, и 2 для отправки sms. На главном web-сервере в реализации канала доставки вы вместо отправки email складываете информацию о получателе, теме, содержимом в очередь под названием mail. Аналогично по поводу sms канала, кладем сообщение в очередь sms. Значит в режиме демона на sms шлюзах мы будем вынимать сообщения из очереди sms и выполнять отправку. То же самое для почтовых серверов. При этом мы достигаем многопоточной отправки.

Нужно еще отмечать время доставки в логе, можно это делать еще на этапе попадания уведомления в очередь RabbitMQ на главном сервере. Но, я предлагаю делать для этого дополнительную общую для всей связки очередь, в которую класть сообщения вида: тому-то, такое-то уведомление было доставлено в такое-то время. Из этой очереди будет читать главный web-сервер и отмечать состояния в БД.

Да конечно есть и распределенные реализации mail серверов с балансером, но это намного сложнее в настройке.

Ну что-же, если вам понравилась или как либо пригодилась эта информация, просьба сказать спасибо поставив лайк :)

Комментарии к статье
Комментарии:
Нет результатов.
Только зарегистрированые пользователи могут оставлять комментарии