我发现后端邮件处理中出现了共同模式:
ServiceA 会生成大量邮件。
ServiceB 一次处理一条消息。
ServiceC 发出对数据库或Web服务调用的调用,这样可以通过批量调用获得显着的性能和可靠性。
在某些情况下,预先批量处理来自ServiceA的消息或处理ServiceB中的批处理消息是不可行的,因此首选项将是单独处理所有消息,直到在ServiceC上进行最终调用。这需要在ServiceC呼叫之前进行批处理步骤。
看似理想的是拥有一个NServiceBus处理程序签名,该签名可选择按批处理方式传递消息,如:
public void Handle(FooMessage[] messageBatch)
{
}
在处理程序执行之前,messageBatch中的所有消息都不会被提交。
NServiceBus似乎没有本机支持。我可以在队列中一次处理消息并写入内存直到批量刷新。但在这种情况下,邮件会在刷新之前提交,如果进程崩溃,我们也不会保留批处理中所有邮件的传递保证。
所以问题是:由于某种原因我不会想到这是一个糟糕的模式吗?我知道知道何时刷新批次存在一个固有的问题,但似乎至少有一些传输实现缓冲了已经在引擎盖下的批处理中的消息,并且只是一次一个地交付。在这个级别进行批处理或者为定期刷新设置一个简单的超时似乎会起作用。
是否有解决方法或缺少的首选模式?
答案 0 :(得分:13)
Upfront / Disclaimer:我为NServiceBus的制造商Particular Software工作。我还写了Learning NServiceBus。
在我为特工工作之前,我曾经发现自己处于你的确切状况。我有一种分析类型的情况,其中12个Web服务器通过MSMQ发送相同类型的命令以指示文章被查看。需要在数据库中跟踪这些计数,以便最常用的#34;可以基于视图的数量生成列表。但是每个页面视图中的插入都不能很好地执行,所以我介绍了服务总线。
使用表值参数,插入器一次最多可以插入50-100,但是NServiceBus在事务中一次只能给你一条消息。
在NServiceBus中,对多个消息进行操作的任何内容通常都需要使用Saga。 (Saga基本上是一堆相关的消息处理程序,它们在处理每条消息之间保持一些存储状态。)
但是Saga必须将其数据存储在某个地方,这通常意味着数据库。所以,让我们进行比较:
因此,佐贺使得#34;持久性负载"更糟糕的是。
当然,您可以选择为Saga使用内存中持久性。这将为您提供批处理而无需额外的持久性开销,但如果Saga端点崩溃,您可能会丢失部分批处理。因此,如果您不能轻易丢失数据,那么这不是一种选择。
所以即使在几年前,我也想象过这样的事情:
// Not a real NServiceBus thing! Only exists in my imagination!
public interface IHandleMessageBatches<TMessage>
{
void Handle(TMessage[] messages);
int MaxBatchSize { get; }
}
这个想法是,如果消息传输可以提前查看并看到许多消息可用,它可以开始接收MaxBatchSize并且您可以立即获取所有消息。当然,如果队列中只有一条消息,那么您将得到一个包含1条消息的数组。
更新版本中的代码更加模块化,很大程度上是因为现在支持多个消息传输。但是,仍然假设一次处理一条消息。
进入V6的当前实现位于IPushMessages
接口。在Initialize
方法中,Core将Func<PushContext, Task> pipe
推送到传输的IPushMessages
实现中。
或者用英语,&#34;嗨运输,当您有可用信息时,执行此操作将其交给核心,我们将从那里接收。&#34;
简而言之,这是因为NServiceBus适用于一次一条消息的可靠处理。从更详细的角度来看,有很多理由说明为什么批量接收会很困难:
SuperMessage
继承BaseMessage
,则两种类型的处理程序可以在同一消息上运行。在考虑一批消息时,多个处理程序和多态消息处理程序的这种可能性变得非常复杂。Handle(BaseMessage[] batch)
但是进来的消息是从BaseMessage
继承的不同超类型怎么办?总而言之,将NServiceBus更改为接受批次将需要针对批次优化整个管道。单个消息(当前规范)将是一个专门的批处理,其中数组大小为1.
从本质上讲,这对于它所能提供的有限商业价值的改变来说风险太大了。
我发现每条消息单个插入并不像我想象的那么昂贵。有害的是,多个Web服务器上的多个线程一次尝试写入数据库,并且一直停留在RPC操作中,直到它完成。
当这些操作被序列化到队列中,并且有限的,设定数量的线程处理这些消息并以数据库可以处理的速率执行数据库插入时,事情往往在大多数情况下都非常顺利地运行。
另外,请仔细考虑您在数据库中执行的操作。现有行的更新比插入更便宜。在我的情况下,我真的只关心计数,并不需要每个页面视图的记录。因此,根据内容ID和5分钟时间窗口更新记录更便宜,并更新该记录的读取计数,而不是每次读取插入记录并强迫自己进入大量的聚合查询
如果这绝对不起作用,您需要考虑可以在可靠性方面做出哪些权衡。您可以使用具有内存持久性的Saga,但随后您可以(并且最有可能最终)丢失整个批次。根据您的使用情况,这很可能是可以接受的。
你也可以使用消息处理程序写入Redis,这比数据库便宜,然后让Saga更像调度程序,将批量数据迁移到数据库。你可以用Kafka或其他一些技术做类似的事情。在这些情况下,您可以自行决定确保所需的可靠性,并设置可以实现这一目标的工具。