实体框架异步操作需要十倍的时间才能完成


139

我有一个使用Entity Framework 6处理数据库的MVC站点,并且我一直在尝试对其进行更改,以使所有内容都作为异步控制器运行,并且对数据库的调用作为它们的异步对应对象(例如ToListAsync()而不是ToList())

我遇到的问题是,仅将查询更改为异步已使它们的运行速度变得异常慢。

以下代码从我的数据上下文中获取“相册”对象的集合,并转换为相当简单的数据库联接:

// Get the albums
var albums = await this.context.Albums
    .Where(x => x.Artist.ID == artist.ID)
    .ToListAsync();

这是创建的SQL:

exec sp_executesql N'SELECT 
[Extent1].[ID] AS [ID], 
[Extent1].[URL] AS [URL], 
[Extent1].[ASIN] AS [ASIN], 
[Extent1].[Title] AS [Title], 
[Extent1].[ReleaseDate] AS [ReleaseDate], 
[Extent1].[AccurateDay] AS [AccurateDay], 
[Extent1].[AccurateMonth] AS [AccurateMonth], 
[Extent1].[Type] AS [Type], 
[Extent1].[Tracks] AS [Tracks], 
[Extent1].[MainCredits] AS [MainCredits], 
[Extent1].[SupportingCredits] AS [SupportingCredits], 
[Extent1].[Description] AS [Description], 
[Extent1].[Image] AS [Image], 
[Extent1].[HasImage] AS [HasImage], 
[Extent1].[Created] AS [Created], 
[Extent1].[Artist_ID] AS [Artist_ID]
FROM [dbo].[Albums] AS [Extent1]
WHERE [Extent1].[Artist_ID] = @p__linq__0',N'@p__linq__0 int',@p__linq__0=134

事实并非如此,它不是一个非常复杂的查询,但是SQL Server运行它几乎需要6秒钟。SQL Server Profiler报告它需要5742毫秒才能完成。

如果我将代码更改为:

// Get the albums
var albums = this.context.Albums
    .Where(x => x.Artist.ID == artist.ID)
    .ToList();

然后生成了完全相同的SQL,但是根据SQL Server Profiler,它只需要474毫秒即可运行。

该数据库在“相册”表中大约有3500行,实际上不是很多,并且在“ Artist_ID”列上有一个索引,因此它应该非常快。

我知道异步有开销,但是让事情慢十倍对我来说似乎有点困难!我在哪里错了?


在我看来,这并不正确。如果使用相同的数据执行相同的查询,则SQL Server Profiler报告的执行时间应大致相同,因为异步是c#中发生的,而不是Sql中发生的。SQL Server甚至不知道您的C#代码是异步的
Khanh TO

第一次运行生成的查询时,编译查询(构建执行计划,...)的时间可能会更长一些,而从第二次开始,相同的查询可能会更快(SQL Server缓存查询),但是不应有太大差异。
庆到

3
您需要确定运行缓慢。在无限循环中运行查询。暂停调试器10次。它最经常停在哪里?发布包含外部代码的堆栈。
usr

1
看来问题出在我完全忘记的Image属性上。它是一个VARBINARY(MAX)列,因此势必会导致运行缓慢,但是仍然有点怪异的是,运行缓慢只会成为运行异步的问题。我已经重组了数据库,以使图像现在成为链接表的一部分,并且现在一切都变得更快了。
迪伦·帕里

1
问题可能是EF向ADO.NET发出了大量异步读取,以检索所有这些字节和行。这样,开销被放大了。由于您没有执行测量,因此我要求我们永远不会知道。问题似乎已经解决。
usr

Answers:


286

我发现这个问题非常有趣,尤其是因为我在asyncAdo.Net和EF 6的任何地方都在使用它。我希望有人对此问题做出解释,但并没有发生。因此,我尝试重现此问题。我希望你们中的一些人会发现这很有趣。

第一个好消息:我转载了它:)区别是巨大的。因子8 ...

第一结果

首先CommandBehavior,由于我读了一篇有关asyncAdo 的有趣文章,因此我怀疑与之相关的事情:

“由于非顺序访问模式必须存储整个行的数据,因此如果您正在从服务器读取大列(例如varbinary(MAX),varchar(MAX),nvarchar(MAX)或XML),则会导致问题)。”

我怀疑ToList()呼叫为be CommandBehavior.SequentialAccess和异步呼叫为CommandBehavior.Default(非顺序呼叫,这可能会引起问题)。因此,我下载了EF6的源代码,并在所有地方(CommandBehavior当然是在使用的地方)放置了断点。

结果:什么都没有。所有的调用都是通过CommandBehavior.Default....因此,我尝试进入EF代码以了解发生了什么...和.. ooouch ...我从未见过如此委派的代码,一切似乎都被懒惰地执行了...

所以我试图做一些分析以了解发生了什么...

我想我有...

