Как управлять автоматическими электронными письмами, отправленными из веб-приложения

12

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

В настоящее время у меня есть эта функция, встроенная в мое веб-приложение, и электронные письма отправляются на основе ввода / взаимодействия с пользователем (например, создание нового пользователя). Проблема в том, что подключение к почтовому серверу напрямую занимает пару секунд. Расширение моего приложения, это будет существенным препятствием в будущем.

Каков наилучший способ управления отправкой большого количества автоматических электронных писем в архитектуре моей системы?

Не будет отправлено огромное количество писем (максимум 2000 в день). Письма не нужно отправлять сразу, задержка до 10 минут - это нормально.

Обновление: Очередь сообщений была дана как ответ, но как это было бы разработано? Будет ли это обрабатываться в приложении и обрабатываться в течение тихого периода, или мне нужно создать новое «почтовое приложение» или веб-сервис, чтобы просто управлять очередью?

Gaz_Edge
источник
Можете ли вы дать нам грубое чувство масштаба? Сотни, тысячи или миллионы писем? Кроме того, следует ли отправлять электронные письма немедленно или допустимо небольшое отставание?
Яннис
Отправка электронной почты включает передачу SMTP-сообщения принимающему почтовому хосту, но это не означает, что сообщение действительно было доставлено. Таким образом, вся отправка электронной почты происходит асинхронно, и нет смысла притворяться, что она «ждет успеха».
Килиан Фот
1
Я не "жду успеха", но мне нужно подождать, пока SMTP-сервер примет мой запрос. @YannisRizos смотрите обновление RE ваш комментарий
Gaz_Edge
Для 2000 (что вы описали макс) писем это будет просто работать. Когда они происходят в течение, скажем, 10 рабочих часов, это 3 письма в минуту, что очень выполнимо. Просто убедитесь, что вы правильно настроили свою DNS-запись, и провайдер принимает вас, отправляя их в этих количествах. Также подумайте: «какой почтовый сервер не работает?». Загрузка 2000 писем не о чем беспокоиться.
Люк Франкен
Ответ на вопрос, где находится CRONTAB
Tulains Córdova

Ответы:

15

Общий подход, как уже упоминал Оз , - это очередь сообщений . С точки зрения дизайна очередь сообщений - это, по существу, очередь FIFO , которая является довольно фундаментальным типом данных:

Очередь FIFO

Особенность очереди сообщений заключается в том, что, хотя ваше приложение отвечает за постановку в очередь, за удаление очереди будет отвечать другой процесс. В очереди на очереди ваше приложение является отправителем сообщения, а процесс удаления из очереди - получателем. Очевидным преимуществом является то, что весь процесс является асинхронным, получатель работает независимо от отправителя, пока есть сообщения для обработки. Очевидным недостатком является то, что вам нужен дополнительный компонент, отправитель, чтобы все это работало.

Поскольку ваша архитектура теперь основана на двух компонентах, обменивающихся сообщениями, вы можете использовать для этого причудливый термин межпроцессное взаимодействие .

Как введение очереди влияет на дизайн вашего приложения?

Определенные действия в вашем приложении генерируют электронные письма. Введение очереди сообщений будет означать, что эти действия теперь должны отправлять сообщения в очередь вместо этого (и ничего более). Эти сообщения должны содержать абсолютный минимальный объем информации, необходимый для построения электронных писем, когда ваш получатель получает их обрабатывать.

Формат и содержание сообщений

Формат и содержание ваших сообщений полностью зависит от вас, но вы должны помнить, что чем меньше, тем лучше. Ваша очередь должна быть настолько быстрой для записи и обработки, насколько это возможно, выброс больших объемов данных в нее, вероятно, создаст узкое место.

Кроме того, некоторые облачные службы очередей имеют ограничения по размеру сообщений и могут разбивать большие сообщения. Вы не заметите, что разделенные сообщения будут обрабатываться как единое целое, когда вы их просите, но вы будете платить за несколько сообщений (при условии, конечно, что вы используете услугу, которая требует платы).

Дизайн ресивера

Поскольку мы говорим о веб-приложении, общим подходом для вашего получателя будет простой скрипт cron. Он будет запускаться каждые xминуты (или секунды) и будет:

  • Pop nколичество сообщений из очереди,
  • Обрабатывать сообщения (т.е. отправлять электронные письма).

Обратите внимание, что я говорю pop вместо get или fetch, потому что ваш получатель не просто получает элементы из очереди, он также очищает их (т.е. удаляет их из очереди или помечает как обработанные). Как именно это произойдет, зависит от вашей реализации очереди сообщений и конкретных потребностей вашего приложения.

