Performa buruk menggunakan metode SqlCommand Async dengan data besar

95

Saya mengalami masalah kinerja SQL yang besar saat menggunakan panggilan async. Saya telah membuat kasus kecil untuk mendemonstrasikan masalahnya.

Saya telah membuat database di SQL Server 2016 yang berada di LAN kami (jadi bukan localDB).

Di database itu, saya memiliki tabel WorkingCopydengan 2 kolom:

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]

Dalam tabel itu, saya telah memasukkan satu catatan ( id= 'PerfUnitTest', Valueadalah string 1,5mb (zip dari dataset JSON yang lebih besar)).

Sekarang, jika saya menjalankan kueri di SSMS:

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

Saya langsung mendapatkan hasilnya, dan saya melihat di SQL Servre Profiler bahwa waktu eksekusi sekitar 20 milidetik. Semuanya normal.

Saat menjalankan kueri dari kode .NET (4.6) menggunakan polos 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;

Waktu eksekusi untuk ini juga sekitar 20-30 milidetik.

Tetapi ketika mengubahnya menjadi kode async:

string value = await command.ExecuteScalarAsync() as string;

Waktu eksekusi tiba-tiba menjadi 1800 ms ! Juga di SQL Server Profiler, saya melihat bahwa durasi eksekusi kueri lebih dari satu detik. Meskipun kueri yang dieksekusi yang dilaporkan oleh profiler sama persis dengan versi non-Async.

Tapi itu lebih buruk. Jika saya bermain-main dengan Ukuran Paket dalam string koneksi, saya mendapatkan hasil sebagai berikut:

Ukuran paket 32768: [WAKTU]: ExecuteScalarAsync di SqlValueStore -> waktu yang berlalu: 450 ms

Ukuran Paket 4096: [WAKTU]: ExecuteScalarAsync di SqlValueStore -> waktu yang berlalu: 3667 md

Ukuran paket 512: [WAKTU]: ExecuteScalarAsync di SqlValueStore -> waktu yang berlalu: 30776 md

30.000 md !! Itu lebih dari 1000x lebih lambat dari versi non-asinkron. Dan SQL Server Profiler melaporkan bahwa eksekusi kueri memakan waktu lebih dari 10 detik. Itu bahkan tidak menjelaskan ke mana 20 detik lainnya pergi!

Kemudian saya beralih kembali ke versi sinkronisasi dan juga bermain-main dengan Ukuran Paket, dan meskipun itu berdampak sedikit pada waktu eksekusi, itu tidak sedramatis dengan versi asinkron.

Sebagai sidenote, jika ia hanya menempatkan string kecil (<100bytes) ke dalam nilai, eksekusi kueri asinkron secepat versi sinkronisasi (menghasilkan 1 atau 2 md).

Saya benar-benar bingung dengan ini, terutama karena saya menggunakan bawaan SqlConnection, bahkan bukan ORM. Juga ketika mencari-cari, saya tidak menemukan apa pun yang dapat menjelaskan perilaku ini. Ada ide?

hcd
sumber
5
@hcd 1,5 MB ????? Dan Anda bertanya mengapa pengambilan yang semakin lambat dengan ukuran paket yang berkurang? Terutama saat Anda menggunakan kueri yang salah untuk BLOB?
Panagiotis Kanavos
3
@PanagiotisKanavos Tadi cuma main-main atas nama OP. Pertanyaan sebenarnya adalah mengapa async jauh lebih lambat dibandingkan dengan sinkronisasi dengan ukuran paket yang sama .
Fildor
2
Periksa Memodifikasi Data Nilai Besar (maks) di ADO.NET untuk mengetahui cara yang benar dalam mengambil CLOB dan BLOB. Alih - alih mencoba membacanya sebagai satu nilai besar, gunakan GetSqlCharsatau GetSqlBinaryambil kembali secara streaming. Pertimbangkan juga untuk menyimpannya sebagai data FILESTREAM - tidak ada alasan untuk menyimpan 1,5MB data di halaman data tabel
Panagiotis Kanavos
8
@PanagiotisKanavos Itu tidak benar. OP menulis sinkronisasi: 20-30 ms dan async dengan segala sesuatu yang lain sama 1800 ms. Efek dari mengubah ukuran paket sangat jelas dan diharapkan.
Fildor
5
@hcd tampaknya Anda dapat menghapus bagian tentang upaya Anda untuk mengubah ukuran paket karena tampaknya tidak relevan dengan masalah dan menyebabkan kebingungan di antara beberapa pemberi komentar.
Kuba Wyrostek

Jawaban:

141

Pada sistem tanpa beban yang signifikan, panggilan asinkron memiliki overhead yang sedikit lebih besar. Meskipun operasi I / O itu sendiri tidak sinkron, pemblokiran bisa lebih cepat daripada pengalihan tugas kumpulan benang.

