起因
微软在Build2021开发者大会上,发布Entity Framework Core 6.0(简称EFCore 6)预览第四版,号称是性能版本,性能提升主要对于Entity Framework Core 5.
性能改进:
EFCore 5.0和EFCore 6.0基准测试,提升了70%.
在查询时,比EFCore5.0提升了31%.
内存改进了不少,减少43%.减少内存分配,就是减少GC回收的次数,降低GC回收的压力.
运行时性能
在以往的几年,EF Core团队主要以功能和稳定为中心,缩小与EF(Entity Framework)的功能差距.当EF Core团队在给EF Core 6.0规划时以性能(高性能和低延迟)为中心.EF Core 6.0以Dapper(这里不给Dapper过多的介绍)为目标.
在发布EF Core 6.0 pre 4的时候,EF Core 6与Dapper在TechEmpower基准测试中的差距由原来的55%,缩小到5% .
当然在TechEmpower基准测试是特定的环境(有针对性的优化,比如加大DbContext池的默认大小改为1024),基准测试是在高性能和低延迟的环境下执行的,EF Core也关闭了跟踪查询(关闭后,无法对实体进行修改),这些和线上生产环境不同.因为EF Core在执行查询时(在线上生产环境)的性能瓶颈,主要是在网络和数据库的IO上.
DbContext创建和回收
使用EF Core的人都知道DbContext是操作数据的入口,通常直接实例化DbContext,或者通过依赖注入的方式获得DbContext的实例,使用它进行一些数据库操作(工作单元),使用结束后进行释放.
通常直接实例化DbContext是一个很好的方式.在有些环境需要和容器(依赖注入)集成到一起,又需要高性能的时候,协调DbContext和容器一起工作是需要时间的.所以在EF Core团队加入DbContextPool,对DbContext进行池化管理.DbContext在使用完毕后放入到DbContextPool允许被重新使用.
通过DbContextPool(DbContext池)减少DbContext创建和回收,减少DbContext的创建,降低堆内存分配,也就减少GC回收的次数.减少GC的压力.
在EF Core 6.0调整池中的实例数量,从原来128个改为1024个.对于大多数程序128个DbContext实例是足够使用的.但在TechEmpower 测试是有些不太够,将实例数量改为1024,TechEmpower 基准吞吐率增加了23%.
DbContext不是万能的,EF Core在查询时是要使用Ado.Net对象的,如DbConnection/DbCommand及DbDataReader,以及使用这些对象中的内部对象,但这些对象在内存使用比较高时,就需要对这些对象进行回收,这些对象回收是有序的.因此,每个DbContext都有专属的自己实例(指DbConnection等),重用的时候也是使用这些对象.这种可重用的对象图以DbContext为根节点延伸到Npgsql(PostgreSQL),这次优化在执行查询时内存分配减少了23%.
日志调整
EF Core有很多可以扩展的地方,允许用户获得关于查询执行的各个阶段的信息,并将其挂钩到查询执行的各个阶段,比如说要对数据库执行Sql查询时,EF Core调用DbCommand.ExcuteReaderAsync(可以记录Sql执行前和运行的时间),也可以写一个DiagnosticSource事件,调用用户配置的拦截器,可以让用户操作之前的命令,EF Core虽然提供了一组灵活切强大的扩展,但这些扩展的开销并不低.一次查询有7个事件,每个事件都有2个扩展(执行前和执行后).一直在检查是否启用日志和注册DiagnosticListener 事件,这些开销都可以在性能分析中体现出.
在日志这一块改进,是检查是否开启任何类型的日志或者拦截器,如果没有启用,默认就对该事件和日志禁用1秒.禁用后,基准吞吐率提升了7%,这为使用EF Core的用户,带来了很好的收益,如果在程序的某一时间注册DiagnosticListener,那么只需要等待一秒钟的时间,就可以看到结果.
线程安全检查
在EF Core中DbContext并不是线程安全的,DbContext内部封装了数据库连接对象,数据库连接对象本身也不允许并发使用.所以我们对DbContext的实例对象不应该在有并发的时候使用,但在EF Core内部有一个线程安全检测机制,但检测到有跨线程使用,会抛出异常,这样方便解决DbContext跨线程使用带来的问题.
EF Core在查询时线程安全机制也是会进行检测的,在异步查询的时候,会使用AsyncLocal将状态加锁并传递给查询线程,AsyncLocal会在堆上进行比较多的内存分配,有太多的堆内存分配,导致GC也会进行频繁回收,最终导致EF Core的吞吐率下降.
EF Core不知道是什么时候检测是否线程安全,也不知道什么时候不需要进行线程安全,在EF Core 6.0加入了线程安全检查机制的关闭参数,通过DbContextOptionsBuilder的EnableThreadSafetyChecks方法进行设置.本想在测试项目测试一下呢?发现在EF Core 6.0 preview 4的具体数据库的实现依赖.都还是EF Core 5.0的.只有Npgsql.EntityFrameworkCore.PostgreSQL内部依赖有EF Core 6.0的,SqlServer和MySQL暂时没有更新.
qqio
在关闭线程安全检测后,在TechEmpower 性能基准测试提升了6.7%.