当网站部署在多个服务器上时,每分钟运行自动化任务的最佳方式是什么?

7
我需要设置一个自动化任务,每分钟运行一次并发送邮件到队列中。我正在使用ASP.NET 4.5和C#。目前,我使用在global.asax中启动的调度程序类,并利用缓存和缓存回调。据我所知,这会导致多个问题。
我这样做的原因是该应用运行在多个负载均衡服务器上,这使我可以在一个地方执行代码,即使一个或多个服务器离线,代码也会运行。
我正在寻找一些指导,以使其更好。我已经阅读了有关Quartz.NET的文章,但从未使用过。Quartz.NET是从应用程序还是从Windows服务或Web服务调用方法?
我也阅读过使用Windows服务的文章,但据我所知,它们直接安装在服务器上。问题是,无论在线服务器的数量如何,我都需要任务执行,并且不想复制它。例如,如果我在服务器1和服务器2上设置了计划任务,它们将同时运行,从而重复请求。但是,如果服务器1下线,我需要服务器2运行该任务。
有关如何继续进行的任何建议,或者全局.asax方法是否是多服务器环境的最佳方法?顺便说一下,Web服务器正在运行Win Server 2012和IIS 8。
编辑
在请求更多信息时,队列存储在数据库中。我也应该提到,数据库服务器与Web服务器是分开的。有两个数据库服务器,但只有一个在运行。它们都从中央存储读取,因此只有一个数据库实例。当一个数据库服务器关闭时,另一个就上线了。
那么,在两个数据库服务器上部署Windows服务是否更合理?这将确保一次只运行一个服务。
此外,你对从应用程序运行Quartz.NET有何看法?正如millimoose所提到的,我不一定需要它运行在Web前端,然而,这样做可以避免在多台机器上部署Windows服务,而且我认为无论哪种方式,性能都不会有差异。 你有什么想法?
感谢大家迄今为止的贡献。如果需要任何其他信息,请告诉我。

Quartz.NET 可以在 ASP.NET 应用程序或 Windows 服务中运行。 - Jace Rhea
这听起来像是您想要一个单独的服务来处理发送电子邮件的排队,而不是将其嵌入到您的 Web 前端中。(然后可以通过编排一堆工作者来进行负载平衡。) - millimoose
2个回答

8

我曾经处理过你现在面临的确切问题。

首先,你必须意识到,在ASP.NET中无法可靠地运行长时间运行的进程。如果你从global.asax实例化调度器类,那么你就无法控制该类的生命周期。

换句话说,IIS可能会随时回收托管你类的工作进程。最好的情况是,你的类将被销毁(你无能为力)。最坏的情况是,在进行工作的过程中,你的类将被终止。糟糕透了。

运行长期进程的适当方法是在计算机上安装Windows服务。我会在每个Web服务器上安装服务,而不是在数据库上安装。

服务实例化Quartz调度程序。这样,你就知道只要机器开着,你的调度程序就保证会继续运行。当需要运行作业时,Quartz只需调用你指定的IJob类的一个方法即可。

class EmailSender : Quartz.IJob
{
    public void Execute(JobExecutionContext context)
    {
        // send your emails here
    }
}

请记住,Quartz在单独的线程上调用Execute方法,因此您必须小心保证线程安全。
当然,现在您将在多台机器上运行相同的服务。虽然听起来您很担心这个问题,但实际上您可以将其转化为积极的事情!
我所做的是在我的数据库中添加了一个"锁定"列。当发送作业执行时,它通过设置锁定列来锁定队列中特定的电子邮件。例如,当作业执行时,生成一个GUID,然后:
UPDATE EmailQueue SET Lock=someGuid WHERE Lock IS NULL LIMIT 1;
SELECT * FROM EmailQueue WHERE Lock=someGuid;

以这种方式,您让数据库服务器处理并发。 UPDATE 查询告诉 DB 将队列中的一个电子邮件(当前未分配)分配给当前实例。 然后 SELECT 锁定的电子邮件并发送它。 发送后,从队列中删除电子邮件(或者按照您处理已发送电子邮件的方式),并重复该过程,直到队列为空。
现在您可以朝两个方向进行扩展:
  • 通过同时运行多个线程来运行相同的作业。
  • 由于此代码正在多台计算机上运行,因此您有效地在所有服务器上平衡了发送工作负载。
由于锁定机制,即使多个线程在多台计算机上运行相同的代码,您也可以保证队列中的每个电子邮件仅发送一次。

回应评论: 我最终实现的方法有几个不同之处。

