Sqlserver
 sql >> Datenbank >  >> RDS >> Sqlserver

SQL Server 2016:Auswirkungen von Always Encrypted auf die Leistung

Im Rahmen von T-SQL Tuesday #69 habe ich über die Einschränkungen von Always Encrypted gebloggt und dort erwähnt, dass die Leistung durch seine Verwendung negativ beeinflusst werden könnte (wie Sie vielleicht erwarten, hat eine stärkere Sicherheit oft Kompromisse). In diesem Beitrag wollte ich einen kurzen Blick darauf werfen, wobei ich (erneut) im Hinterkopf behalten sollte, dass diese Ergebnisse auf CTP 2.2-Code basieren, also sehr früh im Entwicklungszyklus, und nicht unbedingt die Leistung widerspiegeln, die Sie werden siehe kommen RTM.

Zunächst wollte ich demonstrieren, dass Always Encrypted von Clientanwendungen aus funktioniert, auch wenn dort nicht die neueste Version von SQL Server 2016 installiert ist. Sie müssen jedoch die Vorschauversion von .NET Framework 4.6 installieren (neueste Version hier, und das kann sich ändern), um die Column Encryption Setting zu unterstützen Connection-String-Attribut. Wenn Sie Windows 10 ausführen oder Visual Studio 2015 installiert haben, ist dieser Schritt nicht erforderlich, da Sie bereits über eine ausreichend aktuelle Version von .NET Framework verfügen sollten.

Als Nächstes müssen Sie sicherstellen, dass das Always Encrypted-Zertifikat auf allen Clients vorhanden ist. Sie erstellen die Master- und Spaltenverschlüsselungsschlüssel innerhalb der Datenbank, wie es Ihnen jedes Always Encrypted-Tutorial zeigen wird, dann müssen Sie das Zertifikat von diesem Computer exportieren und es auf den anderen Computern importieren, auf denen Anwendungscode ausgeführt wird. Öffnen Sie certmgr.msc , und erweitern Sie Zertifikate – Aktueller Benutzer> Persönlich> Zertifikate, und dort sollte eines namens Always Encrypted Certificate vorhanden sein . Klicken Sie mit der rechten Maustaste darauf, wählen Sie Alle Aufgaben> Exportieren und folgen Sie den Anweisungen. Ich habe den privaten Schlüssel exportiert und ein Passwort angegeben, wodurch eine .pfx-Datei erstellt wurde. Dann wiederholen Sie einfach den umgekehrten Vorgang auf den Client-Rechnern:Öffnen Sie certmgr.msc , erweitern Sie Zertifikate – Aktueller Benutzer> Persönlich, klicken Sie mit der rechten Maustaste auf Zertifikate, wählen Sie Alle Aufgaben> Importieren und zeigen Sie auf die oben erstellte .pfx-Datei. (Offizielle Hilfe hier.)

(Es gibt sicherere Möglichkeiten, diese Zertifikate zu verwalten – es ist unwahrscheinlich, dass Sie das Zertifikat einfach so auf allen Computern bereitstellen möchten, da Sie sich bald fragen werden, was der Sinn war? Ich habe dies nur in meiner isolierten Umgebung getan für die Zwecke dieser Demo – ich wollte sicherstellen, dass meine Anwendung Daten über die Leitung und nicht nur im lokalen Speicher abruft.)

Wir erstellen zwei Datenbanken, eine mit verschlüsselter Tabelle und eine ohne. Wir tun dies, um Verbindungszeichenfolgen zu isolieren und auch die Speicherplatznutzung zu messen. Natürlich gibt es detailliertere Möglichkeiten, um zu steuern, welche Befehle eine verschlüsselungsfähige Verbindung verwenden müssen – siehe den Hinweis mit dem Titel „Steuern der Auswirkungen auf die Leistung…“ in diesem Artikel.

Die Tabellen sehen so aus:

-- encrypted copy, in database Encrypted
 
CREATE TABLE dbo.Employees
(
  ID INT IDENTITY(1,1) PRIMARY KEY,
  LastName NVARCHAR(32) COLLATE Latin1_General_BIN2 
    ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC,
	ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256',
	COLUMN_ENCRYPTION_KEY = ColumnKey) NOT NULL,
  Salary INT
    ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED,
	ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256',
	COLUMN_ENCRYPTION_KEY = ColumnKey) NOT NULL
);
 
-- unencrypted copy, in database Normal
 
