使用SqlCommand异步方法处理大量数据时性能非常差

104

在使用异步调用时,我遇到了严重的SQL性能问题。我创建了一个小示例以演示这个问题。

我在 SQL Server 2016 上创建了一个数据库,它位于我们的局域网中(因此不是本地数据库)。

在该数据库中,我有一个名为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数据集的压缩包))。

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

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

我立即获得了结果,并且在 SQL Server 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毫秒左右。

但是当将它改为异步代码时:

string value = await command.ExecuteScalarAsync() as string;
执行时间突然达到了1800毫秒!而且在SQL Server Profiler中,我看到查询执行持续时间超过了1秒。尽管Profiler报告的已执行查询与非异步版本完全相同。
更糟糕的是,如果我在连接字符串中玩弄数据包大小,我会得到以下结果:
- 数据包大小32768:[TIMING]:ExecuteScalarAsync in SqlValueStore ->经过时间:450毫秒 - 数据包大小4096:[TIMING]:ExecuteScalarAsync in SqlValueStore ->经过时间:3667毫秒 - 数据包大小512:[TIMING]:ExecuteScalarAsync in SqlValueStore ->经过时间:30776毫秒
30000毫秒!比非异步版本慢1000倍以上。而SQL Server Profiler报告查询执行时间超过10秒。这甚至无法解释其他20秒去哪里了!
然后我切换回同步版本,并尝试改变数据包大小,虽然影响了一点执行时间,但远没有异步版本那么戏剧性。
顺便说一下,如果只将一个小字符串(<100字节)放入值中,异步查询执行速度跟同步版本一样快(1或2毫秒的结果)。
我真的被这个问题困扰着,尤其是我正在使用内置的SqlConnection,甚至不是ORM。此外,当我搜索时,找不到可以解释这种行为的任何内容。有任何想法吗?

5
为什么随着数据包大小的减小检索速度会变慢,而且当使用错误的BLOB查询时呢?@hcd 1.5 MB ????? - Panagiotis Kanavos
3
@PanagiotisKanavos那只是OP在闲逛。真正的问题是为什么在包大小相同的情况下,异步与同步相比速度要慢这么多。 - Fildor
2
查看在ADO.NET中修改大值(max)数据以获取正确的检索CLOB和BLOB的方法。不要尝试将它们作为一个大值来读取,而是使用GetSqlCharsGetSqlBinary以流式方式检索它们。另外,考虑将它们存储为FILESTREAM数据 - 没有理由将1.5MB的数据保存在表的数据页中。 - Panagiotis Kanavos
8
这不正确。OP写道同步:20-30毫秒,异步与其他一切相同1800毫秒。更改数据包大小的影响完全清楚且符合预期。 - Fildor
5
@hcd 看起来你可以删除关于尝试更改包装大小的部分,因为它似乎与问题无关,并且会导致一些评论者感到困惑。 - Kuba Wyrostek
显示剩余25条评论
1个回答

158

在负载较轻的系统中,异步调用会有稍微大一些的开销。虽然 I/O 操作本身是异步的,但阻塞可能比线程池任务切换更快。

开销有多大呢?看一下你的计时数据。阻塞调用需要 30ms,异步调用需要 450ms。32 kiB 的数据包大小意味着你需要约 50 个单独的 I/O 操作。这意味着每个数据包都有大约 8ms 的开销,这与不同数据包大小的测量结果相当吻合。这听起来并不像仅仅是因为异步而产生的开销,尽管异步版本需要比同步版本做更多的工作。它听起来像是同步版本是(简化)1 个请求 -> 50 个响应,而异步版本则变成了 1 个请求 -> 1 个响应 -> 1 个请求 -> 1 个响应 -> ...,反复地支付代价。

深入探讨一下。ExecuteReaderExecuteReaderAsync 同样有效。接下来的操作是 Read,然后是 GetFieldValue - 在这里发生了一件有趣的事情。如果其中任何一个是异步的,整个操作就会变慢。所以,一旦你开始真正地将事情异步化,就会发生非常不同的事情 - Read 会很快,然后异步的 GetFieldValueAsync 就会变慢,或者你可以从慢的 ReadAsync 开始,然后 GetFieldValueGetFieldValueAsync 都很快。从流中异步读取的第一个数据包很慢,而且慢速取决于整行的大小。如果我添加更多相同大小的行,则读取每行所需的时间与仅有一行时相同,因此很明显,数据仍按行流式传输 - 只是一旦开始任何异步读取,它似乎更喜欢一次性读取整行。如果我异步读取第一行,然后同步读取第二行 - 读取第二行将再次变得快速。

因此,我们可以看出问题在于单个行和/或列的大小很大。无论总共有多少数据 - 异步读取一百万小行与同步读取一样快。但是只要添加一个太大而无法适应单个数据包的字段,你就会神秘地承担异步读取该数据的成本 - 好像每个数据包都需要单独的请求数据包,而服务器不能一次发送所有数据。使用 CommandBehavior.SequentialAccess 的确会提高预期的性能,但同步和异步之间的巨大差距仍然存在。

我得到的最佳性能是在正确地执行整个过程时。这意味着既要使用 CommandBehavior.SequentialAccess,又要显式地流式传输数据:

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

由此,同步和异步之间的差异变得难以测量,并且改变数据包大小不再像以前一样产生荒谬的开销。

如果你想在边缘情况下获得良好的性能,请确保使用最好的可用工具 - 在这种情况下,流式传输大列数据而不是依赖于像ExecuteScalarGetFieldValue这样的辅助程序。


4
好的回答。重现了原帖中的情境。对于 OP 提到的 1.5 米字符串,同步版本需要 130 毫秒,异步版本需要 2200 毫秒。采用您的方法,这个 1.5 米字符串的测量时间为 60 毫秒,不错。 - Wiktor Zychla
4
很好的调查,此外我还学会了一些其他的调优技巧,用于我们的数据访问层(DAL)代码。 - Adam Houldsworth
8
啊哈,它最终确实起作用了 :) 但我必须在这一行中添加CommandBehavior.SequentialAccess :using (var reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess)) - hcd
1
问题很有意义,但这个答案真是珍贵!还有一句非常优美的话:“如果你想要在边缘情况下获得良好性能,请确保使用最佳工具。”谢谢! - Krishna Gupta
它对单个值的工作正常,但是我们如何读取/获取完整行呢?因为有多种情况需要使用连接从多个表中获取多个列。提前致谢! - Bharat Bhushan
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接