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 WorkingCopy
dengan 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', Value
adalah 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?
GetSqlChars
atauGetSqlBinary
ambil kembali secara streaming. Pertimbangkan juga untuk menyimpannya sebagai data FILESTREAM - tidak ada alasan untuk menyimpan 1,5MB data di halaman data tabelJawaban:
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.
ExecuteReader
bekerja dengan baik jugaExecuteReaderAsync
. Operasi selanjutnyaRead
diikuti olehGetFieldValue
- 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 - aRead
akan cepat, dan kemudian asinkronGetFieldValueAsync
akan lambat, atau Anda dapat memulai dengan lambatReadAsync
, lalu keduanyaGetFieldValue
danGetFieldValueAsync
cepat. 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.SequentialAccess
memang 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
ExecuteScalar
atauGetFieldValue
.sumber
using (var reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess))