Жахлива продуктивність за допомогою методів SqlCommand Async з великими даними


95

У мене виникають основні проблеми з продуктивністю SQL під час використання асинхронних викликів. Я створив невеликий випадок, щоб продемонструвати проблему.

Я створив базу даних на SQL Server 2016, яка знаходиться в нашій локальній мережі (отже, не в localDB).

У цій базі даних у мене є таблиця WorkingCopyз 2 стовпцями:

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]

У цю таблицю я вставив один запис ( id= 'PerfUnitTest', Valueце рядок розміром 1,5 Мб (zip більшого набору даних JSON)).

Тепер, якщо я виконую запит у SSMS:

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

Я негайно отримую результат, і я бачу в SQL Servre Profiler, що час виконання становив близько 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;

Час виконання раптово 1800 мс ! Також у програмі SQL Server Profiler я бачу, що тривалість виконання запиту перевищує секунду. Хоча виконаний запит, про який повідомляє профайлер, точно такий самий, як і версія, що не є асинхронною.

Але стає гірше. Якщо я пограю з розміром пакета в рядку підключення, я отримаю такі результати:

Розмір пакета 32768: [TIMING]: ExecuteScalarAsync in SqlValueStore -> минулий час: 450 мс

Розмір пакета 4096: [TIMING]: ExecuteScalarAsync in SqlValueStore -> минулий час: 3667 мс

Розмір пакета 512: [TIMING]: ExecuteScalarAsync in SqlValueStore -> минулий час: 30776 мс

30000 мс !! Це майже в 1000 разів повільніше, ніж несинхронна версія. А SQL Server Profiler повідомляє, що виконання запиту зайняло більше 10 секунд. Це навіть не пояснює, куди пішли інші 20 секунд!

Потім я повернувся до версії синхронізації, а також погрався з розміром пакета, і хоча це мало вплинуло на час виконання, це ніде не було таким драматичним, як у асинхронній версії.

Як сиденот, якщо він додає у значення лише невеликий рядок (<100 байт), виконання асинхронного запиту відбувається так само швидко, як і версія синхронізації (результат в 1 або 2 мс).

Мене це справді бентежить, тим більше, що я використовую вбудований SqlConnection, навіть не ORM. Також під час пошуку навколо я не знайшов нічого, що могло б пояснити таку поведінку. Будь-які ідеї?


5
@hcd 1,5 МБ ????? І ви запитаєте, чому отримання, яке стає повільнішим із зменшенням розміру пакета? Особливо, коли ви використовуєте неправильний запит для BLOB?
Панайотис Канавос,

3
@PanagiotisKanavos Це просто гралося від імені ОП. Насправді питання полягає в тому, чому асинхронізація набагато повільніша в порівнянні з синхронізацією з однаковим розміром пакета.
Філдор

2
Перевірте Змінення великих (макс.) Даних у ADO.NET для правильного способу отримання CLOB та BLOB. Замість того, щоб намагатися читати їх як одну велику цінність, використовуйте GetSqlCharsабо GetSqlBinaryотримуйте їх у потоковому режимі. Також розгляньте можливість їх збереження як даних FILESTREAM - немає жодних причин зберігати 1,5 МБ даних на сторінці даних таблиці
Панайотис Канавос

8
@PanagiotisKanavos Це не правильно. OP записує синхронізацію: 20-30 мс та асинхронізацію з усім іншим тим же 1800 мс. Ефект від зміни розміру пакету абсолютно чіткий і очікуваний.
Fildor

5
@hcd, здається, ви можете видалити частину про ваші спроби змінити розміри упаковки, оскільки це здається неактуальним для проблеми та викликає плутанину у деяких коментаторів.
Kuba Wyrostek

Відповіді:


140

У системі без значного навантаження асинхронний виклик має трохи більші накладні витрати. Хоча сама операція вводу-виводу є асинхронною незалежно, блокування може бути швидшим, ніж перемикання завдань пулу потоків.

Скільки накладних витрат? Давайте розглянемо ваші часові номери. 30 мс для блокуючого дзвінка, 450 мс для асинхронного дзвінка. Розмір пакета розміром 32 кілобайт означає, що вам потрібно близько п’ятдесяти окремих операцій вводу-виводу. Це означає, що ми маємо приблизно 8 мс накладних витрат на кожен пакет, що досить добре відповідає вашим вимірам для різних розмірів пакетів. Це не звучить як накладні витрати лише від того, що вони є асинхронними, хоча асинхронні версії повинні виконувати набагато більше роботи, ніж синхронні. Здається, синхронна версія - це (спрощена) 1 запит -> 50 відповідей, тоді як асинхронна версія - 1 запит -> 1 відповідь -> 1 запит -> 1 відповідь -> ..., оплачуючи витрати знову і знову знову.

Заглиблюючись. ExecuteReaderпрацює так само добре, як ExecuteReaderAsync. Наступна операція Readсупроводжується GetFieldValue- і там відбувається щось цікаве. Якщо будь-який з двох є асинхронним, вся робота повільна. Отож, звичайно, відбувається щось зовсім інше, як тільки ви починаєте робити речі по-справжньому асинхронними - a Readбуде швидким, а потім асинхронізація GetFieldValueAsyncбуде повільним, або ви можете починати з повільного ReadAsync, а потім і те, GetFieldValueі інше GetFieldValueAsyncшвидко. Перше асинхронне зчитування з потоку відбувається повільно, і повільність повністю залежить від розміру цілого рядка. Якщо додати кілька рядків одного і того ж розміру, читаючи кожен рядок займає стільки ж часу , як якщо б я тільки один рядок, так що очевидно , що дані євсе ще передається в рядку за рядком - просто, схоже, воліє читати весь рядок відразу, як тільки ви починаєте будь-яке асинхронне читання. Якщо я читаю перший рядок асинхронно, а другий синхронно - другий рядок, який читається, буде знову швидким.

Тож ми можемо побачити, що проблема полягає у великому розмірі окремого рядка та / або стовпця. Не має значення, скільки у вас даних загалом - асинхронне читання мільйона маленьких рядків відбувається так само швидко, як і синхронно. Але додайте лише одне поле, яке занадто велике, щоб вмістити його в один пакет, і ви таємниче несете витрати на асинхронне читання цих даних - ніби кожному пакету потрібен окремий пакет запитів, і сервер не може просто надіслати всі дані в один раз. Використання CommandBehavior.SequentialAccessпокращує продуктивність, як очікувалося, але все ще існує значний розрив між синхронізацією та асинхронізацією.

Найкращий показник у мене був, коли я все це робив належним чином. Це означає використовувати CommandBehavior.SequentialAccess, а також прямо передавати дані:

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

Завдяки цьому різницю між синхронізацією та асинхронізацією стає важко виміряти, і зміна розміру пакета більше не спричиняє смішних накладних витрат, як раніше.

Якщо вам потрібна хороша продуктивність у крайових випадках, переконайтеся, що використовуєте найкращі доступні інструменти - у цьому випадку передавайте дані великих стовпців, а не покладайтесь на помічників, таких як ExecuteScalarабо GetFieldValue.


3
Чудова відповідь. Відтворено сценарій OP. Для цього 1,5-міліметрового рядка згадується OP, я отримую 130 мс для версії синхронізації проти 2200 мс для асинхронізації. З вашим підходом, виміряний час для струни 1,5 м становить 60 мс, непогано.
Wiktor Zychla

4
Хороші розвідки там, плюс я вивчив кілька інших прийомів налаштування нашого DAL-коду.
Адам Голдсворт,

Щойно повернувся в офіс і спробував код на моєму прикладі замість ExecuteScalarAsync, але я все одно отримав 30-секундний час виконання з розміром пакета 512 байт :(
hcd

6
Ага, це врешті-решт спрацювало :) Але я повинен додати CommandBehavior.SequentialAccess до цього рядка: using (var reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
hcd

@hcd Мій погано, у мене це було в тексті, але не в зразку коду :)
Luaan
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.