大きなデータでSqlCommand Asyncメソッドを使用した恐ろしいパフォーマンス


95

非同期呼び出しを使用すると、SQLパフォーマンスに大きな問題が発生します。私は問題を実証するために小さなケースを作成しました。

LANにあるSQL Server 2016(localDBではない)にデータベースを作成しました。

そのデータベースには、WorkingCopy2つの列を持つテーブルがあります。

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.5 MBの文字列(より大きなJSONデータセットのzip)です)。

ここで、SSMSでクエリを実行すると:

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

結果はすぐにわかり、SQL Servreプロファイラーで実行時間が約20ミリ秒であることがわかりました。すべて正常。

プレーンを使用して.NET(4.6)コードからクエリを実行する場合SqlConnection

// 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;

実行時間はいきなり1800ms!また、SQL Serverプロファイラーでは、クエリの実行時間が1秒を超えていることがわかります。プロファイラーによって報告された実行済みクエリは、非同期ではないバージョンとまったく同じですが。

しかし、それはさらに悪化します。接続文字列のパケットサイズをいじると、次の結果が得られます。

パケットサイズ32768:[タイミング]:SqlValueStoreのExecuteScalarAsync->経過時間:450ミリ秒

パケットサイズ4096:[タイミング]:SqlValueStoreのExecuteScalarAsync->経過時間:3667ミリ秒

パケットサイズ512:[タイミング]:SqlValueStoreのExecuteScalarAsync->経過時間:30776ミリ秒

30,000ミリ秒 !! これは、非同期バージョンよりも1000倍以上遅いです。また、SQL Server Profilerは、クエリの実行に10秒以上かかったと報告しています。それでは、残りの20秒がどこに向かっているのかさえ説明されていません!

次に、同期バージョンに切り替えて、パケットサイズをいじってみました。実行時間には多少の影響はありましたが、非同期バージョンほど劇的ではありませんでした。

補足として、小さな文字列(100バイト未満)だけを値に入れると、非同期クエリの実行は同期バージョンと同じくらい高速になります(結果は1または2ミリ秒)。

特にSqlConnectionORMでなく組み込みのを使用しているので、これには本当に困惑しています。また、周りを検索したところ、この動作を説明できるものは何も見つかりませんでした。何か案は?


4
@hcd 1.5 MB ????? そして、パケットサイズが小さくなると、取得が遅くなるのはなぜですか。特に、BLOBに対して間違ったクエリを使用する場合はどうでしょうか。
Panagiotis Kanavos 2017

3
@PanagiotisKanavosそれはOPに代わって遊んでいた。実際の問題は、同じパッケージサイズで同期する場合と比較して、非同期の速度が非常に遅い理由です。
Fildor

2
CLOBおよびBLOBを取得する正しい方法については、ADO.NETの「大きな値(最大)データの変更」を確認してください。代わりに、一つの大きな値としてそれらを読んで使用しようとしているのGetSqlCharsか、GetSqlBinaryストリーミング形式でそれらを取得するために。また、それらをFILESTREAMデータとして保存することも検討してください。1.5MBのデータをテーブルのデータページに保存する理由はありません
Panagiotis Kanavos '23

8
@PanagiotisKanavos不正解です。OPは同期を書き込みます。20〜30ミリ秒で、それ以外はすべて1800ミリ秒で非同期です。パケットサイズの変更による影響は完全に明確であり、予想されたものです。
Fildor

5
@hcdそれは問題に無関係であると思われ、一部のコメント投稿者の間で混乱を引き起こすので、パッケージサイズを変更する試みに関する部分を削除できるようです。
Kuba Wyrostek

回答:


139

大きな負荷のないシステムでは、非同期呼び出しのオーバーヘッドがわずかに大きくなります。I / O操作自体は非同期ですが、ブロッキングはスレッドプールタスクの切り替えよりも高速です。

オーバーヘッドはどのくらいですか?タイミングの数値を見てみましょう。ブロッキング呼び出しの場合は30ms、非同期呼び出しの場合は450ms。32 KiBのパケットサイズは、約50の個別のI / O操作が必要であることを意味します。これは、各パケットに約8msのオーバーヘッドがあることを意味します。これは、さまざまなパケットサイズでの測定値にかなり対応しています。非同期バージョンは同期バージョンよりもはるかに多くの作業を行う必要がありますが、非同期であることだけではオーバーヘッドのようには聞こえません。同期バージョンは(簡略化された)1リクエスト-> 50レスポンスであるように思えますが、非同期バージョンは最終的に1リクエスト-> 1レスポンス-> 1リクエスト-> 1レスポンス-> ...となり、何度もコストがかかります再び。

より深く行く。ExecuteReaderと同様に動作しExecuteReaderAsyncます。次の操作のRead後にはGetFieldValue- が続き、そこで興味深いことが起こります。2つのうちどちらかが非同期の場合、全体の動作が遅くなります。ですから、完全に非同期にすると、非常に異なることが起こりますRead。a GetFieldValueAsyncは高速になり、次に非同期は低速になります。あるいは、低速ReadAsyncで開始してから、両方GetFieldValueGetFieldValueAsync高速にできます。ストリームからの最初の非同期読み取りは低速であり、速度は行全体のサイズに完全に依存します。同じサイズの行をさらに追加した場合、各行の読み取りには、行が1つしかない場合と同じ時間がかかるため、データ行ごとにまだストリーミングされています- 非同期読み取りを開始すると、一度に行全体を読み取る方がよいようです。最初の行を非同期で読み取り、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();
  }
}

これにより、syncとasyncの違いを測定することが困難になり、パケットサイズを変更しても以前のようにばかげたオーバーヘッドが発生しなくなりました。

エッジケースで優れたパフォーマンスが必要な場合は、利用可能な最高のツールを使用してください。この場合、ExecuteScalarまたはなどのヘルパーに依存するのではなく、大きな列データをストリーミングしますGetFieldValue


3
すばらしい答えです。OPのシナリオを再現。この1.5m文字列OPについて言及しているのは、同期バージョンで130ms、非同期バージョンで2200msです。このアプローチでは、1.5mストリングの測定時間は60msであり、悪くはありません。
Wiktor Zychla 2017

4
そこでの良い調査に加えて、DALコードのための他のチューニング手法をいくつか学びました。
アダムホールズワース2017

ちょうどオフィスに戻って、ExecuteScalarAsyncの代わりに私の例のコードを試しましたが、512バイトのパケットサイズで30秒の実行時間を得ました:(
hcd

6
ああ、結局はうまくいきました:)しかし、私はこの行にCommandBehavior.SequentialAccessを追加する必要があります: using (var reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
hcd

@hcd私の悪いこと、私はそれをテキストに入れましたが、サンプルコードには入れませんでした:)
Luaan
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.