我在使用异步调用时遇到了严重的SQL性能问题.我创建了一个小 case 来证明这个问题.

我已经在驻留在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='PerfurnitTest',Value是一个1.5mb的字符串(一个更大的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毫秒左右.

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

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

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

但情况变得更糟了.如果我在连接字符串中处理数据包大小,我会得到以下结果:

Packet size 32768 : [TIMING]: ExecuteScalarAsync in SqlValueStore -> elapsed time : 450 ms

Packet Size 4096 : [TIMING]: ExecuteScalarAsync in SqlValueStore -> elapsed time : 3667 ms

Packet size 512 : [TIMING]: ExecuteScalarAsync in SqlValueStore -> elapsed time : 30776 ms

30,000 ms!! 这比非异步版本慢30,000 ms0倍以上.SQL Server Profiler报告查询执行耗时超过10秒.这甚至不能解释剩下的20秒到哪里go 了!

然后我又切换回了同步版本,并对数据包的大小进行了调整,虽然它确实对执行时间产生了一些影响,但它没有异步版本那么引人注目.

另外,如果只将一个小字符串(<100字节)放入值中,异步查询的执行速度与同步版本一样快(结果为1或2毫秒).

我真的很困惑,尤其是因为我使用的是内置的SqlConnection,甚至不是ORM.此外,在四处搜索时,我发现没有任何东西可以解释这种行为.有什么 idea 吗?

推荐答案

在没有显著负载的系统上,异步调用的开销稍大一些.尽管I/O操作本身无论如何都是异步的,但是阻塞可能比线程池任务切换更快.

How much overhead? Let's look at your timing numbers. 30ms for a blocking call, 450ms for an asynchronous call. 32 kiB packet size means you need you need about fifty individual I/O operations. That means we have roughly 8ms of overhead on each packet, which corresponds pretty well with your measurements over different packet sizes. That doesn't sound like overhead just from being asynchronous, even though the asynchronous versions need to do a lot more work than the synchronous. It sounds like the synchronous version is (simplified) 1 request -> 50 responses, while the asynchronous version ends up being 1 request -> 1 response -> 1 request -> 1 response -> ..., paying the cost over and over again.

深入.ExecuteReaderExecuteReaderAsync一样有效.下一个操作是Read,然后是GetFieldValue,在那里发生了一件有趣的事情.如果两者中的任何一个是异步的,那么整个操作都很慢.因此,一旦你开始让事情变得真正异步,肯定会有very种不同的情况发生——Read将是快的,然后异步GetFieldValueAsync将是慢的,或者你可以从慢ReadAsync开始,然后GetFieldValueGetFieldValueAsync都是快的.从流中第一次异步读取的速度很慢,速度完全取决于整行的大小.如果我添加更多相同大小的行,读取每一行所需的时间与仅读取一行所需的时间相同,因此很明显,数据is仍在逐行传输——它似乎更喜欢在开始any异步读取后立即读取整行.如果我异步读取第一行,同步读取第二行,那么正在读取的第二行将再次变快.

所以我们可以看到,问题是单个行和/或列的大小太大.不管你总共有多少数据——异步读取一百万小行和同步读取一样快.但是,如果只添加一个太大而无法放入单个数据包中的字段,那么异步读取该数据会产生神秘的成本——就好像每个数据包都需要一个单独的请求数据包,服务器不能一次发送所有数据一样.使用CommandBehavior.SequentialAccess确实会像预期的那样提高性能,但同步和异步之间仍然存在巨大的差距.

我得到的最好的表现就是把整件事做好.这意味着使用CommandBehavior.SequentialAccess,并显式地流式传输数据:

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

这样一来,同步和异步之间的差异就变得难以衡量,改变数据包大小不再像以前那样带来荒谬的开销.

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

.net相关问答推荐

如果只有一个 : 存在于字符串中,则提取冒号后的内容

IIS 发布 ASP.NET Core 应用程序而不关闭 IIS 网站

使用 MassTransit、.NET Core 和 RabbitMQ 的设计挑战

为什么不能使用 null 作为 Dictionary 的键?

从 byte[] 创建 zip 文件

如何从控制台应用程序中的 Task.WaitAll() 获取返回值?

我可以从我的应用程序中抛出哪些内置 .NET 异常?

将客户端证书添加到 .NET Core HttpClient

每第 N 个字符/数字拆分一个字符串/数字?

SubscribeOn 和 ObserveOn 有什么区别

C# 属性实际上是方法吗?

我们应该总是在类中包含一个默认构造函数吗?

Iif 在 C# 中等效

判断 .NET 中的目录和文件写入权限

如何修复 .NET Windows 应用程序在启动时崩溃并出现异常代码:0xE0434352?

如何从文件中删除单个属性(例如只读)?

使用 DateTime.ToString() 时获取日期后缀

通过反射获取公共静态字段的值

如何将两个 List 相互比较?

您可以将 Microsoft Entity Framework 与 Oracle 一起使用吗?