这是用于创建我进行基准测试的表的模型,其中包含3500行,每行256 Kb随机数据varbinary(MAX)。(EF 6.1-CodeFirst- CodePlex):

public class TestContext : DbContext
{
    public TestContext()
        : base(@"Server=(localdb)\\v11.0;Integrated Security=true;Initial Catalog=BENCH") // Local instance
    {
    }
    public DbSet<TestItem> Items { get; set; }
}

public class TestItem
{
    public int ID { get; set; }
    public string Name { get; set; }
    public byte[] BinaryData { get; set; }
}

这是我用来创建测试数据和基准EF的代码。

using (TestContext db = new TestContext())
{
    if (!db.Items.Any())
    {
        foreach (int i in Enumerable.Range(0, 3500)) // Fill 3500 lines
        {
            byte[] dummyData = new byte[1 << 18];  // with 256 Kbyte
            new Random().NextBytes(dummyData);
            db.Items.Add(new TestItem() { Name = i.ToString(), BinaryData = dummyData });
        }
        await db.SaveChangesAsync();
    }
}

using (TestContext db = new TestContext())  // EF Warm Up
{
    var warmItUp = db.Items.FirstOrDefault();
    warmItUp = await db.Items.FirstOrDefaultAsync();
}

Stopwatch watch = new Stopwatch();
using (TestContext db = new TestContext())
{
    watch.Start();
    var testRegular = db.Items.ToList();
    watch.Stop();
    Console.WriteLine("non async : " + watch.ElapsedMilliseconds);
}

using (TestContext db = new TestContext())
{
    watch.Restart();
    var testAsync = await db.Items.ToListAsync();
    watch.Stop();
    Console.WriteLine("async : " + watch.ElapsedMilliseconds);
}

using (var connection = new SqlConnection(CS))
{
    await connection.OpenAsync();
    using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
    {
        watch.Restart();
        List<TestItem> itemsWithAdo = new List<TestItem>();
        var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess);
        while (await reader.ReadAsync())
        {
            var item = new TestItem();
            item.ID = (int)reader[0];
            item.Name = (String)reader[1];
            item.BinaryData = (byte[])reader[2];
            itemsWithAdo.Add(item);
        }
        watch.Stop();
        Console.WriteLine("ExecuteReaderAsync SequentialAccess : " + watch.ElapsedMilliseconds);
    }
}

using (var connection = new SqlConnection(CS))
{
    await connection.OpenAsync();
    using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
    {
        watch.Restart();
        List<TestItem> itemsWithAdo = new List<TestItem>();
        var reader = await cmd.ExecuteReaderAsync(CommandBehavior.Default);
        while (await reader.ReadAsync())
        {
            var item = new TestItem();
            item.ID = (int)reader[0];
            item.Name = (String)reader[1];
            item.BinaryData = (byte[])reader[2];
            itemsWithAdo.Add(item);
        }
        watch.Stop();
        Console.WriteLine("ExecuteReaderAsync Default : " + watch.ElapsedMilliseconds);
    }
}

using (var connection = new SqlConnection(CS))
{
    await connection.OpenAsync();
    using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
    {
        watch.Restart();
        List<TestItem> itemsWithAdo = new List<TestItem>();
        var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess);
        while (reader.Read())
        {
            var item = new TestItem();
            item.ID = (int)reader[0];
            item.Name = (String)reader[1];
            item.BinaryData = (byte[])reader[2];
            itemsWithAdo.Add(item);
        }
        watch.Stop();
        Console.WriteLine("ExecuteReader SequentialAccess : " + watch.ElapsedMilliseconds);
    }
}

using (var connection = new SqlConnection(CS))
{
    await connection.OpenAsync();
    using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
    {
        watch.Restart();
        List<TestItem> itemsWithAdo = new List<TestItem>();
        var reader = cmd.ExecuteReader(CommandBehavior.Default);
        while (reader.Read())
        {
            var item = new TestItem();
            item.ID = (int)reader[0];
            item.Name = (String)reader[1];
            item.BinaryData = (byte[])reader[2];
            itemsWithAdo.Add(item);
        }
        watch.Stop();
        Console.WriteLine("ExecuteReader Default : " + watch.ElapsedMilliseconds);
    }
}

对于常规的EF调用(.ToList()),分析似乎“正常”并且易于阅读:

到清单跟踪

在这里,我们发现使用秒表的时间为8.4秒(分析会降低性能)。我们还在调用路径上发现HitCount = 3500,这与测试中的3500行一致。在TDS解析器方面,情况开始变得更糟,因为我们读取了对TryReadByteArray()方法的118 353次调用,这就是发生缓冲循环的原因。(每个byte[]256kb的通话平均需要33.8次通话)

对于这种async情况,确实确实有所不同。...首先,.ToListAsync()在ThreadPool上安排调用,然后等待。这里没什么好奇怪的。但是,现在,这async是ThreadPool 上的地狱:

ToListAsync地狱

