通过.NET

时间:2016-05-24 11:44:37

标签: c# .net 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();
    }
}

效果不佳,很可能是因为我在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次插入/秒。没有使用投影管道进行测试,而是单独测试
  • 插入后不TableAdapter的ADO.NET SELECT的单线程插入:~3000插入/秒
    • 10.84行的单线程ADO.NET TableAdapter批量插入(单个数据集,内存中10,000行):> 10.000次插入/秒(我的样本大小和窗口太小)

3 个答案:

答案 0 :(得分:2)

作为Projac的作者,我建议您查看它提供的内容,并窃取合适的内容。我专门构建它是因为LINQ / EF在读取模型/投影方面的选择很差......

答案 1 :(得分:1)

一次将一条记录保存到SQL Server总是会表现不佳。你有两个选择;

  1. 表变量参数
  2. 使用表变量在单个调用中将多个记录保存到存储过程

    1. ADO批量复制
    2. 使用批量插入ADO库批量复制

      中的数据

      除了连接处理之外,其中任何一个都不会受益于EF。

      如果您的数据是简单的键值对,我也不会这样做;使用RDBMS可能不太适合。可能Mongo \ Raven或其他平面数据存储的性能会更好。

答案 2 :(得分:1)

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

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

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

  • 对POCO的有序,排队和并行反序列化是通过TPL DataFlow 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一切。