Letzte Woche habe ich über die Einschränkungen von Always Encrypted sowie die Auswirkungen auf die Leistung geschrieben. Ich wollte ein Follow-up posten, nachdem ich weitere Tests durchgeführt habe, hauptsächlich aufgrund der folgenden Änderungen:
- Ich habe einen Test für lokal hinzugefügt, um zu sehen, ob der Netzwerk-Overhead signifikant war (zuvor war der Test nur remote). Allerdings sollte ich „Netzwerk-Overhead“ in Anführungszeichen setzen, da es sich um zwei VMs auf demselben physischen Host handelt, also nicht wirklich um eine echte Bare-Metal-Analyse.
- Ich habe der Tabelle ein paar zusätzliche (nicht verschlüsselte) Spalten hinzugefügt, um sie realistischer zu machen (aber nicht wirklich so realistisch).
DateCreated DATETIME NOT NULL DEFAULT SYSUTCDATETIME(), DateModified DATETIME NOT NULL DEFAULT SYSUTCDATETIME(), IsActive BIT NOT NULL DEFAULT 1
Dann das Abrufverfahren entsprechend geändert:
ALTER PROCEDURE dbo.RetrievePeople AS BEGIN SET NOCOUNT ON; SELECT TOP (100) LastName, Salary, DateCreated, DateModified, Active FROM dbo.Employees ORDER BY NEWID(); END GO
- Eine Prozedur hinzugefügt, um die Tabelle zu kürzen (früher machte ich das manuell zwischen den Tests):
CREATE PROCEDURE dbo.Cleanup AS BEGIN SET NOCOUNT ON; TRUNCATE TABLE dbo.Employees; END GO
- Eine Prozedur zum Aufzeichnen von Timings hinzugefügt (vorher habe ich die Konsolenausgabe manuell analysiert):
USE Utility; GO CREATE TABLE dbo.Timings ( Test NVARCHAR(32), InsertTime INT, SelectTime INT, TestCompleted DATETIME NOT NULL DEFAULT SYSUTCDATETIME(), HostName SYSNAME NOT NULL DEFAULT HOST_NAME() ); GO CREATE PROCEDURE dbo.AddTiming @Test VARCHAR(32), @InsertTime INT, @SelectTime INT AS BEGIN SET NOCOUNT ON; INSERT dbo.Timings(Test,InsertTime,SelectTime) SELECT @Test,@InsertTime,@SelectTime; END GO
- Ich habe ein Paar Datenbanken hinzugefügt, die Seitenkomprimierung verwendeten – wir alle wissen, dass verschlüsselte Werte nicht gut komprimiert werden, aber dies ist eine polarisierende Funktion, die einseitig sogar auf Tabellen mit verschlüsselten Spalten verwendet werden kann, also dachte ich, ich würde es einfach tun profilieren Sie diese auch. (Und fügte
App.Config
zwei weitere Verbindungszeichenfolgen hinzu .)<connectionStrings> <add name="Normal" connectionString="...;Initial Catalog=Normal;"/> <add name="Encrypt" connectionString="...;Initial Catalog=Encrypt;Column Encryption Setting=Enabled;"/> <add name="NormalCompress" connectionString="...;Initial Catalog=NormalCompress;"/> <add name="EncryptCompress" connectionString="...;Initial Catalog=EncryptCompress;Column Encryption Setting=Enabled;"/> </connectionStrings>
- Ich habe viele Verbesserungen am C#-Code vorgenommen (siehe Anhang), basierend auf dem Feedback von Tobi (das zu dieser Code-Review-Frage führte) und einiger großartiger Unterstützung von Kollegin Brooke Philpott (@Macromullet). Dazu gehörten:
- Beseitigung der gespeicherten Prozedur zur Generierung zufälliger Namen/Gehälter, und dies stattdessen in C#
- mit
Stopwatch
statt plumper Datums-/Uhrzeit-Strings - konsequentere Verwendung von
using()
und Eliminierung von.Close()
- etwas bessere Namenskonventionen (und Kommentare!)
- Ändern von
while
Schleifen zufor
Schleifen - unter Verwendung eines
StringBuilder
statt naiver Verkettung (die ich anfangs bewusst gewählt hatte) - Konsolidieren der Verbindungszeichenfolgen (obwohl ich immer noch absichtlich eine neue Verbindung innerhalb jeder Schleifeniteration herstelle)
Dann erstellte ich eine einfache Stapeldatei, die jeden Test fünfmal ausführte (und dies sowohl auf dem lokalen als auch auf dem entfernten Computer wiederholte):
for /l %%x in (1,1,5) do ( ^ AEDemoConsole "Normal" & ^ AEDemoConsole "Encrypt" & ^ AEDemoConsole "NormalCompress" & ^ AEDemoConsole "EncryptCompress" & ^ )
Nach Abschluss der Tests wäre das Messen der Dauer und des verwendeten Speicherplatzes trivial (und das Erstellen von Diagrammen aus den Ergebnissen würde nur ein wenig Manipulation in Excel erfordern):
-- duration SELECT HostName, Test, AvgInsertTime = AVG(1.0*InsertTime), AvgSelectTime = AVG(1.0*SelectTime) FROM Utility.dbo.Timings GROUP BY HostName, Test ORDER BY HostName, Test; -- space USE Normal; -- NormalCompress; Encrypt; EncryptCompress; SELECT COUNT(*)*8.192 FROM sys.dm_db_database_page_allocations(DB_ID(), OBJECT_ID(N'dbo.Employees'), NULL, NULL, N'LIMITED');
Dauerergebnisse
Hier sind die Rohergebnisse der obigen Dauerabfrage (CANUCK
der Name des Computers ist, der die Instanz von SQL Server hostet, und HOSER
ist die Maschine, auf der die Remote-Version des Codes ausgeführt wurde):
Rohergebnisse der Dauerabfrage
Offensichtlich wird es einfacher sein, es in einer anderen Form zu visualisieren. Wie das erste Diagramm zeigt, hatte der Fernzugriff einen erheblichen Einfluss auf die Dauer der Einfügungen (Steigerung um über 40 %), aber die Komprimierung hatte überhaupt keine Auswirkungen. Allein die Verschlüsselung hat die Dauer für jede Testkategorie ungefähr verdoppelt:
Dauer (Millisekunden) zum Einfügen von 100.000 Zeilen
Bei den Lesevorgängen hatte die Komprimierung einen viel größeren Einfluss auf die Leistung als die Verschlüsselung oder das Lesen der Daten aus der Ferne:
Dauer (Millisekunden) zum 1.000-maligen Lesen von 100 zufälligen Zeilen
Space-Ergebnisse
Wie Sie vielleicht vorhergesagt haben, kann die Komprimierung den zum Speichern dieser Daten erforderlichen Speicherplatz erheblich reduzieren (ungefähr halbieren), während die Verschlüsselung die Datengröße in die entgegengesetzte Richtung beeinflusst (fast verdreifacht). Und natürlich lohnt es sich nicht, verschlüsselte Werte zu komprimieren:
Verwendeter Speicherplatz (KB) zum Speichern von 100.000 Zeilen mit oder ohne Komprimierung und mit oder ohne Verschlüsselung
Zusammenfassung
Dies sollte Ihnen eine ungefähre Vorstellung davon geben, welche Auswirkungen bei der Implementierung von Always Encrypted zu erwarten sind. Denken Sie jedoch daran, dass dies ein sehr spezieller Test war und dass ich einen frühen CTP-Build verwendet habe. Ihre Daten und Zugriffsmuster können zu sehr unterschiedlichen Ergebnissen führen, und weitere Fortschritte in zukünftigen CTPs und Aktualisierungen des .NET Framework können einige dieser Unterschiede sogar in diesem Test verringern.
Sie werden auch feststellen, dass die Ergebnisse hier durch die Bank etwas anders waren als in meinem vorherigen Beitrag. Dies kann erklärt werden:
- Die Einfügezeiten waren in allen Fällen schneller, weil ich keinen zusätzlichen Hin- und Rückweg zur Datenbank mehr brauche, um den zufälligen Namen und das Gehalt zu generieren.
- Die Auswahlzeiten waren in allen Fällen schneller, da ich keine schlampige Methode der Zeichenfolgenverkettung mehr verwende (die als Teil der Dauermetrik enthalten war).
- Der verwendete Platz war in beiden Fällen etwas größer, ich vermute, weil eine unterschiedliche Verteilung der zufällig generierten Zeichenfolgen auftritt.
Anhang A – C#-Konsolenanwendungscode
using System; using System.Configuration; using System.Text; using System.Data; using System.Data.SqlClient; namespace AEDemo { class AEDemo { static void Main(string[] args) { // set up a stopwatch to time each portion of the code var timer = System.Diagnostics.Stopwatch.StartNew(); // random object to furnish random names/salaries var random = new Random(); // connect based on command-line argument var connectionString = ConfigurationManager.ConnectionStrings[args[0]].ToString(); using (var sqlConnection = new SqlConnection(connectionString)) { // this simply truncates the table, which I was previously doing manually using (var sqlCommand = new SqlCommand("dbo.Cleanup", sqlConnection)) { sqlConnection.Open(); sqlCommand.ExecuteNonQuery(); } } // first, generate 100,000 name/salary pairs and insert them for (int i = 1; i <= 100000; i++) { // random salary between 32750 and 197500 var randomSalary = random.Next(32750, 197500); // random string of random number of characters var length = random.Next(1, 32); char[] randomCharArray = new char[length]; for (int byteOffset = 0; byteOffset < length; byteOffset++) { randomCharArray[byteOffset] = (char)random.Next(65, 90); // A-Z } var randomName = new string(randomCharArray); // this stored procedure accepts name and salary and writes them to table // in the databases with encryption enabled, SqlClient encrypts here // so in a trace you would see @LastName = 0xAE4C12..., @Salary = 0x12EA32... using (var sqlConnection = new SqlConnection(connectionString)) { using (var sqlCommand = new SqlCommand("dbo.AddEmployee", sqlConnection)) { sqlCommand.CommandType = CommandType.StoredProcedure; sqlCommand.Parameters.Add("@LastName", SqlDbType.NVarChar, 32).Value = randomName; sqlCommand.Parameters.Add("@Salary", SqlDbType.Int).Value = randomSalary; sqlConnection.Open(); sqlCommand.ExecuteNonQuery(); } } } // capture the timings timer.Stop(); var timeInsert = timer.ElapsedMilliseconds; timer.Reset(); timer.Start(); var placeHolder = new StringBuilder(); for (int i = 1; i <= 1000; i++) { using (var sqlConnection = new SqlConnection(connectionString)) { // loop through and pull 100 rows, 1,000 times using (var sqlCommand = new SqlCommand("dbo.RetrieveRandomEmployees", sqlConnection)) { sqlCommand.CommandType = CommandType.StoredProcedure; sqlConnection.Open(); using (var sqlDataReader = sqlCommand.ExecuteReader()) { while (sqlDataReader.Read()) { // do something tangible with the output placeHolder.Append(sqlDataReader[0].ToString()); } } } } } // capture timings again, write both to db timer.Stop(); var timeSelect = timer.ElapsedMilliseconds; using (var sqlConnection = new SqlConnection(connectionString)) { using (var sqlCommand = new SqlCommand("Utility.dbo.AddTiming", sqlConnection)) { sqlCommand.CommandType = CommandType.StoredProcedure; sqlCommand.Parameters.Add("@Test", SqlDbType.NVarChar, 32).Value = args[0]; sqlCommand.Parameters.Add("@InsertTime", SqlDbType.Int).Value = timeInsert; sqlCommand.Parameters.Add("@SelectTime", SqlDbType.Int).Value = timeSelect; sqlConnection.Open(); sqlCommand.ExecuteNonQuery(); } } } } }