使用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个响应 – > …,一遍又一遍地支付成本再次。
走得更深。 ExecuteReader
和ExecuteReaderAsync
。 接下来的操作是Read
然后是GetFieldValue
– 在这里发生一件有趣的事情。 如果两者中的任何一个是asynchronous的,则整个操作是缓慢的。 所以一旦开始使事物真正asynchronous,肯定会有一些非常不同的情况发生 – 一个Read
会很快,然后asynchronousGetFieldValueAsync
会很慢,或者您可以从慢ReadAsync
开始,然后GetFieldValue
和GetFieldValueAsync
很快。 从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大列数据,而不是依赖像ExecuteScalar
或GetFieldValue
这样的助手。