Berapa biaya overhead? Mari kita lihat nomor waktu Anda. 30ms untuk panggilan pemblokiran, 450ms untuk panggilan asynchronous. Ukuran paket 32 ​​kiB berarti Anda memerlukan sekitar lima puluh operasi I / O individu. Itu berarti kami memiliki kira-kira 8ms overhead pada setiap paket, yang cukup sesuai dengan pengukuran Anda pada ukuran paket yang berbeda. Itu tidak terdengar seperti overhead hanya dari menjadi asynchronous, meskipun versi asynchronous perlu melakukan lebih banyak pekerjaan daripada sinkronisasi. Sepertinya versi sinkron adalah (disederhanakan) 1 permintaan -> 50 tanggapan, sedangkan versi asinkron akhirnya menjadi 1 permintaan -> 1 tanggapan -> 1 permintaan -> 1 tanggapan -> ..., membayar biaya berulang kali lagi.

Lebih dalam. ExecuteReaderbekerja dengan baik juga ExecuteReaderAsync. Operasi selanjutnya Readdiikuti oleh GetFieldValue- dan hal menarik terjadi di sana. Jika salah satu dari keduanya asinkron, seluruh operasi menjadi lambat. Jadi pasti ada sesuatu yang sangat berbeda terjadi setelah Anda mulai membuat semuanya benar-benar asinkron - a Readakan cepat, dan kemudian asinkron GetFieldValueAsyncakan lambat, atau Anda dapat memulai dengan lambat ReadAsync, lalu keduanya GetFieldValuedan GetFieldValueAsynccepat. Pembacaan asinkron pertama dari aliran lambat, dan kelambatan bergantung sepenuhnya pada ukuran keseluruhan baris. Jika saya menambahkan lebih banyak baris dengan ukuran yang sama, membaca setiap baris membutuhkan waktu yang sama seolah-olah saya hanya memiliki satu baris, jadi jelas bahwa datanya adalahmasih di-streaming baris demi baris - itu hanya tampaknya lebih memilih untuk membaca seluruh baris sekaligus setelah Anda mulai setiap membaca asynchronous. Jika saya membaca baris pertama secara asinkron, dan baris kedua secara sinkron - baris kedua yang dibaca akan cepat kembali.

Jadi kita dapat melihat bahwa masalahnya adalah ukuran besar dari setiap baris dan / atau kolom. Tidak masalah berapa banyak data yang Anda miliki - membaca jutaan baris kecil secara asinkron secepat sinkronisasi. Tetapi tambahkan hanya satu bidang yang terlalu besar untuk muat dalam satu paket, dan Anda secara misterius dikenai biaya untuk membaca data itu secara tidak sinkron - seolah-olah setiap paket memerlukan paket permintaan terpisah, dan server tidak bisa begitu saja mengirim semua data di sekali. Penggunaan CommandBehavior.SequentialAccessmemang meningkatkan kinerja seperti yang diharapkan, tetapi kesenjangan besar antara sinkronisasi dan asinkron masih ada.

Performa terbaik yang saya dapatkan adalah saat melakukan semuanya dengan benar. Itu berarti menggunakan CommandBehavior.SequentialAccess, serta mengalirkan data secara eksplisit:

using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
{
  while (await reader.ReadAsync())
  {
    var data = await reader.GetTextReader(0).ReadToEndAsync();
  }
}

Dengan ini, perbedaan antara sinkronisasi dan asinkron menjadi sulit untuk diukur, dan mengubah ukuran paket tidak lagi menimbulkan overhead yang konyol seperti sebelumnya.

Jika Anda menginginkan kinerja yang baik dalam kasus edge, pastikan untuk menggunakan alat terbaik yang tersedia - dalam hal ini, streaming data kolom besar daripada mengandalkan pembantu seperti ExecuteScalaratau GetFieldValue.

Luaan
sumber
3
Jawaban yang bagus. Mereproduksi skenario OP. Untuk string OP 1.5m ini disebutkan, saya mendapatkan 130ms untuk versi sinkronisasi vs 2200ms untuk async. Dengan pendekatan Anda, waktu yang diukur untuk string 1,5m adalah 60ms, lumayan.
Wiktor Zychla
4
Investigasi yang bagus di sana, ditambah lagi saya mempelajari beberapa teknik penyetelan lain untuk kode DAL kami.
Adam Houldorth
Baru saja kembali ke kantor dan mencoba kode pada contoh saya alih-alih ExecuteScalarAsync, tetapi saya masih mendapat waktu eksekusi 30 detik dengan ukuran paket 512 byte :(
hcd
6
Aha, ternyata berhasil :) Tapi saya harus menambahkan CommandBehavior.SequentialAccess ke baris ini: using (var reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
hcd
@hcd Saya buruk, saya memilikinya dalam teks tetapi tidak dalam kode sampel :)
Luaan