Конечно, я описываю, по сути, пакетную операцию , самый простой способ обработки очереди. В зависимости от ваших потребностей вы можете захотеть обрабатывать сообщения более сложным способом (это также потребует более сложной очереди).

Движение

Ваш получатель может принимать во внимание трафик и регулировать количество сообщений, которые он обрабатывает, основываясь на трафике во время его выполнения. Упрощенным подходом было бы предсказать ваши часы с большим трафиком на основе прошлых данных о трафике и предполагая, что вы использовали скрипт cron, который запускается каждую xминуту, вы можете сделать что-то вроде этого:

if( 
    now() > 2pm && now() < 7pm
) {
    process(10);
} else {
    process(100);
}

function process(count) {
    for(i=0; i<=count; i++) {
        message = dequeue();
        mail(message)
    }
}

Очень наивный и грязный подход, но он работает. Если этого не произойдет, то другим подходом будет выяснить текущий трафик вашего сервера на каждой итерации и соответствующим образом скорректировать количество элементов процесса. Пожалуйста, не оптимизируйте микро, если это не является абсолютно необходимым, хотя вы бы напрасно тратили время.

Хранение в очереди

Если ваше приложение уже использует базу данных, то единственная таблица в ней будет самым простым решением:

CREATE TABLE message_queue (
  id int(11) NOT NULL AUTO_INCREMENT,
  timestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  processed enum('0','1') NOT NULL DEFAULT '0',
  message varchar(255) NOT NULL,
  PRIMARY KEY (id),
  KEY timestamp (timestamp),
  KEY processed (processed)
) 

Это действительно не сложнее, чем это. Конечно, вы можете сделать это настолько сложным, насколько вам нужно, например, вы можете добавить поле приоритета (что будет означать, что это больше не очередь FIFO, но если вам это действительно нужно, кого это волнует?). Вы также можете упростить это, пропустив обработанное поле (но тогда вам придется удалять строки после того, как вы их обработали).

Таблица базы данных была бы идеальной для 2000 сообщений в день, но, вероятно, не будет хорошо масштабироваться для миллионов сообщений в день. Необходимо учитывать миллион факторов, и все в вашей инфраструктуре играет роль в общей масштабируемости вашего приложения.

В любом случае, если вы уже определили очередь на основе базы данных как узкое место, следующим шагом будет рассмотрение облачной службы. Amazon SQS - это единственная служба, которую я использовал и выполнил то, что обещал. Я уверен, что есть довольно много подобных услуг там.

Очереди на основе памяти - это тоже кое-что, особенно для коротких очередей. memcached отлично подходит для хранения очереди сообщений.

На каком бы хранилище вы ни решили построить свою очередь, будьте умны и абстрагируйтесь. Ни ваш отправитель, ни ваш получатель не должны быть привязаны к определенному хранилищу, в противном случае переключение на другое хранилище в более позднее время будет полным PITA.

Подход реальной жизни

Я построил очередь сообщений для писем, которая очень похожа на то, что вы делаете. Он был в проекте PHP, и я построил его вокруг Zend Queue , компонента Zend Framework, который предлагает несколько адаптеров для разных хранилищ. Мои хранилища где:

  • PHP массивы для модульного тестирования,
  • Amazon SQS на производстве,
  • MySQL в среде разработки и тестирования.

Мои сообщения были настолько просты, насколько это возможно, мое приложение создавало небольшие массивы с необходимой информацией ( [user_id, reason]). Хранилище сообщений было сериализованной версией этого массива (сначала это был внутренний формат сериализации PHP, затем JSON, я не помню, почему я переключился). Это reasonконстанта, и, конечно, у меня где-то есть большая таблица, которая отображает reasonболее полные объяснения (мне удалось разослать около 500 электронных писем клиентам с загадочным текстом reasonвместо одного более полного сообщения).

дальнейшее чтение

Стандарты:

Инструменты:

Интересно читает:

Яннис
источник
Ух ты. Примерно лучший ответ, который я когда-либо получал здесь! Не могу отблагодарить вас достаточно!
Gaz_Edge
Я, и уверен, что миллионы других используют этот FIFO с Gmail и Google Apps Script. фильтр Gmail помечает любую входящую почту на основе критериев, и это все, ставит их в очередь. Скрипт Google Apps запускается каждые X, получает первые y сообщений, отправляет их, удаляет их из очереди. Промыть и повторить.
ДавЧана
6

Вам нужна какая-то система очередей.

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

Вы можете иметь значение для электронных писем, чтобы некоторые из них действовали почти немедленно (например, сброс пароля), а те, которые имеют меньшее значение, могли быть сгруппированы для последующей отправки.

