Database
 sql >> Base de Dados >  >> RDS >> Database

Desempenho sempre criptografado:um acompanhamento


Na semana passada, escrevi sobre as limitações do Always Encrypted, bem como o impacto no desempenho. Eu queria postar um acompanhamento depois de realizar mais testes, principalmente devido às seguintes alterações:
  • Adicionei um teste para local, para ver se a sobrecarga de rede era significativa (anteriormente, o teste era apenas remoto). No entanto, devo colocar "sobrecarga de rede" entre aspas, porque essas são duas VMs no mesmo host físico, portanto, não é realmente uma análise bare metal verdadeira.
  • Adicionei algumas colunas extras (não criptografadas) à tabela para torná-la mais realista (mas não tão realista).
      DateCreated  DATETIME NOT NULL DEFAULT SYSUTCDATETIME(),
      DateModified DATETIME NOT NULL DEFAULT SYSUTCDATETIME(),
      IsActive     BIT NOT NULL DEFAULT 1

    Em seguida, alterou o procedimento de recuperação de acordo:
    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
  • Adicionado um procedimento para truncar a tabela (anteriormente eu fazia isso manualmente entre os testes):
    CREATE PROCEDURE dbo.Cleanup
    AS
    BEGIN
      SET NOCOUNT ON;
      TRUNCATE TABLE dbo.Employees;
    END
    GO
  • Adicionado um procedimento para gravação de tempos (anteriormente eu estava analisando manualmente a saída do 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
  • Adicionei um par de bancos de dados que usavam compactação de página – todos sabemos que valores criptografados não são compactados bem, mas esse é um recurso de polarização que pode ser usado unilateralmente mesmo em tabelas com colunas criptografadas, então pensei em apenas perfil estes também. (E adicionou mais duas strings de conexão ao 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>
  • Fiz muitas melhorias no código C# (consulte o Apêndice) com base no feedback de tobi (que levou a esta pergunta de revisão de código) e uma grande ajuda da colega de trabalho Brooke Philpott (@Macromullet). Estes incluem:
    • eliminando o procedimento armazenado para gerar nomes/salários aleatórios e fazendo isso em C#
    • usando Stopwatch em vez de strings desajeitadas de data/hora
    • uso mais consistente de using() e eliminação de .Close()
    • convenções de nomenclatura ligeiramente melhores (e comentários!)
    • alterando while loops para for laços
    • usando um StringBuilder em vez de concatenação ingênua (que inicialmente escolhi intencionalmente)
    • consolidando as strings de conexão (embora eu ainda esteja fazendo uma nova conexão intencionalmente em cada iteração de loop)

Em seguida, criei um arquivo de lote simples que executaria cada teste 5 vezes (e repeti isso nos computadores local e remoto):
for /l %%x in (1,1,5) do (        ^
AEDemoConsole "Normal"          & ^
AEDemoConsole "Encrypt"         & ^
AEDemoConsole "NormalCompress"  & ^
AEDemoConsole "EncryptCompress" & ^
)

Após a conclusão dos testes, medir as durações e o espaço usado seria trivial (e construir gráficos a partir dos resultados exigiria apenas um pouco de manipulação no 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 duração


Aqui estão os resultados brutos da consulta de duração acima (CANUCK é o nome da máquina que hospeda a instância do SQL Server e HOSER é a máquina que executou a versão remota do código):

Resultados brutos da consulta de duração

Obviamente será mais fácil visualizar de outra forma. Conforme mostrado no primeiro gráfico, o acesso remoto teve um impacto significativo na duração das inserções (aumento de mais de 40%), mas a compressão teve pouco impacto. A criptografia sozinha praticamente dobrou a duração de qualquer categoria de teste:

Duração (milissegundos) para inserir 100.000 linhas

Para as leituras, a compactação teve um impacto muito maior no desempenho do que a criptografia ou a leitura dos dados remotamente:

Duração (milissegundos) para ler 100 linhas aleatórias 1.000 vezes

Resultados do Espaço


Como você deve ter previsto, a compactação pode reduzir significativamente a quantidade de espaço necessária para armazenar esses dados (aproximadamente pela metade), enquanto a criptografia pode ser vista afetando o tamanho dos dados na direção oposta (quase o triplicando). E, claro, compactar valores criptografados não compensa:

Espaço usado (KB) para armazenar 100.000 linhas com ou sem compactação e com ou sem criptografia

Resumo


Isso deve fornecer uma ideia aproximada do que esperar do impacto ao implementar o Always Encrypted. Tenha em mente, porém, que este foi um teste muito particular e que eu estava usando uma versão CTP inicial. Seus dados e padrões de acesso podem produzir resultados muito diferentes, e avanços adicionais em futuros CTPs e atualizações do .NET Framework podem reduzir algumas dessas diferenças mesmo neste teste.

Você também notará que os resultados aqui foram ligeiramente diferentes do que no meu post anterior. Isso pode ser explicado:
  • Os tempos de inserção foram mais rápidos em todos os casos porque não estou mais incorrendo em uma viagem extra de ida e volta ao banco de dados para gerar o nome e o salário aleatórios.
  • Os tempos de seleção foram mais rápidos em todos os casos porque não estou mais usando um método desleixado de concatenação de strings (que foi incluído como parte da métrica de duração).
  • O espaço usado foi um pouco maior em ambos os casos, suspeito devido a uma distribuição diferente de strings aleatórias que foram geradas.


Apêndice A – Código do aplicativo de 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();
                }
            }
        }
    }
}