Schreckliche Leistung bei der Verwendung von SqlCommand-Async-Methoden
Bin ich mit großen SQL-performance-Probleme bei der Verwendung von asynchronen Aufrufe. Ich habe eine kleine Falle um das problem zu demonstrieren.
Habe ich erstellen Sie eine Datenbank auf einem SQL Server-2016, welche sich in unserem LAN (also nicht eine localDB).
In dieser Datenbank habe ich eine Tabelle WorkingCopy
mit 2 Spalten:
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]
In dieser Tabelle, die ich eingefügt haben, die einen einzelnen Datensatz (id
='PerfUnitTest', Value
ist ein 1,5 mb string (zip einer größeren JSON-Datensatz)).
Nun, wenn ich die Abfrage ausführen, die in SSMS :
SELECT [Value]
FROM [Workingcopy]
WHERE id = 'perfunittest'
Bekomme ich sofort das Ergebnis, und ich sehe in SQL Servre Profiler, dass der Zeitpunkt der Ausführung war rund 20 Millisekunden. Alle normal.
Beim ausführen der Abfrage aus .NET (4.6) code mit einem einfachen 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;
Die Ausführungszeit für diese ist auch etwa 20-30 Millisekunden.
Aber wenn es zu verändern async-code :
string value = await command.ExecuteScalarAsync() as string;
Die Ausführungszeit ist plötzlich 1800 ms ! Auch in SQL Server Profiler, sehe ich, dass die Ausführung der Abfrage Dauer mehr als eine Sekunde. Obwohl der ausgeführten query berichtet von der profiler ist genau die gleiche wie die nicht-Async-version.
Aber es kommt noch schlimmer. Wenn ich spielen, um mit der Paketgröße in der Verbindungszeichenfolge, bekomme ich folgende Ergebnisse :
Packet size-32768 : [TIMING]: ExecuteScalarAsync in SqlValueStore ->
verstrichene Zeit : 450 msPaketgröße von 4096 : [TIMING]: ExecuteScalarAsync in SqlValueStore ->
verstrichene Zeit : 3667 msPacket size 512 : [TIMING]: ExecuteScalarAsync in SqlValueStore ->
verstrichene Zeit : 30776 ms
30,000 ms!! Das ist über 1000 mal langsamer als die nicht-async-version. Und SQL Server Profiler-Berichte, die die Ausführung der Abfrage dauerte über 10 Sekunden. Das auch nicht erklären, wo die anderen 20 Sekunden sind Weg!
Dann hab ich wieder eingeschaltet, um die sync-version und auch rumprobiert mit der Paket-Größe, und obwohl es hat Auswirkungen ein wenig der Zeitpunkt der Ausführung, der war nicht so dramatisch, wie mit dem async-version.
Nebenbei, wenn es nur eine kleine Zeichenkette (< 100bytes) in den Wert, der asynchrone Ausführung der Abfragen ist genauso schnell wie die sync-version (Ergebnis in 1 oder 2 ms).
Ich bin wirklich verblüfft über diese, vor allem, da ich die eingebaute SqlConnection
, nicht einmal ein ORM. Auch bei der Suche rund um, ich fand nichts, was erklären könnte dieses Verhalten. Irgendwelche Ideen?
Das war nur Herumspielen im Auftrag von OP. Die eigentliche Frage ist, warum async ist so viel langsamer im Vergleich zur Synchronisierung mit dieselbe Paket-Größe.
Check Ändern von Großem Wert (max) - Daten in ADO.NET für die richtige Möglichkeit zum abrufen von CLOBs und BLOBs. Statt, zu versuchen, Sie zu Lesen, als einen großen Wert, nutzen
GetSqlChars
oder GetSqlBinary
abrufen, die Ihnen im streaming-Mode. Berücksichtigen Sie auch speichern Sie Sie als FILESTREAM-Daten - es gibt keinen Grund zu sparen, 1.5 MB an Daten in die Daten der Tabelle SeiteDas ist nicht korrekt. OP schreibt sync : 20-30 ms und async mit sonst alles gleich 1800 ms. Die Wirkung der änderung der Paketgröße ist völlig klar und zu erwarten.
es scheint, dass Sie konnten, entfernen Sie den Teil über Ihre versuche, ändern die Packungsgrößen, da es scheint irrelevant für das problem und führt zu Verwirrung bei einigen Kommentatoren.
InformationsquelleAutor hcd | 2017-02-23
Du musst angemeldet sein, um einen Kommentar abzugeben.
Auf einem system ohne große Last, einen asynchronen Aufruf hat einen etwas größeren Aufwand. Während die I/O-operation selbst ist asynchron, unabhängig, die Blockierung kann schneller sein als die thread-pool-Aufgabe wechseln.
Wie viel Aufwand? Let ' s look at Ihr timing zahlen. 30ms für einen blockierenden Aufruf, 450ms für einen asynchronen Aufruf. 32 kiB Paket-Größe bedeutet, dass Sie brauchen, die Sie brauchen etwa fünfzig einzelne I/O-Operationen. Das heißt, wir haben etwa 8ms von overhead auf jedes Paket, das entspricht ziemlich gut mit Ihren Messungen in verschiedenen Paketgrößen. Das klingt nicht overhead nur von asynchronen, auch wenn die asynchronen Versionen müssen tun viel mehr Arbeit als die synchrone. Es klingt wie die synchron-version ist (vereinfacht) 1 Anfrage -> 50 Antworten, während die asynchrone version endet als 1 Anfrage -> 1 Antwort -> 1 Anfrage -> 1 Antwort -> ..., die Zahlung der Kosten immer und immer wieder.
Tiefer gehen.
ExecuteReader
funktioniert genauso gut wieExecuteReaderAsync
. Die nächste operation istRead
gefolgt von einemGetFieldValue
- und eine interessante Sache, die da passiert. Wenn eine der beiden ist async, der ganze Vorgang ist langsam. Also es ist sicherlich etwas, was sehr anderen geschieht, sobald Sie beginnen, die Dinge wirklich asynchron - eineRead
schnell, und dann die async -GetFieldValueAsync
wird langsam sein, oder Sie können beginnen mit dem langsamenReadAsync
, und dann beideGetFieldValue
undGetFieldValueAsync
sind schnell. Die ersten asynchronen Lesen aus dem stream ist langsam, und die Langsamkeit, hängt ganz von der Größe der gesamten Zeile. Wenn ich mehr Zeilen in der gleichen Größe, Lesen jeder Zeile nimmt die gleiche Menge an Zeit, als wenn ich nur eine Zeile, so ist es offensichtlich, dass die Daten ist noch gestreamt werden Zeile für Zeile - es scheint nur zu bevorzugen, um Lesen Sie die gesamte Zeile auf einmal, sobald Sie beginnen alle asynchrone Lesen. Wenn ich lese die erste Zeile asynchron, und die zweiten synchron - die zweite Zeile gelesen wird, dann wird schnell wieder.So können wir sehen, dass das problem ist eine große Größe einer einzelnen Zeile und/oder Spalte. Es spielt keine Rolle, wie viele Daten Sie haben insgesamt Lesen eine million kleine Zeilen asynchron ist genau so schnell wie synchron. Aber fügen Sie nur ein einzelnes Feld zu groß, um zu passen in ein einziges Paket, und Sie auf mysteriöse Weise fallen Kosten an asynchron zu Lesen, dass die Daten - als wenn jedes Paket benötigt ein trennen-request-Paket und der server konnte nicht einfach senden Sie alle Daten auf einmal. Mit
CommandBehaviour.SequentialAccess
keine Verbesserung der performance, wie erwartet, aber die riesige Lücke zwischen sync und async ist noch vorhanden.Die beste Leistung, die ich bekam war, wenn macht die ganze Sache richtig. Das bedeutet, dass mit
CommandBehaviour.SequentialAccess
sowie streaming der Daten explizit:Mit dabei, der Unterschied zwischen sync und async wird schwer zu Messen, und die änderung der Paketgröße nicht mehr anfallen, die lächerlichen Aufwand als zuvor.
Wenn Sie möchten, eine gute Leistung in Rand-Fällen, stellen Sie sicher, dass Sie die besten verfügbaren Werkzeuge - in diesem Fall-stream große Spalte Daten, anstatt sich auf Helfer wie
ExecuteScalar
oderGetFieldValue
.Gute Untersuchungen gibt es, und ich lernte eine Handvoll anderer Techniken optimieren für unsere DAL code.
Danke für diese hervorragende Erklärung 🙂
Gerade zurück ins Büro und versucht, den code auf mein Beispiel statt der ExecuteScalarAsync, aber ich habe immer noch 30seconds Ausführungszeit mit 512byte Paket-Größe 🙁
Aha, es hat funktioniert, nachdem alle 🙂 Aber ich muss hinzufügen das CommandBehavior.SequentialAccess zu dieser Zeile :
using (var reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
InformationsquelleAutor Luaan