使用SqlCommand Async方法处理大数据的性能糟糕


95

使用异步调用时,我遇到了主要的SQL性能问题。我创建了一个小案例来演示该问题。

我在驻留在我们LAN中的SQL Server 2016上创建了一个数据库(因此没有localDB)。

在该数据库中,我有一个WorkingCopy包含2列的表:

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]

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

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

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

我立即得到结果,并且在SQL Servre Profiler中看到执行时间大约为20毫秒。一切正常。

使用纯文本从.NET(4.6)代码执行查询时SqlConnection

// 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毫秒。

但是,当将其更改为异步代码时:

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

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

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

数据包大小32768:[TIMING]:SqlValueStore中的ExecuteScalarAsync->经过的时间:450 ms

数据包大小4096:[时间]:SqlValueStore中的ExecuteScalarAsync->经过的时间:3667毫秒

数据包大小512:[TIMING]:SqlValueStore中的ExecuteScalarAsync->经过的时间:30776毫秒

30,000毫秒!这比非异步版本慢1000倍。并且SQL Server Profiler报告查询执行花费了10秒钟以上。这甚至不能解释其他20秒的去向!

然后,我切换回同步版本,并使用Packet Size,虽然它确实影响了一些执行时间,但与异步版本相比,它没有那么引人注目。

附带说明一下,如果将一个小的字符串(<100bytes)放入值中,则异步查询的执行速度与同步版本一样快(结果为1或2毫秒)。

我对此感到非常困惑,尤其是因为我使用的是内置的SqlConnection,甚至没有使用ORM。另外,在四处搜寻时,我什么也没找到能解释这种现象的东西。有任何想法吗?


4
@hcd 1.5 MB ????? 您问为什么随着数据包大小的减小而检索速度变慢?特别是当您对BLOB 使用错误的查询时?
Panagiotis Kanavos '17

3
@PanagiotisKanavos只是代表OP在玩。实际的问题是,与具有相同包大小的同步相比,为什么异步要慢得多。
Fildor

2
选中“ 在ADO.NET中修改大数值(最大)数据”,以获取正确的CLOB和BLOB检索方法。与其尝试以大价值读取它们,不如以流方式使用GetSqlCharsGetSqlBinary检索它们。还可以考虑将它们存储为FILESTREAM数据-没有理由在表的数据页中保存1.5MB的数据
Panagiotis Kanavos

8
@PanagiotisKanavos这是不正确的。OP写入同步:20-30毫秒,并与其他所有内容同步1800毫秒。更改数据包大小的效果是完全明确的和预期的。
Fildor

5
@hcd似乎可以删除有关更改程序包大小的尝试的部分,因为它似乎与问题无关,并引起一些评论者的困惑。
Kuba Wyrostek

Answers:


139

在没有大量负载的系统上,异步调用的开销会稍大。尽管I / O操作本身是异步的,但阻塞比线程池任务切换要快。

多少开销?让我们看看您的计时号码。阻塞呼叫30ms,异步呼叫450ms。32 kiB数据包大小意味着您需要大约五十个单独的I / O操作。这意味着每个数据包大约有8ms的开销,这与您对不同数据包大小的测量非常吻合。尽管异步版本需要比同步版本做更多的工作,但这听起来不仅仅只是异步的开销。听起来好像同步版本是(简化)1个请求-> 50个响应,而异步版本最终是1个请求-> 1个响应-> 1个请求-> 1个响应-> ...,这是一遍又一遍地付出的代价再次。

更深入。ExecuteReader效果和ExecuteReaderAsync。下一个操作Read后跟一个GetFieldValue-,并且有趣的事情在那里发生。如果两者之一异步,则整个操作很慢。因此,一旦开始真正实现异步,肯定会发生非常不同的事情Read-a GetFieldValueAsync将很快,然后异步将变得很慢,或者您可以先从slow开始ReadAsync,然后又开始GetFieldValue并且GetFieldValueAsync很快。从流中进行的第一个异步读取很慢,并且该慢度完全取决于整个行的大小。如果我添加更多相同大小的行,则读取每一行所花费的时间就好像我只有一行一样,因此很明显,数据仍在逐行流式传输-似乎更喜欢在开始任何异步读取后立即读取整行。如果我异步读取第一行,然后异步读取第二行,那么第二行将很快读取。

因此,我们可以看到问题出在单个行和/或列的大小很大。总共有多少数据都没有关系-异步读取一百万个小行与同步一样快。但是,仅添加一个太大而无法容纳在单个数据包中的字段,就会神秘地异步读取该数据而产生成本-好像每个数据包都需要一个单独的请求数据包,并且服务器不能只发送所有数据一旦。使用CommandBehavior.SequentialAccess确实可以改善性能,但是同步和异步之间仍然存在巨大差距。

我获得的最佳性能是正确完成整个操作。这意味着使用CommandBehavior.SequentialAccess,以及显式传输数据:

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

这样,同步和异步之间的差异就变得难以测量,并且更改数据包大小不再像以前那样引起可笑的开销。

如果你想在边缘的情况下性能良好,确保使用最好的工具可用-在这种情况下,流大量列数据,而不是依靠像佣工ExecuteScalarGetFieldValue


3
好答案。复制了OP的场景。对于这个1.5m的字符串OP来说,同步版本为130ms,异步版本为2200ms。使用您的方法,1.5m字符串的测量时间为60ms,还不错。
Wiktor Zychla

4
在那里进行了很好的研究,此外,我还学习了一些其他的DAL代码调优技术。
亚当·霍尔兹沃思

刚回到办公室并尝试了我的示例中的代码,而不是ExecuteScalarAsync,但是我仍然获得了30秒的执行时间和512字节的数据包大小:(
hcd

6
啊哈,毕竟它确实有效了:)但是我必须在该行中添加CommandBehavior.SequentialAccess: using (var reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
hcd

@hcd我不好,我在文本中有它,但在示例代码中没有它:)
Luaan
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.