首先,我的ASP应用程序可以通知服务队列中有新电子邮件。这意味着我甚至不必按计划运行,我只需告诉服务何时开始工作即可。然而,在分布式环境中,这种通知机制非常难以正确实现,因此每分钟检查一次队列应该就可以了。

你选择的间隔时间取决于电子邮件传递的时间敏感性。如果需要尽快发送电子邮件,则可能需要每30秒或更短时间触发一次。如果不那么紧急,则可以每5分钟检查一次。Quartz限制同时执行的作业数量(可配置),您可以配置如果触发器被错过应该发生什么,因此您不必担心有数百个作业积压。

其次,我实际上一次锁定5封电子邮件,以减少对DB服务器的查询负载。由于处理的数据量很大,因此这有助于提高效率(服务和DB之间的网络往返次数更少)。需要注意的是,如果在发送一组电子邮件时某个节点(无论出于什么原因,从异常到机器崩溃)意外关闭,则会在DB中留下“锁定”行,并且没有任何服务可以为其提供服务。组的大小越大,风险就越大。此外,如果所有剩余电子邮件都被锁定,则空闲节点显然无法处理任何内容。
至于线程安全性,我指的是一般意义。Quartz维护一个线程池,因此您不必担心实际管理线程本身。
您必须小心您的作业中的代码所访问的内容。作为经验法则,局部变量应该没问题。但是,如果您访问函数范围之外的任何内容,则线程安全性是一个真正的问题。例如:
class EmailSender : IJob {
    static int counter = 0;

    public void Execute(JobExecutionContext context) {
        counter++; // BAD!
    }
}

这段代码不是线程安全的,因为多个线程可能同时尝试访问counter

Thread A           Thread B
Execute()
                   Execute()
Get counter (0)
                   Get counter (0)
Increment (1)
                   Increment (1)
Store value
                   Store value

            counter = 1 

counter应该是2,但是我们却遇到了一个极难调试的竞态条件。下一次运行这段代码时,可能会出现以下情况:

Thread A           Thread B
Execute()
                   Execute()
Get counter (0)
Increment (1)
Store value
                   Get counter (1)
                   Increment (2)
                   Store value

            counter = 2

...而你只是一头雾水,不明白这次为什么可以正常工作。

在你的特定情况下,只要在每次调用Execute时创建一个新的数据库连接并且不访问任何全局数据结构,你就应该没问题。


这很有道理,我可以看出您使用多服务器部署的优势逻辑。以前,我会在一个查询中选择所有待处理的电子邮件,然后在循环中发送。在您的示例中,我会一次处理一个,使所有机器同时工作。在您的示例中,您建议任务执行的时间间隔是多少? - Ricketts
另外,您能详细说明一下什么是线程安全吗?Quartz在处理完后不应该自动终止线程吗?这种方法是否有可能达到最大线程数,从而降低应用程序的响应时间? - Ricketts
+1 对于我不久前的一个项目,Quartz 运行得非常好。 - Eonasdan
刚刚完成了这个方法的部署,它运行得非常好!我也采用了每次发送5封电子邮件的方法。为了解决处理错误问题,我还添加了一个DateLocked字段。在每次执行结束时,我会检查状态为待处理、已锁定且已锁定超过5分钟的记录。如果找到任何记录,这意味着它们在执行期间发生了某些事情,因此将其解锁,以便在下一次执行和重新发送时可以捡起来。我还设置了一个“FreezeQueue”指示器,在出现关键错误(例如模式更改)的情况下停止所有执行。感谢您的帮助! - Ricketts
我想要补充一点,这是一个非常经过深思熟虑的回答。我也完全同意,在数据库服务器上运行此类服务几乎总是一个坏主意。最好让数据库服务器做它擅长的事情。再次感谢您详细的回答。 - Rob

1

关于你的架构,需要更具体的说明。电子邮件队列在哪里; 在内存中还是数据库中?如果它们存在于数据库中,您可以有一个名为“processing”的标志列,当任务从队列中抓取电子邮件时,仅抓取当前未处理的电子邮件,并为其抓取的电子邮件设置处理标志为true。然后您将并发问题留给数据库。


4
应该在评论中提出澄清问题,而不是作为答案发布。 - Servy
我认为你的编辑是正确的。创建一个带有定时器线程的Windows服务,其回调函数检查要处理的电子邮件数据库。不过,我建议你添加一个处理列。我曾经构建了一个高度使用的电子邮件系统,没有某种标志,随着邮件数量的增加,有时我的定时器线程会在所有电子邮件完成处理之前再次回调,然后获取相同的电子邮件并开始处理它们N次... - pwnyexpress

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接