通过 .NET 提高 RDBMS (SQL) 的事件源预测性能
Improve performance of event sourcing projections to RDBMS (SQL) via .NET
我目前正在使用 C# 开发一个使用 CQRS 和事件源的原型,我在 SQL 数据库的预测中遇到了性能瓶颈。
我的第一个原型是用 Entity Framework6 构建的,代码优先。做出这个选择主要是为了开始,因为读取端将从 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();
}
}
这不是很好,很可能是因为我在 IRepository.Save()
方法中调用了 DbContext.SaveChanges()
。每个事件一个。
接下来我应该探索哪些选项?我不想花几天时间追逐可能被证明只是稍微好一点的想法。
我目前看到以下选项:
- 坚持使用 EF,但批处理事件(即 new/save 每 X 个事件的上下文)只要投影落后 运行。
- 尝试做更多低级 SQL,例如 ADO.NET。
- 不要使用 SQL 来存储投影(即使用 NoSQL)
我预计会看到数百万个事件,因为我们计划获取大型遗留应用程序的来源并以事件的形式迁移数据。新投影也将经常添加,因此处理速度是一个实际问题。
基准:
- 当前解决方案(EF,在每个事件后保存)每秒处理约 200 个事件(每个投影)。它不直接与活动投影的数量成比例(即 N 个投影过程小于 N * 200 events/second)。
- 当投影不保存上下文时,events/second 的数量略有增加(小于两倍)
- 当投影什么都不做时(单个 return 语句),我的原型管道的处理速度是 ~30.000 events/second 全局
更新基准
- 通过 ADO.NET
TableAdapter
的单线程插入(每次迭代都会有新的 DataSet
和新的 TableAdapter
):~2.500 inserts/second。没有使用投影管道进行测试,而是独立测试
- 通过 ADO.NET
TableAdapter
的单线程插入,插入后不会 SELECT
:~3.000 inserts/second
- 单线程 ADO.NET
TableAdapter
批量插入 10.000 行(单个数据集,内存中 10.000 行):>10.000 inserts/second(我的样本大小和 window 太小了)
一次将一条记录保存到 SQL 服务器的性能总是很差。您有两个选择;
- Table 变量参数
使用 table 变量在一次调用中将多条记录保存到存储过程
- ADO 批量复制
使用Bulk Insert ADO库批量复制数据到
除了连接处理之外,它们都不会从 EF 中受益。
如果您的数据是简单的键值对,我不会做任何事情;使用 RDBMS 可能不太合适。可能 Mongo\Raven 或其他平面数据存储性能更好。
作为 Projac 的作者,我建议您看看它提供的内容,并窃取您认为合适的内容。我专门构建它是因为 LINQ/EF 在读取 model/projection 方面是糟糕的选择 ...
即使使用 Entity Framework,当 批处理提交 并改进我的整体投影引擎时,我已经看到了几个数量级的性能改进。
- 每个投影都是 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 的批量大小时,为每个事件添加新行的投影速度会大大降低 - 而其他仅更新几个实体的投影受益于更大的批量大小。
反序列化队列大小对于良好的性能也至关重要。
所以,长话短说:
Entity framework 速度足以每秒处理多达 10.000 次修改 - 在并行线程上,每个。
利用您的工作单元并避免提交每一个更改——尤其是在 CQRS 中,其中投影是对数据进行任何更改的唯一线程。
适当交错并行任务,不要盲目async
什么都
我目前正在使用 C# 开发一个使用 CQRS 和事件源的原型,我在 SQL 数据库的预测中遇到了性能瓶颈。
我的第一个原型是用 Entity Framework6 构建的,代码优先。做出这个选择主要是为了开始,因为读取端将从 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();
}
}
这不是很好,很可能是因为我在 IRepository.Save()
方法中调用了 DbContext.SaveChanges()
。每个事件一个。
接下来我应该探索哪些选项?我不想花几天时间追逐可能被证明只是稍微好一点的想法。
我目前看到以下选项:
- 坚持使用 EF,但批处理事件(即 new/save 每 X 个事件的上下文)只要投影落后 运行。
- 尝试做更多低级 SQL,例如 ADO.NET。
- 不要使用 SQL 来存储投影(即使用 NoSQL)
我预计会看到数百万个事件,因为我们计划获取大型遗留应用程序的来源并以事件的形式迁移数据。新投影也将经常添加,因此处理速度是一个实际问题。
基准:
- 当前解决方案(EF,在每个事件后保存)每秒处理约 200 个事件(每个投影)。它不直接与活动投影的数量成比例(即 N 个投影过程小于 N * 200 events/second)。
- 当投影不保存上下文时,events/second 的数量略有增加(小于两倍)
- 当投影什么都不做时(单个 return 语句),我的原型管道的处理速度是 ~30.000 events/second 全局
更新基准
- 通过 ADO.NET
TableAdapter
的单线程插入(每次迭代都会有新的DataSet
和新的TableAdapter
):~2.500 inserts/second。没有使用投影管道进行测试,而是独立测试 - 通过 ADO.NET
TableAdapter
的单线程插入,插入后不会SELECT
:~3.000 inserts/second- 单线程 ADO.NET
TableAdapter
批量插入 10.000 行(单个数据集,内存中 10.000 行):>10.000 inserts/second(我的样本大小和 window 太小了)
- 单线程 ADO.NET
一次将一条记录保存到 SQL 服务器的性能总是很差。您有两个选择;
- Table 变量参数
使用 table 变量在一次调用中将多条记录保存到存储过程
- ADO 批量复制
使用Bulk Insert ADO库批量复制数据到
除了连接处理之外,它们都不会从 EF 中受益。
如果您的数据是简单的键值对,我不会做任何事情;使用 RDBMS 可能不太合适。可能 Mongo\Raven 或其他平面数据存储性能更好。
作为 Projac 的作者,我建议您看看它提供的内容,并窃取您认为合适的内容。我专门构建它是因为 LINQ/EF 在读取 model/projection 方面是糟糕的选择 ...
即使使用 Entity Framework,当 批处理提交 并改进我的整体投影引擎时,我已经看到了几个数量级的性能改进。
- 每个投影都是 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 的批量大小时,为每个事件添加新行的投影速度会大大降低 - 而其他仅更新几个实体的投影受益于更大的批量大小。
反序列化队列大小对于良好的性能也至关重要。
所以,长话短说:
Entity framework 速度足以每秒处理多达 10.000 次修改 - 在并行线程上,每个。
利用您的工作单元并避免提交每一个更改——尤其是在 CQRS 中,其中投影是对数据进行任何更改的唯一线程。
适当交错并行任务,不要盲目async
什么都