CREATE TABLE dbo.Employees
(
  ID INT IDENTITY(1,1) PRIMARY KEY,
  LastName NVARCHAR(32) COLLATE Latin1_General_BIN2 NOT NULL,
  Salary INT NOT NULL
);

Mit diesen Tabellen wollte ich eine sehr einfache Befehlszeilenanwendung einrichten, um die folgenden Aufgaben sowohl für die verschlüsselte als auch für die unverschlüsselte Version der Tabelle auszuführen:

  • Fügen Sie 100.000 Mitarbeiter einzeln ein
  • Lies 100 zufällige Zeilen 1.000 Mal durch
  • Zeitstempel vor und nach jedem Schritt ausgeben

Wir haben also eine gespeicherte Prozedur in einer völlig separaten Datenbank, die verwendet wird, um zufällige Ganzzahlen zur Darstellung von Gehältern und zufällige Unicode-Strings unterschiedlicher Länge zu erzeugen. Wir werden dies einzeln tun, um die reale Nutzung von 100.000 Einfügungen besser zu simulieren, die unabhängig voneinander erfolgen (wenn auch nicht gleichzeitig, da ich nicht mutig genug bin, eine C#-Anwendung mit mehreren Threads richtig zu entwickeln und zu verwalten oder zu versuchen, sie zu koordinieren und mehrere Instanzen einer einzelnen Anwendung synchronisieren).

CREATE DATABASE Utility;
GO
 
USE Utility;
GO
 
CREATE PROCEDURE dbo.GenerateNameAndSalary
  @Name NVARCHAR(32) OUTPUT,
  @Salary INT OUTPUT
AS
BEGIN
  SET NOCOUNT ON;
  SELECT @Name = LEFT(CONVERT(NVARCHAR(32), CRYPT_GEN_RANDOM(64)), RAND() * 32 + 1);
  SELECT @Salary = CONVERT(INT, RAND()*100000)/100*100;
END
GO

Ein paar Zeilen Beispielausgabe (wir kümmern uns nicht um den tatsächlichen Inhalt der Zeichenfolge, nur dass er variiert):

酹2׿ዌ륒㦢㮧羮怰㉤盿⚉嗝䬴敏⽁캘♜鼹䓧
98600
 
贓峂쌄탠❼缉腱蛽☎뱶
72000

Dann werden die gespeicherten Prozeduren, die die Anwendung letztendlich aufruft (diese sind in beiden Datenbanken identisch, da Ihre Abfragen nicht geändert werden müssen, um Always Encrypted zu unterstützen):

CREATE PROCEDURE dbo.AddPerson
  @LastName NVARCHAR(32),
  @Salary INT
AS
BEGIN
  SET NOCOUNT ON;
  INSERT dbo.Employees(LastName, Salary) SELECT @LastName, @Salary;
END
GO
 
CREATE PROCEDURE dbo.RetrievePeople
AS
BEGIN
  SET NOCOUNT ON;
  SELECT TOP (100) ID, LastName, Salary 
    FROM dbo.Employees
    ORDER BY NEWID();
END
GO

Nun der C#-Code, beginnend mit dem connectionStrings-Teil von App.config. Der wichtige Teil ist die Column Encryption Setting Option nur für die Datenbank mit den verschlüsselten Spalten (nehmen Sie der Kürze halber an, dass alle drei Verbindungszeichenfolgen dieselbe Data Source enthalten , und dieselbe SQL-Authentifizierung User ID und Password ):

<connectionStrings>
  <add name="Utility" connectionString="Initial Catalog=Utility;..."/>
  <add name="Normal"  connectionString="Initial Catalog=Normal;..."/>
  <add name="Encrypt" connectionString="Initial Catalog=Encrypted; Column Encryption Setting=Enabled;..."/>
</connectionStrings>

Und Program.cs (sorry, für Demos wie diese bin ich schrecklich darin, Dinge logisch umzubenennen):

using System;
using System.Collections.Generic;
using System.Text;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
 
namespace AEDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (SqlConnection con1 = new SqlConnection())
            {
                Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff"));
                string name;
                string EmptyString = "";
                int salary;
                int i = 1;
                while (i <= 100000)
                {
                    con1.ConnectionString = ConfigurationManager.ConnectionStrings["Utility"].ToString();
                    using (SqlCommand cmd1 = new SqlCommand("dbo.GenerateNameAndSalary", con1))
                    {
                        cmd1.CommandType = CommandType.StoredProcedure;
                        SqlParameter n = new SqlParameter("@Name", SqlDbType.NVarChar, 32) 
                                         { Direction = ParameterDirection.Output };
                        SqlParameter s = new SqlParameter("@Salary", SqlDbType.Int) 
                                         { Direction = ParameterDirection.Output };
                        cmd1.Parameters.Add(n);
                        cmd1.Parameters.Add(s);
                        con1.Open();
                        cmd1.ExecuteNonQuery();
                        name = n.Value.ToString();
                        salary = Convert.ToInt32(s.Value);
                        con1.Close();
                    }
 
                    using (SqlConnection con2 = new SqlConnection())
                    {
                        con2.ConnectionString = ConfigurationManager.ConnectionStrings[args[0]].ToString();
                        using (SqlCommand cmd2 = new SqlCommand("dbo.AddPerson", con2))
                        {
                            cmd2.CommandType = CommandType.StoredProcedure;
                            SqlParameter n = new SqlParameter("@LastName", SqlDbType.NVarChar, 32);
                            SqlParameter s = new SqlParameter("@Salary", SqlDbType.Int);
                            n.Value = name;
                            s.Value = salary;
                            cmd2.Parameters.Add(n);
                            cmd2.Parameters.Add(s);
                            con2.Open();
                            cmd2.ExecuteNonQuery();
                            con2.Close();
                        }
                    }
                    i++;
                }
                Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff"));
                i = 1;
                while (i <= 1000)
                {
                    using (SqlConnection con3 = new SqlConnection())
                    {
                        con3.ConnectionString = ConfigurationManager.ConnectionStrings[args[0]].ToString();
                        using (SqlCommand cmd3 = new SqlCommand("dbo.RetrievePeople", con3))
                        {
                            cmd3.CommandType = CommandType.StoredProcedure;
                            con3.Open();
                            SqlDataReader rdr = cmd3.ExecuteReader();
                            while (rdr.Read())
                            {
                                EmptyString += rdr[0].ToString();
                            }
                            con3.Close();
                        }
                    }
                    i++;
                }
                Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff"));
            }
        }
    }
}

