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

Prestazioni sempre crittografate:un follow-up

La scorsa settimana ho scritto dei limiti di Always Encrypted e dell'impatto sulle prestazioni. Volevo pubblicare un follow-up dopo aver eseguito più test, principalmente a causa delle seguenti modifiche:

  • Ho aggiunto un test per locale, per vedere se l'overhead di rete era significativo (in precedenza, il test era solo remoto). Tuttavia, dovrei mettere "overhead di rete" tra virgolette aeree, perché si tratta di due macchine virtuali sullo stesso host fisico, quindi non proprio una vera analisi bare metal.
  • Ho aggiunto alcune colonne extra (non crittografate) alla tabella per renderla più realistica (ma non così realistica).
      DateCreated  DATETIME NOT NULL DEFAULT SYSUTCDATETIME(),
      DateModified DATETIME NOT NULL DEFAULT SYSUTCDATETIME(),
      IsActive     BIT NOT NULL DEFAULT 1

    Quindi ha modificato di conseguenza la procedura di recupero:

    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
  • Aggiunta una procedura per troncare la tabella (in precedenza lo facevo manualmente tra i test):
    CREATE PROCEDURE dbo.Cleanup
    AS
    BEGIN
      SET NOCOUNT ON;
      TRUNCATE TABLE dbo.Employees;
    END
    GO
  • Aggiunta una procedura per la registrazione dei tempi (in precedenza stavo analizzando manualmente l'output della console):
    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
  • Ho aggiunto una coppia di database che utilizzavano la compressione della pagina:sappiamo tutti che i valori crittografati non si comprimono bene, ma questa è una caratteristica polarizzante che può essere utilizzata unilateralmente anche su tabelle con colonne crittografate, quindi ho pensato di profila anche questi. (E aggiunto altre due stringhe di connessione a App.Config .)
    <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>
  • Ho apportato molti miglioramenti al codice C# (vedi l'Appendice) sulla base del feedback di tobi (che ha portato a questa domanda sulla revisione del codice) e dell'ottima assistenza del collega Brooke Philpott (@Macromullet). Questi includevano:
    • eliminando la procedura memorizzata per generare nomi/stipendi casuali e facendolo invece in C#
    • usando Stopwatch invece di scadenti stringhe di data/ora
    • uso più coerente di using() ed eliminazione di .Close()
    • convenzioni di denominazione (e commenti!) leggermente migliori
    • modifica while passa a for loop
    • utilizzando un StringBuilder invece della concatenazione ingenua (che inizialmente avevo scelto intenzionalmente)
    • consolidare le stringhe di connessione (anche se sto ancora creando intenzionalmente una nuova connessione all'interno di ogni iterazione del ciclo)

Quindi ho creato un semplice file batch che avrebbe eseguito ogni test 5 volte (e l'ho ripetuto sia sul computer locale che su quello remoto):

for /l %%x in (1,1,5) do (        ^
AEDemoConsole "Normal"          & ^
AEDemoConsole "Encrypt"         & ^
AEDemoConsole "NormalCompress"  & ^
AEDemoConsole "EncryptCompress" & ^
)

Dopo che i test sono stati completati, misurare le durate e lo spazio utilizzato sarebbe banale (e costruire grafici dai risultati richiederebbe solo una piccola manipolazione in Excel):

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

Risultati della durata

Ecco i risultati grezzi della query sulla durata sopra (CANUCK è il nome della macchina che ospita l'istanza di SQL Server e HOSER è la macchina che ha eseguito la versione remota del codice):

Risultati non elaborati della query sulla durata

Ovviamente sarà più facile visualizzare in un'altra forma. Come mostrato nel primo grafico, l'accesso remoto ha avuto un impatto significativo sulla durata degli inserti (aumento di oltre il 40%), ma la compressione ha avuto un impatto minimo. La sola crittografia ha approssimativamente raddoppiato la durata di qualsiasi categoria di test:

Durata (millisecondi) per inserire 100.000 righe

Per le letture, la compressione ha avuto un impatto molto maggiore sulle prestazioni rispetto alla crittografia o alla lettura dei dati in remoto:

Durata (millisecondi) per leggere 100 righe casuali 1000 volte

Risultati spaziali

Come avrai previsto, la compressione può ridurre significativamente la quantità di spazio richiesta per archiviare questi dati (circa della metà), mentre la crittografia può influire sulla dimensione dei dati nella direzione opposta (quasi triplicandola). E, naturalmente, la compressione dei valori crittografati non dà i suoi frutti:

Spazio utilizzato (KB) per memorizzare 100.000 righe con o senza compressione e con o senza crittografia

Riepilogo

Questo dovrebbe darti un'idea approssimativa di cosa aspettarsi dall'impatto quando si implementa Always Encrypted. Tieni presente, tuttavia, che questo è stato un test molto particolare e che stavo usando una prima build CTP. I tuoi dati e i tuoi modelli di accesso potrebbero produrre risultati molto diversi e ulteriori progressi nei futuri CTP e aggiornamenti a .NET Framework potrebbero ridurre alcune di queste differenze anche in questo stesso test.

Noterai anche che i risultati qui erano leggermente diversi su tutta la linea rispetto al mio post precedente. Questo può essere spiegato:

  • I tempi di inserimento sono stati in tutti i casi più rapidi perché non devo più sostenere un viaggio di andata e ritorno aggiuntivo nel database per generare il nome e lo stipendio casuali.
  • I tempi di selezione sono stati in tutti i casi più rapidi perché non utilizzo più un metodo sciatto di concatenazione delle stringhe (che era incluso come parte della metrica della durata).
  • Lo spazio utilizzato era leggermente maggiore in entrambi i casi, sospetto a causa di una diversa distribuzione delle stringhe casuali generate.

Appendice A – Codice dell'applicazione console C#

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