通过.NET提高事件源预测到RDBMS(SQL)的性能

urb*_*sky 6 .net c# rdbms projection event-sourcing

我目前正在研究C#中使用CQRS和事件源的原型,并且在我对SQL数据库的预测中遇到了性能瓶颈.

我的第一个原型是使用Entity Framework 6构建的,代码优先.这个选择主要是为了开始,因为读取方将受益于LINQ.

每个(适用的)事件都由多个投影使用,这些投影可以创建或更新相应的实体.

这样的投影目前看起来像这样:

public async Task HandleAsync(ItemPlacedIntoStock @event)
{
    var bookingList = new BookingList();
    bookingList.Date = @event.Date;
    bookingList.DeltaItemQuantity = @event.Quantity;
    bookingList.IncomingItemQuantity = @event.Quantity;
    bookingList.OutgoingItemQuantity = 0;
    bookingList.Item = @event.Item;
    bookingList.Location = @event.Location;
    bookingList.Warehouse = @event.Warehouse;

    using (var repository = new BookingListRepository())
    {
        repository.Add(bookingList);
        await repository.Save();
    }
}
Run Code Online (Sandbox Code Playgroud)

这不是很好的表现,很可能是因为我调用DbContext.SaveChanges()了这个IRepository.Save()方法.每个活动一个.

我接下来应该探索哪些选择?我不想花几天时间追逐那些可能只是稍微好一点的想法.

我目前看到以下选项:

  • 坚持使用EF,但只要投影在后面运行,就可以批处理事件(即每X次事件的新/保存上下文).
  • 尝试执行更多低级SQL,例如使用ADO.NET.
  • 不要使用SQL来存储投影(即使用NoSQL)

我希望看到数百万个事件,因为我们计划采用大型遗留应用程序并以事件的形式迁移数据.新的预测也会经常添加,因此处理速度是一个实际问题.

基准:

  • 当前的解决方案(EF,在每个事件之后保存)每秒处理~200个事件(每个投影).它不直接与活动投影的数量成比例(即N个投影处理小于N*200个事件/秒).
  • 当预测没有保存上下文时,事件/秒的数量略有增加(小于两倍)
  • 当投影没有做任何事情(单一返回语句)时,我的原型管道的处理速度是全局~30,000事件/秒

更新的基准

  • 通过ADO.NET进行单线程插入TableAdapter(每次迭代时都是新的DataSet和新的TableAdapter):~2.500次插入/秒.没有使用投影管道进行测试,而是单独测试
  • 通过ADO.NET的单线程插入TableAdapter,SELECT插入后不插入:~3000插入/秒
    • TableAdapter10.84行的单线程ADO.NET 批量插入(单个数据集,内存中10.000行):> 10.000次插入/秒(我的样本大小和窗口太小)

urb*_*sky 5

在批量提交和改进我的整体投影引擎时,即使使用实体框架,我也看到了几个数量级的性能改进。

  • 每个投影都是 Event Store 上的单独订阅。这允许每个投影以其最大速度运行。我的机器上管道的理论最大值是每秒 40,000 个事件(可能更多,我没有可供采样的事件)
  • 每个投影维护一个事件队列并将 json 反序列化为 POCO。每个投影的多个反序列化并行运行。还从数据契约序列化切换到 json.net。
  • 每个投影都支持工作单元的概念。在处理 1000 个事件后或者如果反序列化队列为空(即我位于头部位置或经历了缓冲区欠载),则提交工作单元。这意味着如果仅落后几个事件,则投影会更频繁地提交。
  • 使用异步 TPL 处理,并交错获取、排队、处理、跟踪和提交。

这是通过使用以下技术和工具实现的:

  • POCO 中的有序、排队和并行反序列化是通过 TPL 数据流完成的,TransformBlock并行BoundedCapacity度超过 100。最大并行度为Environment.ProcessorCount(即 4 或 8)。我看到队列大小为 100-200 与 10 时性能大幅提高:从每秒 200-300 个事件增加到 10,000 个事件。这很可能意味着 10 的缓冲区导致过多的欠载,从而过于频繁地提交工作单元。
  • 处理是从链接异步调度的ActionBlock
  • 每次反序列化事件时,我都会增加待处理事件的计数器
  • 每次处理事件时,我都会增加已处理事件的计数器
  • 在处理 1000 个事件后,或者每当反序列化缓冲区耗尽时(待处理事件数 = 已处理事件数),就会提交工作单元。我将两个计数器都减少了已处理事件的数量。我不会将它们重置为 0,因为其他线程可能增加了待处理事件的数量。

批量大小为 1000 个事件、队列大小为 200 的值是实验结果。这还显示了通过独立调整每个投影的这些值来进一步改进的选项。当使用 10.000 的批量大小时,为每个事件添加新行的投影会显着减慢 - 而仅更新一些实体的其他投影则受益于较大的批量大小。

反序列化队列大小对于良好的性能也至关重要。

所以,TL;DR:

实体框架的速度足够快,每秒可以处理多达 10,000 次修改 - 每个并行线程。利用您的工作单元并避免提交每一个更改 - 尤其是在 CQRS 中,其中投影是对数据进行任何更改的唯一线程。适当地交错并行任务,不要盲目地把async所有事情都做完。