Dann können wir die .exe mit den folgenden Befehlszeilen aufrufen:

AEDemoConsole.exe "Normal"
AEDemoConsole.exe "Encrypt"

Und es werden drei Ausgabezeilen für jeden Aufruf erzeugt:die Startzeit, die Zeit, nachdem 100.000 Zeilen eingefügt wurden, und die Zeit, nachdem 100 Zeilen 1.000 Mal gelesen wurden. Hier sind die Ergebnisse, die ich auf meinem System gesehen habe, gemittelt über jeweils 5 Läufe:

Dauer (Sekunden) des Schreibens und Lesens von Daten

Es gibt einen deutlichen Einfluss auf das Schreiben der Daten – nicht ganz das 2-fache, aber mehr als das 1,5-fache. Beim Auslesen und Entschlüsseln der Daten gab es – zumindest in diesen Tests – ein viel geringeres Delta, aber auch das war nicht kostenlos.

Was die Speicherplatznutzung angeht, gibt es ungefähr eine 3-fache Strafe für das Speichern verschlüsselter Daten (angesichts der Art der meisten Verschlüsselungsalgorithmen sollte dies nicht schockierend sein). Denken Sie daran, dass dies auf einer Tabelle mit nur einem einzigen gruppierten Primärschlüssel war. Hier waren die Zahlen:

Zum Speichern von Daten verwendeter Speicherplatz (MB)

Offensichtlich gibt es bei der Verwendung von Always Encrypted einige Nachteile, wie es normalerweise bei fast allen sicherheitsbezogenen Lösungen der Fall ist (das Sprichwort "kein kostenloses Mittagessen" kommt mir in den Sinn). Ich wiederhole, dass diese Tests mit CTP 2.2 durchgeführt wurden, das sich möglicherweise radikal von der endgültigen Version von SQL Server 2016 unterscheidet. Außerdem spiegeln diese Unterschiede, die ich beobachtet habe, möglicherweise nur die Art der Tests wider, die ich ausgeheckt habe. natürlich hoffe ich, dass Sie diesen Ansatz verwenden können, um Ihre Ergebnisse gegen Ihr Schema, Ihre Hardware und Ihre Datenzugriffsmuster zu testen.