首先,在第一种情况下,整个调用路径中的点击数仅为3500,这里只有118371。此外,您还必须想象一下我没有在屏幕截图中看到的所有同步调用...

其次,在第一种情况下,我们仅对该TryReadByteArray()方法有“ 118 353”个调用,这里有2050210个调用!是原来的17倍...(在具有1Mb大型阵列的测试中,它是原来的160倍)

此外,还有:

  • Task创建了12 万个实例
  • 727519 Interlocked电话
  • 290569 Monitor电话
  • 98283个ExecutionContext实例,其中包含264481个捕获
  • 208733 SpinLock电话

我的猜测是,缓冲是通过异步方式(而不是一种好的方式)进行的,并行任务试图从TDS读取数据。仅为了解析二进制数据,创建了太多任务。

作为初步结论,我们可以说Async很棒,EF6很棒,但是EF6在其当前实现中使用async会增加性能,线程和CPU方面的开销(在CPU方面为12%)。ToList()情况下需要20%的ToListAsync时间才能工作8到10倍...我在旧的i7 920上运行)。

在做一些测试时,我又在想这篇文章,我发现我想念的东西:

“对于.Net 4.5中的新异步方法,它们的行为与同步方法完全相同,除了一个显着的例外:非顺序模式下的ReadAsync。”

什么 ?!!!

因此,我将基准测试扩展到常规/异步调用中的Ado.Net,并通过CommandBehavior.SequentialAccess/进行了扩展CommandBehavior.Default,这是一个很大的惊喜!:

阿多

我们与Ado.Net具有完全相同的行为!Facepalm ...

我的明确结论是:EF 6实施中存在一个错误。当对包含列的表进行异步调用时,应将切换CommandBehavior到。在Ado.Net方面,创建太多Task会减慢该过程的速度。EF问题是它没有按原样使用Ado.Net。SequentialAccessbinary(max)

现在您知道了,而不是使用EF6异步方法,最好是以常规的非异步方式调用EF,然后使用a TaskCompletionSource<T>以异步方式返回结果。

注意1:由于可耻的错误,我编辑了帖子。...我已经通过网络而不是本地进行了第一次测试,并且有限的带宽使结果失真。这是更新的结果。

注意2:我没有将测试扩展到其他用例(例如:nvarchar(max)具有大量数据),但是有可能发生相同的行为。

注3:常见的ToList()情况是12%的CPU(我的CPU的1/8 = 1个逻辑内核)。在这种ToListAsync()情况下,最大的20%是不寻常的情况,就好像调度程序无法使用所有的Treads。这可能是由于创建了太多任务,或者是TDS解析器中的瓶颈,我不知道...


2
我在Codeplex上打开了一个问题,希望他们能对此有所作为。entityframework.codeplex.com/workitem/2686
rducom

3
我在托管在github上的新EF代码存储库上打开了一个问题:github.com/aspnet/EntityFramework6/issues/88
Korayem,2016年

5
不幸的是,GitHub上的问题已被关闭,建议不要对varbinary使用异步。从理论上讲,应该使用varbinary,因为在传输文件时线程将被阻塞更长的时间,所以异步最有意义。那么,如果要将二进制数据保存在数据库中,我们现在该怎么办?
斯蒂尔加'17

8
有人知道这是否仍然是EF Core中的问题吗?我一直找不到任何信息或基准。
安德鲁·刘易斯

2
@AndrewLewis我有它背后没有科学,但我有重复EF核心连接池超时,其中两个查询造成的问题是.ToListAsync().CountAsync()......为了别人发现此评论线程,该查询可能会有帮助。上帝的速度。
斯科特(Scott)

2

由于几天前我获得了此问题的链接,所以我决定发布一个小更新。我能够使用最新版本的EF(6.4.0)和.NET Framework 4.7.2 重现原始答案的结果。令人惊讶的是,这个问题从未得到改善。

.NET Framework 4.7.2 | EF 6.4.0 (Values in ms. Average of 10 runs)

non async : 3016
async : 20415
ExecuteReaderAsync SequentialAccess : 2780
ExecuteReaderAsync Default : 21061
ExecuteReader SequentialAccess : 3467
ExecuteReader Default : 3074

这就提出了一个问题:dotnet核心是否有所改进?

我将代码从原始答案复制到新的dotnet core 3.1.3项目,并添加了EF Core 3.1.3。结果是:

dotnet core 3.1.3 | EF Core 3.1.3 (Values in ms. Average of 10 runs)

non async : 2780
async : 6563
ExecuteReaderAsync SequentialAccess : 2593
ExecuteReaderAsync Default : 6679
ExecuteReader SequentialAccess : 2668
ExecuteReader Default : 2315

令人惊讶的是,有了很多改进。似乎仍然存在一些时间滞后,因为调用了线程池,但是它比.NET Framework实现快了3倍。

我希望这个答案可以帮助将来收到此消息的其他人。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.