sql >> Base de Datos >  >> RDS >> Database

Rendimiento de Always Encrypted:seguimiento

La semana pasada, escribí sobre las limitaciones de Always Encrypted y el impacto en el rendimiento. Quería publicar un seguimiento después de realizar más pruebas, principalmente debido a los siguientes cambios:

  • Agregué una prueba local para ver si la sobrecarga de la red era significativa (anteriormente, la prueba solo era remota). Sin embargo, debería poner "sobrecarga de la red" entre comillas, porque se trata de dos máquinas virtuales en el mismo host físico, por lo que no es realmente un verdadero análisis completo.
  • Agregué algunas columnas adicionales (no encriptadas) a la tabla para hacerla más realista (pero no tanto).
      DateCreated  DATETIME NOT NULL DEFAULT SYSUTCDATETIME(),
      DateModified DATETIME NOT NULL DEFAULT SYSUTCDATETIME(),
      IsActive     BIT NOT NULL DEFAULT 1

    Luego modificó el procedimiento de recuperación en consecuencia:

    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
  • Se agregó un procedimiento para truncar la tabla (anteriormente lo hacía manualmente entre pruebas):
    CREATE PROCEDURE dbo.Cleanup
    AS
    BEGIN
      SET NOCOUNT ON;
      TRUNCATE TABLE dbo.Employees;
    END
    GO
  • Se agregó un procedimiento para registrar tiempos (anteriormente estaba analizando manualmente la salida de la consola):
    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
  • Agregué un par de bases de datos que usaban compresión de página; todos sabemos que los valores cifrados no se comprimen bien, pero esta es una característica polarizadora que se puede usar unilateralmente incluso en tablas con columnas cifradas, así que pensé en simplemente perfila estos también. (Y agregó dos cadenas de conexión más 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>
  • Realicé muchas mejoras en el código de C# (consulte el Apéndice) en función de los comentarios de tobi (lo que llevó a esta pregunta de revisión de código) y una gran ayuda de mi compañero de trabajo Brooke Philpott (@Macromullet). Estos incluyeron:
    • eliminar el procedimiento almacenado para generar nombres/salarios aleatorios y hacerlo en C# en su lugar
    • usando Stopwatch en lugar de torpes cadenas de fecha/hora
    • uso más consistente de using() y eliminación de .Close()
    • Convenciones de nomenclatura ligeramente mejores (¡y comentarios!)
    • cambiando while bucles a for bucles
    • utilizando un StringBuilder en lugar de una concatenación ingenua (que inicialmente había elegido intencionalmente)
    • consolidando las cadenas de conexión (aunque sigo haciendo intencionalmente una nueva conexión dentro de cada iteración de bucle)

Luego creé un archivo por lotes simple que ejecutaría cada prueba 5 veces (y repetí esto en las computadoras locales y remotas):

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

Una vez completadas las pruebas, medir las duraciones y el espacio utilizado sería trivial (y crear gráficos a partir de los resultados solo requeriría una pequeña manipulación en 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');

Resultados de duración

Estos son los resultados sin procesar de la consulta de duración anterior (CANUCK es el nombre de la máquina que aloja la instancia de SQL Server y HOSER es la máquina que ejecutó la versión remota del código):

Resultados sin procesar de la consulta de duración

Obviamente será más fácil visualizarlo de otra forma. Como se muestra en el primer gráfico, el acceso remoto tuvo un impacto significativo en la duración de las inserciones (aumento de más del 40 %), pero la compresión tuvo poco impacto. Solo el cifrado duplicó aproximadamente la duración de cualquier categoría de prueba:

Duración (milisegundos) para insertar 100 000 filas

Para las lecturas, la compresión tuvo un impacto mucho mayor en el rendimiento que el cifrado o la lectura de datos de forma remota:

Duración (milisegundos) para leer 100 filas aleatorias 1000 veces

Resultados espaciales

Como podría haber predicho, la compresión puede reducir significativamente la cantidad de espacio requerido para almacenar estos datos (aproximadamente a la mitad), mientras que el cifrado puede afectar el tamaño de los datos en la dirección opuesta (casi triplicándolo). Y, por supuesto, comprimir valores cifrados no da resultado:

Espacio utilizado (KB) para almacenar 100 000 filas con o sin compresión y con o sin cifrado

Resumen

Esto debería darle una idea aproximada del impacto esperado al implementar Always Encrypted. Sin embargo, tenga en cuenta que esta fue una prueba muy particular y que estaba usando una compilación temprana de CTP. Sus datos y patrones de acceso pueden arrojar resultados muy diferentes, y los avances adicionales en futuros CTP y las actualizaciones de .NET Framework pueden reducir algunas de estas diferencias incluso en esta misma prueba.

También notará que los resultados aquí fueron ligeramente diferentes en todos los ámbitos que en mi publicación anterior. Esto se puede explicar:

  • Los tiempos de inserción fueron más rápidos en todos los casos porque ya no estoy incurriendo en un viaje de ida y vuelta adicional a la base de datos para generar el nombre y el salario aleatorios.
  • Los tiempos seleccionados fueron más rápidos en todos los casos porque ya no uso un método descuidado de concatenación de cadenas (que se incluyó como parte de la métrica de duración).
  • El espacio utilizado fue un poco más grande en ambos casos, sospecho que se debió a una distribución diferente de cadenas aleatorias que se generaron.

Apéndice A:código de aplicación de la consola 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();
                }
            }
        }
    }
}