非同期呼び出しを使用すると、SQLのパフォーマンスに大きな問題が発生します。問題を示すために小さなケースを作成しました。
LANにあるSQL Server 2016にデータベースを作成しました(localDBではありません)。
そのデータベースには、2列のテーブル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]
そのテーブルに、1つのレコードを挿入しました(id
= 'PerfUnitTest'、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では、クエリの実行時間が1秒以上であることがわかります。プロファイラーによって報告された実行されたクエリは、非同期バージョンとまったく同じです。
しかし、それはさらに悪化します。接続文字列のパケットサイズをいじると、次の結果が得られます。
パケットサイズ32768:[タイミング]:SqlValueStoreのExecuteScalarAsync->経過時間:450ミリ秒
パケットサイズ4096:[タイミング]:SqlValueStoreのExecuteScalarAsync->経過時間:3667ミリ秒
パケットサイズ512:[タイミング]:SqlValueStoreのExecuteScalarAsync->経過時間:30776ミリ秒
,000ミリ秒 !!それは、非同期バージョンよりも1000倍以上遅いです。また、SQL Server Profilerは、クエリの実行に10秒以上かかったことを報告しています。それは他の20秒がどこに行ったのかさえ説明していません!
その後、同期バージョンに切り替えて、パケットサイズを試してみましたが、実行時間には少し影響がありましたが、非同期バージョンほど劇的ではありませんでした。
補足として、小さな文字列(<100バイト)を値に入れると、非同期クエリの実行は同期バージョンと同じくらい高速になります(結果は1または2ミリ秒)。
私はこれに本当に困惑しています。特にORMでなく、組み込みのSqlConnection
を使用しているからです。また、周りを検索したときに、この動作を説明できるものは何も見つかりませんでした。何か案は?
大きな負荷のないシステムでは、非同期呼び出しのオーバーヘッドはわずかに大きくなります。 I/O操作自体は関係なく非同期ですが、ブロッキングはスレッドプールタスクの切り替えよりも高速です。
オーバーヘッドはいくらですか?タイミングの数値を見てみましょう。ブロッキングコールの場合は30ミリ秒、非同期コールの場合は450ミリ秒。 32 kiBのパケットサイズは、約50の個別のI/O操作が必要であることを意味します。つまり、各パケットには約8ミリ秒のオーバーヘッドがあります。これは、さまざまなパケットサイズでの測定値にかなり対応しています。非同期バージョンは同期よりも多くの作業を行う必要がありますが、それは非同期であることによるオーバーヘッドのようには聞こえません。同期バージョンは(単純化された)1リクエスト-> 50レスポンスで、非同期バージョンは1リクエスト-> 1レスポンス-> 1リクエスト-> 1レスポンス-> ...のように聞こえます。再び。
もっと深く。 ExecuteReader
はExecuteReaderAsync
と同様に機能します。次の操作はRead
の後にGetFieldValue
が続き、そこで興味深いことが起こります。 2つのいずれかが非同期の場合、操作全体が遅くなります。ですから、確かに何かがあります非常に物事を真に非同期にすると、異なることが起こります-Read
は高速になり、非同期GetFieldValueAsync
は遅くなります。遅いReadAsync
から始めて、GetFieldValue
とGetFieldValueAsync
の両方が高速です。ストリームからの最初の非同期読み取りは遅く、その速度は行全体のサイズに完全に依存します。同じサイズの行をさらに追加すると、各行の読み取りには1行しかない場合と同じ時間がかかるため、データisが行ごとにストリームされていることは明らかです。 any非同期読み取りを開始すると、一度に行全体を読み取ることを好むようです。最初の行を非同期で読み取り、2番目の行を同期的に読み取ると、読み取られる2番目の行は再び高速になります。
したがって、問題は個々の行や列のサイズが大きいことがわかります。合計でどれだけのデータがあるかは関係ありません。100万個の小さな行を非同期で読み取ることは、同期と同じくらい高速です。ただし、1つのパケットに収まるには大きすぎる1つのフィールドを追加すると、そのデータを非同期的に読み取るコストが不思議になります。各パケットが個別の要求パケットを必要とし、サーバーがすべてのデータを送信できない一度。 CommandBehavior.SequentialAccess
を使用すると、期待どおりにパフォーマンスが向上しますが、同期と非同期の間に大きなギャップが依然として存在します。
私が得た最高のパフォーマンスは、すべてを適切に行うことでした。つまり、CommandBehavior.SequentialAccess
を使用し、データを明示的にストリーミングします。
using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
{
while (await reader.ReadAsync())
{
var data = await reader.GetTextReader(0).ReadToEndAsync();
}
}
これにより、同期と非同期の違いを測定するのが難しくなり、パケットサイズを変更しても、以前のように途方もないオーバーヘッドが発生しなくなります。
Edgeのケースで良好なパフォーマンスが必要な場合は、利用可能な最高のツールを使用してください。この場合、ExecuteScalar
やGetFieldValue
などのヘルパーに依存するのではなく、大きな列データをストリーミングします。