我在使用异步调用时遇到了严重的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相关问答推荐

PowerShell中窗体定时器和系统定时器的统一处理

为什么解码后的字节数组与原始字节数组不同?

查找所有源硬编码字符串

C# - 获取不包括隐藏文件的文件列表

如何获取 Sql Server 数据库中所有模式的列表

使用 IIS Express 托管网站(临时)

如何使用 log4net 记录跟踪消息?

.NET - 实现捕获所有异常处理程序的最佳方法是什么

支持 HTTPS 的 Httplistener

Java 和 .NET 技术/框架的类似物

Await 运算符只能在 Async 方法中使用

如何对 LINQ to XML 中的元素进行深层复制?

如何将 WebResponse.GetResponseStream 返回转换为字符串?

如何确定字符串是 C# 中的有效 IPv4 还是 IPv6 地址?

String.Replace() 与 StringBuilder.Replace()

DateTime.Compare 如何判断日期是否小于 30 天?

System.Array.CopyTo() 和 System.Array.Clone() 之间的区别

在 .NET 中乘以时间跨度

如何在我的机器上找到 fuslogvw.exe?

多行 C# 插值字符串文字