Database
 sql >> Datenbank >  >> RDS >> Database

Immer verschlüsselte Leistung:Ein Follow-Up

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 zu for 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();
                }
            }
        }
    }
}