web-dev-qa-db-ja.com

大量のデータでSqlCommand非同期メソッドを使用した場合の恐ろしいパフォーマンス

非同期呼び出しを使用すると、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を使用しているからです。また、周りを検索したときに、この動作を説明できるものは何も見つかりませんでした。何か案は?

81
hcd

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

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

もっと深く。 ExecuteReaderExecuteReaderAsyncと同様に機能します。次の操作はReadの後にGetFieldValueが続き、そこで興味深いことが起こります。 2つのいずれかが非同期の場合、操作全体が遅くなります。ですから、確かに何かがあります非常に物事を真に非同期にすると、異なることが起こります-Readは高速になり、非同期GetFieldValueAsyncは遅くなります。遅いReadAsyncから始めて、GetFieldValueGetFieldValueAsyncの両方が高速です。ストリームからの最初の非同期読み取りは遅く、その速度は行全体のサイズに完全に依存します。同じサイズの行をさらに追加すると、各行の読み取りには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のケースで良好なパフォーマンスが必要な場合は、利用可能な最高のツールを使用してください。この場合、ExecuteScalarGetFieldValueなどのヘルパーに依存するのではなく、大きな列データをストリーミングします。

127
Luaan