如何管理从Web应用程序发送的自动电子邮件


12

我正在设计一个Web应用程序,并且想知道如何设计用于管理自动电子邮件发送的体系结构。

目前,我已将此功能内置到我的Web应用程序中,并且根据用户输入/交互(例如创建新用户)发送电子邮件。问题是直接连接到邮件服务器需要花费几秒钟的时间。扩大我的应用程序,这将是未来的重大瓶颈。

在我的系统体系结构中管理发送大量自动电子邮件的最佳方法是什么?

不会发送大量电子邮件(每天最多20​​00个)。电子邮件无需立即发送,最多可以延迟10分钟。

更新:消息队列已作为答案,但是如何设计?是否可以在应用程序中处理并在安静的时间段进行处理,还是我需要创建一个新的“邮件应用程序”或网络服务来仅管理队列?


您能给我们一个大概的规模感吗?成百上千的邮件?另外,应该立即发送电子邮件还是可以接受小的延迟?
yannis 2013年

发送电子邮件涉及将SMTP消息移交给接收邮件的主机,但这并不意味着该消息实际上已被传递。如此有效,所有电子邮件发送都是异步的,并且没有必要假装“等待成功”。
Kilian Foth 2013年

1
我不是“等待成功”,而是必须等待smtp服务器接受我的请求。@YannisRizos看到更新RE您的评论
Gaz_Edge

对于2000(这是您所描述的最大值)邮件,它将正常工作。当它们发生在10个工作小时内时,这是每分钟3封邮件,这是非常可行的。只要确保您设置好DNS记录,提供商就会接受您以这些金额发送它们。还要考虑:“什么是邮件服务器故障?”。不必担心发送2000封邮件的负担。
Luc Franken

CRONTAB的答案在哪里
TulainsCórdova13年

Answers:


15

正如Ozz已经提到的那样,常见的方法是消息队列。从设计的角度来看,消息队列实质上是FIFO队列,这是一种非常基本的数据类型:

FIFO队列

使消息队列与众不同的原因是,虽然您的应用程序负责排队,但另一个进程将负责排队。在对语言进行排队时,您的应用程序是消息的发送者,而出队列过程是接收者。明显的优点是,整个过程是异步的,只要有消息要处理,接收者就独立于发送者工作。明显的缺点是您需要一个额外的组件,即发送者,才能使整个工作正常进行。

由于您的体系结构现在依赖于交换消息的两个组件,因此您可以为其使用幻想术语“ 进程间通信 ”。

引入队列如何影响您的应用程序设计?

您的应用程序中的某些操作会生成电子邮件。引入消息队列将意味着这些操作现在应改为将消息推送到队列(仅此而已)。这些消息应携带您的收件人处理邮件时构造电子邮件所必需的绝对最少信息量。

消息的格式和内容

消息的格式和内容完全取决于您,但是请记住,消息越小越好。您的队列应尽可能快地进行写入和处理,将大量数据丢给队列可能会造成瓶颈。

此外,一些基于云的排队服务对消息大小有限制,并且可能会拆分较大的消息。您不会注意到,当您要求拆分消息时,它们将被当作一个消息使用,但是您需要为多条消息付费(假设您使用的是收费服务)。

接收器设计

因为我们在谈论Web应用程序,所以对于您的接收者来说,一种通用的方法是使用简单的cron脚本。它每x分钟(或几秒钟)运行一次,它将:

  • 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到更完整的说明(我确实设法用隐秘的方式(reason而不是一次完整的消息)向客户发送了大约500封电子邮件)。

进一步阅读

标准:

工具:

有趣的读物:


哇。差不多是我在这里收到的最佳答案!感激不尽!
Gaz_Edge 2013年

我,并确保数百万其他人将此FIFO与Gmail和Google Apps脚本一起使用。Gmail过滤器会根据条件标记所有传入的邮件,然后将所有邮件排队。Google Apps脚本每隔X个持续时间运行一次,获取前y条消息,发送它们,然后使它们出队。冲洗并重复。
DavChana

6

您需要某种排队系统。

一种简单的方法是写入数据库表,并在该表中让另一个外部应用程序处理行,但是您可以使用许多其他排队技术。

您可能对电子邮件很重要,因此某些电子邮件几乎可以立即采取措施(例如,密码重置),而次要的电子邮件则可以分批处理以便以后发送。


您是否有架构图或示例显示其工作原理?例如,队列位于另一个“应用程序”(例如邮件应用程序)中,还是在安静时段从Web应用程序中获取进程。还是我应该创建一种Web服务来处理它们?
Gaz_Edge 2013年

1
@Gaz_Edge您的应用程序将项目推送到队列。后台进程(最有可能是cron脚本)每隔n秒从队列中弹出x个项目并进行处理(在您的情况下,发送电子邮件)。单个数据库表可以很好地用作少量项目的队列存储,但是通常来讲,对数据库的写操作非常昂贵,对于较大数量的数据,您可能希望查看Amazon SQS之的服务
yannis 2013年

1
@Gaz_Edge我不确定我能以比写“ ...写入数据库表并在此表中有另一个外部应用程序处理行...”更简单的方式来绘制它,对于表,请阅读“任何队列无论采用哪种技术。
ozz 2013年

1
(续...)您可以构建后台进程,以考虑流量的方式来清除队列,例如,可以指示它在服务器承受压力时处理更少的项目(或根本不处理任何项目) 。您要么通过查看过去的流量数据(比听起来容易些,但有很大的误差)来预测那些压力大的时间,要么让后台进程每次运行时检查流量状态(更准确,但很少需要增加额外的开销)。
扬尼斯,2013年

@YannisRizos希望将您的评论合并为答案?此外,架构图和设计也将有所帮助(这次我决心从这个问题中解决它们!;
Gaz_Edge 2013年

2

不会发送大量电子邮件(每天最多20​​00个)。

除了队列之外,您还应该考虑的第二件事是通过专用服务发送电子邮件:例如MailChimp(我不隶属于该服务)。否则,许多邮件服务(例如gmail)很快就会将您的信件发送到垃圾邮件文件夹中。


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关系。

消息表,用于存储消息内容。实际内容(收件人,抄送,密件抄送,主题,正文等)以XML格式序列化到“图形”字段。Other From,To信息仅用于报告问题,而无需反序列化图形。分离该表可以将表内容分区到其他磁盘存储中。一旦准备好发送消息,就需要阅读所有信息,因此将所有内容序列化为具有主键索引的一列都没有问题。

MessageState表用于存储消息内容的状态以及其他基于日期的信息。分离此表可以使用快速IO存储上的其他索引建立快速访问机制。其他专栏已经很容易说明了。

您可以使用一个单独的线程池来扫描该表。如果应用程序和池位于同一台计算机上,则可以使用EventWaitHandle类从应用程序向池发出有关插入这些表的内容的信号,否则最好定期进行超时扫描。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.