使用SqlCommandasynchronous方法时性能糟糕

在使用asynchronous调用时,我遇到了主要的SQL性能问题。 我创build了一个小案例来演示这个问题。

我已经创build了一个数据库驻留在我们的局域网(所以不是一个localDB)的SQL Server 2016年。

在那个数据库中,我有一个WorkingCopy表,包含两列:

 Id (nvarchar(255, PK)) Value (nvarchar(max)) 

DDL

 CREATE TABLE [dbo].[Workingcopy] ( [Id] [nvarchar](255) NOT NULL, [Value] [nvarchar](max) NULL, CONSTRAINT [PK_Workingcopy] PRIMARY KEY CLUSTERED ([Id] ASC) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] 

在该表中,我插入了一条logging( id ='PerfUnitTest', Value是一个1.5mb的string(一个较大的JSON数据集的zip))。

现在,如果我在SSMS中执行查询:

 SELECT [Value] FROM [Workingcopy] WHERE id = 'perfunittest' 

我马上得到了结果,我在SQL Servre Profiler中看到执行时间大约是20毫秒。 一切正常。

使用普通的SqlConnection从.NET(4.6)代码执行查询时:

 // at this point, the connection is already open var command = new SqlCommand($"SELECT Value FROM WorkingCopy WHERE Id = @Id", _connection); command.Parameters.Add("@Id", SqlDbType.NVarChar, 255).Value = key; string value = command.ExecuteScalar() as string; 

执行时间也是20-30毫秒左右。

但是,将其更改为asynchronous代码时:

 string value = await command.ExecuteScalarAsync() as string; 

执行时间突然1800毫秒 ! 同样在SQL Server Profiler中,我看到查询执行持续时间超过一秒钟。 尽pipe探查器报告的已执行查询与非asynchronous版本完全相同。

但情况变得更糟。 如果我在连接string中使用数据包大小,我会得到以下结果:

数据包大小32768:[TIMING]:SqlValueStore中的ExecuteScalarAsync – >已用时间:450 ms

数据包大小4096:[TIMING]:SqlValueStore中的ExecuteScalarAsync – >已用时间:3667毫秒

数据包大小512:[TIMING]:SqlValueStore中的ExecuteScalarAsync – >已用时间:30776毫秒

30,000毫秒 ! 这比非asynchronous版本慢了1000倍。 并且SQL Server Profiler报告查询执行超过10秒钟。 这甚至不能解释另外20秒的地方!

然后我又回到了同步版本,并且还使用了“数据包大小”(Packet Size),虽然它的确影响了一点执行时间,但是与asynchronous版本相比,却没有那么戏剧化。

作为旁注,如果它只将一个小string(<100bytes)放入值中,那么asynchronous查询的执行速度与同步版本(1ms或2ms)的速度一样快。

我真的很困惑,特别是因为我使用内置的SqlConnection ,甚至没有一个ORM。 另外,当四处搜寻,我什么也没有发现可以解释这种行为。 有任何想法吗?

在没有重要负载的系统上,asynchronous调用的开销稍大。 尽pipeI / O操作本身是asynchronous的,但是阻塞可以比线程池任务切换更快。

多less开销? 让我们看看你的时间编号。 阻塞呼叫为30ms,asynchronous呼叫为450ms。 32 kiB数据包大小意味着您需要大约50个单独的I / O操作。 这意味着每个数据包大约有8ms的开销,这与您在不同数据包大小下的测量结果相当吻合。 即使asynchronous版本需要比同步执行更多的工作,这听起来不像是asynchronous的开销。 这听起来像是同步版本是(简化)1个请求 – > 50个响应,而asynchronous版本最终是1个请求 – > 1个响应 – > 1个请求 – > 1个响应 – > …,一遍又一遍地支付成本再次。

走得更深。 ExecuteReaderExecuteReaderAsync 。 接下来的操作是Read然后是GetFieldValue – 在这里发生一件有趣的事情。 如果两者中的任何一个是asynchronous的,则整个操作是缓慢的。 所以一旦开始使事物真正asynchronous,肯定会有一些非常不同的情况发生 – 一个Read会很快,然后asynchronousGetFieldValueAsync会很慢,或者您可以从慢ReadAsync开始,然后GetFieldValueGetFieldValueAsync很快。 从stream中第一次asynchronous读取很慢,慢度完全取决于整行的大小。 如果我添加更多的相同大小的行,读取每行所花的时间与我只有一行相同,所以很明显,数据仍然是逐行进行stream式传输 – 它似乎更喜欢读取整个一旦你开始任何asynchronous读取行。 如果我读取第一行asynchronous,第二行同步 – 正在读取的第二行将再次快速。

所以我们可以看到问题是个别行和/或列的大小。 总共有多less数据并不重要 – asynchronous读取一百万行的速度与同步速度一样快。 但是添加一个太大的字段不适合放在单个数据包中,而且你在asynchronous读取数据时会产生费用 – 就好像每个数据包都需要一个单独的请求数据包一样,服务器不能只发送所有的数据一旦。 使用CommandBehaviour.SequentialAccess确实可以提高性能,但同步和asynchronous之间的巨大差距依然存在。

我得到的最好的performance是在正确地完成整个事情的时候。 这意味着使用CommandBehaviour.SequentialAccess ,以及显式stream式传输数据:

 using (var reader = await cmd.ExecuteReaderAsync(CommandBehaviour.SequentialAccess)) { while (await reader.ReadAsync()) { var data = await reader.GetTextReader(0).ReadToEndAsync(); } } 

有了这个,sync和async之间的区别变得难以衡量,改变数据包大小不再像以前那样引起荒谬的开销。

如果你想在边缘情况下获得良好的性能,请确保使用最好的工具 – 在这种情况下,stream大列数据,而不是依赖像ExecuteScalarGetFieldValue这样的助手。