Ozz
источник
у вас есть схема архитектуры или пример, который показывает, как это работает? Например, находится ли очередь в другом «приложении», скажем, почтовое приложение, или она получает процесс из веб-приложения в течение тихого периода. Или я должен создать своего рода веб-сервис для их обработки?
Gaz_Edge
1
@Gaz_Edge Ваше приложение помещает элементы в очередь. Фоновый процесс (скорее всего, cron-скрипт) извлекает x элементов из очереди каждые n секунд и обрабатывает их (в вашем случае отправляет электронное письмо). Одна таблица базы данных отлично работает в качестве хранилища очереди для небольшого количества элементов, но, вообще говоря, операции записи в базу данных являются дорогостоящими, а для больших объемов вы можете захотеть взглянуть на такие сервисы, как SQS Amazon .
Яннис
1
@Gaz_Edge Я не уверен, что могу изобразить это немного проще, чем то, что я написал "... записать в таблицу базы данных и получить в этой таблице строки других внешних процессов приложения ...", а для таблицы читать "любую очередь" «Какие бы технологии ни были.
Оз
1
(продолжение ...) Вы можете создать фоновый процесс, который очищает очередь таким образом, чтобы учитывать ваш трафик, например, вы можете настроить его на обработку меньшего количества элементов (или ни одного вообще) в моменты, когда ваш сервер находится под нагрузкой , Вам придется либо прогнозировать эти стрессовые времена, просматривая данные о прошлом трафике (проще, чем кажется, но с большим запасом ошибок), либо заставляя фоновый процесс проверять состояние трафика при каждом запуске (более точно, но дополнительные накладные расходы редко нужны).
Яннис
@YannisRizos хотите объединить ваши комментарии в ответ? Также были бы полезны архитектурные схемы и проекты (я решил получить их от этого вопроса на этот раз! ;-))
Gaz_Edge
2

Не будет отправлено огромное количество писем (максимум 2000 в день).

В дополнение к очереди, вторая вещь, которую вы должны рассмотреть, это отправка электронных писем через специализированные службы: например, MailChimp (я не связан с этой службой). В противном случае многие почтовые сервисы, такие как gmail, скоро отправят ваши письма в папку для спама.

OZ_
источник
2

Я смоделировал мою систему очередей в разных 2 таблицах как;

CREATE TABLE [dbo].[wMessages](
  [Id] [uniqueidentifier]  NOT NULL,
  [FromAddress] [nvarchar](255) NOT NULL,
  [FromDisplayName] [nvarchar](255) NULL,
  [ToAddress] [nvarchar](255) NOT NULL,
  [ToDisplayName] [nvarchar](255) NULL,
  [Graph] [xml] NOT NULL,
  [Priority] [int] NOT NULL,
  PRIMARY KEY CLUSTERED ( [Id] ASC ))

CREATE TABLE [dbo].[wMessageStates](
  [MessageId] [uniqueidentifier] NOT NULL,
  [Status] [int] NOT NULL,
  [LastChange] [datetimeoffset](7) NOT NULL,
  [SendAfter] [datetimeoffset](7) NULL,
  [SendBefore] [datetimeoffset](7) NULL,
  [DeleteAfter] [datetimeoffset](7) NULL,
  [SendDate] [datetimeoffset](7) NULL,
  PRIMARY KEY CLUSTERED ( [MessageId] ASC )) ON [PRIMARY]
) ON [PRIMARY]

Там 1-1 отношение между этими таблицами.

Таблица сообщений для хранения содержимого сообщения. Фактическое содержимое (To, CC, BCC, Subject, Body и т. Д.) Сериализуется в поле Graph в формате XML. Другое From, To информация просто используется для сообщения о проблемах без десериализации графа. Разделение этой таблицы позволяет разбить содержимое таблицы на другое дисковое хранилище. Когда вы готовы отправить сообщение, вам нужно прочитать всю информацию, поэтому нет ничего плохого в сериализации всего контента в один столбец с индексом первичного ключа.

Таблица MessageState для хранения состояния содержимого сообщения с дополнительной информацией на основе даты. Разделение этой таблицы позволяет использовать механизм быстрого доступа с дополнительными индексами для быстрого ввода-вывода. Другие столбцы уже говорят сами за себя.

Вы можете использовать отдельный пул потоков, который сканирует эти таблицы. Если приложение и пул находятся на одной и той же машине, вы можете использовать класс EventWaitHandle, чтобы сообщить пулу из приложения о том, что эти таблицы вставлены, в противном случае периодическое сканирование с таймаутом является лучшим.

Ertan
источник