Sqlserver
 sql >> Database >  >> RDS >> Sqlserver

SQL Server 2016:impatto sulle prestazioni di Always Encrypted

Come parte di T-SQL Tuesday n. 69, ho scritto sul blog i limiti di Always Encrypted e ho menzionato che le prestazioni potrebbero essere influenzate negativamente dal suo utilizzo (come ci si potrebbe aspettare, una sicurezza più forte spesso ha dei compromessi). In questo post, ho voluto dare una rapida occhiata a questo, tenendo presente (di nuovo) che questi risultati si basano sul codice CTP 2.2, quindi molto presto nel ciclo di sviluppo, e non riflettono necessariamente le prestazioni che farai vedi vieni RTM.

Innanzitutto, volevo dimostrare che Always Encrypted funziona dalle applicazioni client anche se l'ultima versione di SQL Server 2016 non è installata lì. Tuttavia, devi installare l'anteprima di .NET Framework 4.6 (la versione più recente qui e potrebbe cambiare) per supportare l'Column Encryption Setting attributo della stringa di connessione. Se utilizzi Windows 10 o hai installato Visual Studio 2015, questo passaggio non è necessario, poiché dovresti già disporre di una versione sufficientemente recente di .NET Framework.

Successivamente, devi assicurarti che il certificato Always Encrypted esista su tutti i client. Crei le chiavi di crittografia master e di colonna all'interno del database, come ti mostrerà qualsiasi tutorial Always Encrypted, quindi devi esportare il certificato da quella macchina e importarlo sugli altri in cui verrà eseguito il codice dell'applicazione. Apri certmgr.msc ed espandi Certificati – Utente corrente> Personale> Certificati, e dovrebbe essercene uno chiamato Always Encrypted Certificate . Fare clic con il pulsante destro del mouse, scegliere Tutte le attività> Esporta e seguire le istruzioni. Ho esportato la chiave privata e fornito una password, che ha prodotto un file .pfx. Quindi ripeti semplicemente il processo opposto sui computer client:Apri certmgr.msc , espandi Certificati – Utente corrente> Personale, fai clic con il pulsante destro del mouse su Certificati, scegli Tutte le attività> Importa e puntalo sul file .pfx creato in precedenza. (Guida ufficiale qui.)

(Esistono modi più sicuri per gestire questi certificati:non è probabile che tu voglia semplicemente distribuire il certificato in questo modo su tutte le macchine, poiché presto ti chiederai qual era il punto? Lo stavo facendo solo nel mio ambiente isolato ai fini di questa demo, volevo assicurarmi che la mia applicazione stesse recuperando i dati via cavo e non solo nella memoria locale.)

Creiamo due database, uno con una tabella crittografata e uno senza. Lo facciamo per isolare le stringhe di connessione e anche per misurare l'utilizzo dello spazio. Naturalmente, esistono modi più dettagliati per controllare quali comandi devono utilizzare una connessione abilitata alla crittografia:vedere la nota intitolata "Controllo dell'impatto sulle prestazioni..." in questo articolo.

Le tabelle si presentano così:

-- 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
);

Con queste tabelle a posto, volevo configurare un'applicazione da riga di comando molto semplice per eseguire le seguenti attività sia sulla versione crittografata che su quella non crittografata della tabella:

  • Inserisci 100.000 dipendenti, uno alla volta
  • Leggi 100 righe casuali, 1.000 volte
  • Emetti timestamp prima e dopo ogni passaggio

Quindi abbiamo una procedura memorizzata in un database completamente separato utilizzato per produrre numeri interi casuali per rappresentare gli stipendi e stringhe Unicode casuali di lunghezza variabile. Lo faremo uno alla volta per simulare meglio l'utilizzo reale di 100.000 inserti che si verificano in modo indipendente (anche se non contemporaneamente, poiché non sono abbastanza coraggioso da provare a sviluppare e gestire correttamente un'applicazione C# multi-thread, o provare a coordinare e sincronizza più istanze di una singola applicazione).

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

Un paio di righe di output di esempio (non ci interessa il contenuto effettivo della stringa, solo che varia):

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

Quindi le stored procedure che l'applicazione chiamerà alla fine (queste sono identiche in entrambi i database, poiché le tue query non devono essere modificate per supportare Always Encrypted):

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

Ora, il codice C#, a partire dalla parte connectionStrings di App.config. La parte importante è l'Column Encryption Setting opzione solo per il database con le colonne crittografate (per brevità, supponiamo che tutte e tre le stringhe di connessione contengano la stessa Data Source e la stessa autenticazione SQL User ID e 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>

E Program.cs (scusate, per demo come questa, sono terribile ad entrare e rinominare le cose in modo logico):

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"));
            }
        }
    }
}

Quindi possiamo chiamare il .exe con le seguenti righe di comando:

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

E produrrà tre righe di output per ogni chiamata:l'ora di inizio, l'ora dopo che sono state inserite 100.000 righe e l'ora dopo che 100 righe sono state lette 1.000 volte. Ecco i risultati che ho visto sul mio sistema, con una media di 5 esecuzioni ciascuna:

Durata (secondi) di scrittura e lettura dei dati

C'è un chiaro impatto nella scrittura dei dati:non proprio 2X, ma più di 1,5X. C'era un delta molto più basso nella lettura e nella decrittazione dei dati, almeno in questi test, ma neanche questo era gratuito.

Per quanto riguarda l'utilizzo dello spazio, c'è una penalità di circa 3 volte per l'archiviazione di dati crittografati (data la natura della maggior parte degli algoritmi di crittografia, questo non dovrebbe essere scioccante). Tieni presente che si trovava su una tabella con una sola chiave primaria raggruppata. Ecco le cifre:

Spazio (MB) utilizzato per memorizzare i dati

Quindi ovviamente ci sono alcune penalità con l'utilizzo di Always Encrypted, come in genere ci sono quasi tutte le soluzioni relative alla sicurezza (mi viene in mente il detto "niente pranzo gratis"). Ripeto che questi test sono stati eseguiti contro CTP 2.2, che potrebbe essere radicalmente diverso dalla versione finale di SQL Server 2016. Inoltre, queste differenze che ho osservato possono riflettere solo la natura dei test che ho inventato; ovviamente spero che tu possa usare questo approccio per testare i tuoi risultati rispetto al tuo schema, sul tuo hardware e con i tuoi schemi di accesso ai dati.