sql >> Base de Datos >  >> RDS >> Sqlserver

SQL Server 2016:impacto en el rendimiento de Always Encrypted

Como parte de T-SQL Tuesday #69, escribí en un blog sobre las limitaciones de Always Encrypted, y mencioné que el rendimiento podría verse afectado negativamente por su uso (como era de esperar, una seguridad más sólida a menudo tiene compensaciones). En esta publicación, quería echar un vistazo rápido a esto, teniendo en cuenta (nuevamente) que estos resultados se basan en el código CTP 2.2, por lo que es muy temprano en el ciclo de desarrollo y no reflejan necesariamente el rendimiento que obtendrá. ver venir RTM.

Primero, quería demostrar que Always Encrypted funciona desde las aplicaciones cliente, incluso si la última versión de SQL Server 2016 no está instalada allí. Sin embargo, debe instalar la vista previa de .NET Framework 4.6 (la versión más reciente aquí, y eso puede cambiar) para admitir la Column Encryption Setting atributo de cadena de conexión. Si está ejecutando Windows 10 o ha instalado Visual Studio 2015, este paso no es necesario, ya que debería tener una versión lo suficientemente reciente de .NET Framework.

A continuación, debe asegurarse de que el certificado de Always Encrypted exista en todos los clientes. Usted crea las claves de cifrado maestras y de columna dentro de la base de datos, como le mostrará cualquier tutorial de Always Encrypted, luego necesita exportar el certificado de esa máquina e importarlo en las otras donde se ejecutará el código de la aplicación. Abra certmgr.msc y expanda Certificados - Usuario actual> Personal> Certificados, y debería haber uno allí llamado Always Encrypted Certificate . Haga clic con el botón derecho en eso, elija Todas las tareas> Exportar y siga las indicaciones. Exporté la clave privada y proporcioné una contraseña, lo que generó un archivo .pfx. Luego simplemente repite el proceso opuesto en las máquinas cliente:abre certmgr.msc , expanda Certificados – Usuario actual> Personal, haga clic con el botón derecho en Certificados, seleccione Todas las tareas> Importar y señale el archivo .pfx que creó anteriormente. (Ayuda oficial aquí.)

(Hay formas más seguras de administrar estos certificados; no es probable que desee implementar el certificado de esta manera en todas las máquinas, ya que pronto se preguntará cuál era el punto? Solo estaba haciendo esto en mi entorno aislado. para los fines de esta demostración, quería asegurarme de que mi aplicación recuperara datos a través del cable y no solo en la memoria local).

Creamos dos bases de datos, una con una tabla cifrada y otra sin ella. Hacemos esto para aislar las cadenas de conexión y también para medir el uso del espacio. Por supuesto, hay formas más granulares de controlar qué comandos necesitan usar una conexión habilitada para el cifrado; consulte la nota titulada "Controlar el impacto en el rendimiento..." en este artículo.

Las tablas se ven así:

-- 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 estas tablas en su lugar, quería configurar una aplicación de línea de comandos muy simple para realizar las siguientes tareas en las versiones cifradas y no cifradas de la tabla:

  • Insertar 100.000 empleados, uno a la vez
  • Lea 100 filas al azar, 1000 veces
  • Marcas de tiempo de salida antes y después de cada paso

Así que tenemos un procedimiento almacenado en una base de datos completamente separada que se usa para producir números enteros aleatorios para representar salarios y cadenas Unicode aleatorias de diferentes longitudes. Vamos a hacer esto uno a la vez para simular mejor el uso real de 100 000 inserciones que ocurren de forma independiente (aunque no al mismo tiempo, ya que no soy lo suficientemente valiente como para tratar de desarrollar y administrar correctamente una aplicación C # de subprocesos múltiples, o tratar de coordinar y sincronizar múltiples instancias de una sola aplicación).

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 par de filas de salida de muestra (no nos importa el contenido real de la cadena, solo que varía):

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

Luego, los procedimientos almacenados a los que la aplicación llamará en última instancia (son idénticos en ambas bases de datos, ya que no es necesario cambiar sus consultas para que sean compatibles con 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

Ahora, el código C#, comenzando con la porción de cadenas de conexión de App.config. La parte importante es la Column Encryption Setting opción solo para la base de datos con las columnas cifradas (por razones de brevedad, suponga que las tres cadenas de conexión contienen el mismo Data Source y la misma autenticación SQL User ID y 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>

Y Program.cs (lo siento, para demostraciones como esta, soy terrible para entrar y cambiar el nombre de las cosas lógicamente):

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

Luego podemos llamar al .exe con las siguientes líneas de comando:

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

Y producirá tres líneas de salida para cada llamada:la hora de inicio, la hora después de que se insertaron 100 000 filas y la hora después de que se leyeron 100 filas 1000 veces. Estos fueron los resultados que vi en mi sistema, con un promedio de 5 ejecuciones cada uno:

Duración (segundos) de escritura y lectura de datos

Hay un claro impacto en la escritura de los datos:no exactamente 2X, pero más de 1.5X. Hubo un delta mucho más bajo en la lectura y descifrado de los datos, al menos en estas pruebas, pero eso tampoco fue gratis.

En cuanto al uso del espacio, hay una penalización de aproximadamente 3 veces por almacenar datos cifrados (dada la naturaleza de la mayoría de los algoritmos de cifrado, esto no debería ser sorprendente). Tenga en cuenta que esto estaba en una tabla con una sola clave principal agrupada. Aquí estaban las cifras:

Espacio (MB) utilizado para almacenar datos

Entonces, obviamente, hay algunas penalizaciones con el uso de Always Encrypted, como suele ocurrir con casi todas las soluciones relacionadas con la seguridad (me viene a la mente el dicho "no hay almuerzo gratis"). Repetiré que estas pruebas se realizaron con CTP 2.2, que puede ser radicalmente diferente a la versión final de SQL Server 2016. Además, estas diferencias que he observado pueden reflejar solo la naturaleza de las pruebas que inventé; obviamente espero que pueda usar este enfoque para probar sus resultados contra su esquema, en su hardware y con sus patrones de acceso a datos.