Sqlserver
 sql >> Base de données >  >> RDS >> Sqlserver

SQL Server 2016 :impact sur les performances d'Always Encrypted

Dans le cadre de T-SQL Tuesday #69, j'ai blogué sur les limites de Always Encrypted, et j'y ai mentionné que les performances pourraient être affectées négativement par son utilisation (comme vous pouvez vous y attendre, une sécurité plus forte a souvent des compromis). Dans cet article, je voulais jeter un coup d'œil à cela, en gardant à l'esprit (encore une fois) que ces résultats sont basés sur le code CTP 2.2, donc très tôt dans le cycle de développement, et ne reflètent pas nécessairement les performances que vous aurez voir venir RTM.

Tout d'abord, je voulais démontrer qu'Always Encrypted fonctionne à partir des applications clientes même si la dernière version de SQL Server 2016 n'y est pas installée. Cependant, vous devez installer l'aperçu .NET Framework 4.6 (la version la plus récente ici, et cela peut changer) afin de prendre en charge le Column Encryption Setting attribut de chaîne de connexion. Si vous utilisez Windows 10 ou avez installé Visual Studio 2015, cette étape n'est pas nécessaire, car vous devriez déjà disposer d'une version suffisamment récente du .NET Framework.

Ensuite, vous devez vous assurer que le certificat Always Encrypted existe sur tous les clients. Vous créez les clés de chiffrement principale et de colonne dans la base de données, comme tout didacticiel Always Encrypted vous le montrera, puis vous devez exporter le certificat de cette machine et l'importer sur les autres où le code d'application sera exécuté. Ouvrez certmgr.msc , et développez Certificates – Current User> Personal> Certificates, et il devrait y en avoir un appelé Always Encrypted Certificate . Cliquez dessus avec le bouton droit de la souris, choisissez Toutes les tâches> Exporter et suivez les invites. J'ai exporté la clé privée et fourni un mot de passe, ce qui a produit un fichier .pfx. Ensuite, il vous suffit de répéter le processus inverse sur les machines clientes :ouvrez certmgr.msc , développez Certificats - Utilisateur actuel> Personnel, cliquez avec le bouton droit sur Certificats, choisissez Toutes les tâches> Importer et pointez-le sur le fichier .pfx que vous avez créé ci-dessus. (Aide officielle ici.)

(Il existe des moyens plus sûrs de gérer ces certificats - il est peu probable que vous vouliez simplement déployer le certificat comme celui-ci sur toutes les machines, car vous vous demanderez bientôt à quoi cela servait ? Je ne faisais cela que dans mon environnement isolé pour les besoins de cette démo - je voulais m'assurer que mon application récupérait les données sur le réseau et pas seulement dans la mémoire locale.)

Nous créons deux bases de données, une avec une table cryptée et une sans. Nous faisons cela pour isoler les chaînes de connexion et également pour mesurer l'utilisation de l'espace. Bien sûr, il existe des moyens plus précis de contrôler quelles commandes doivent utiliser une connexion activée par chiffrement - voir la note intitulée "Contrôle de l'impact sur les performances…" dans cet article.

Les tableaux ressemblent à ceci :

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

Avec ces tables en place, je voulais configurer une application de ligne de commande très simple pour effectuer les tâches suivantes sur les versions chiffrées et non chiffrées de la table :

  • Insérer 100 000 employés, un à la fois
  • Lire 100 lignes aléatoires, 1 000 fois
  • Horodatages de sortie avant et après chaque étape

Nous avons donc une procédure stockée dans une base de données complètement séparée utilisée pour produire des entiers aléatoires pour représenter les salaires, et des chaînes Unicode aléatoires de longueurs variables. Nous allons le faire un à la fois pour mieux simuler l'utilisation réelle de 100 000 insertions se produisant indépendamment (mais pas simultanément, car je ne suis pas assez courageux pour essayer de développer et de gérer correctement une application C # multithread, ou essayer de coordonner et synchroniser plusieurs instances d'une même application).

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

Quelques lignes d'exemples de sortie (nous ne nous soucions pas du contenu réel de la chaîne, juste qu'il varie) :

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

Ensuite, les procédures stockées que l'application appellera finalement (elles sont identiques dans les deux bases de données, car vos requêtes n'ont pas besoin d'être modifiées pour prendre en charge 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

Maintenant, le code C#, en commençant par la partie connectionStrings de App.config. La partie importante étant le Column Encryption Setting option pour uniquement la base de données avec les colonnes chiffrées (par souci de brièveté, supposons que les trois chaînes de connexion contiennent la même Data Source , et la même authentification SQL User ID et 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>

Et Program.cs (désolé, pour les démos comme celle-ci, je suis nul pour entrer et renommer les choses logiquement) :

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

Ensuite, nous pouvons appeler le .exe avec les lignes de commande suivantes :

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

Et il produira trois lignes de sortie pour chaque appel :l'heure de début, l'heure après l'insertion de 100 000 lignes et l'heure après que 100 lignes ont été lues 1 000 fois. Voici les résultats que j'ai vus sur mon système, en moyenne sur 5 exécutions chacune :

Durée (secondes) d'écriture et de lecture de données

Il y a un impact clair sur l'écriture des données - pas tout à fait 2X, mais plus de 1,5X. Il y avait un delta beaucoup plus faible lors de la lecture et du décryptage des données - du moins dans ces tests - mais ce n'était pas gratuit non plus.

En ce qui concerne l'utilisation de l'espace, il y a environ une pénalité de 3X pour le stockage de données cryptées (étant donné la nature de la plupart des algorithmes de cryptage, cela ne devrait pas être choquant). Gardez à l'esprit que c'était sur une table avec une seule clé primaire en cluster. Voici les chiffres :

Espace (Mo) utilisé pour stocker les données

Donc, évidemment, il y a des pénalités à utiliser Always Encrypted, comme c'est généralement le cas avec à peu près toutes les solutions liées à la sécurité (le dicton "pas de repas gratuit" vient à l'esprit). Je répète que ces tests ont été effectués par rapport à CTP 2.2, qui peut être radicalement différent de la version finale de SQL Server 2016. De plus, ces différences que j'ai observées peuvent ne refléter que la nature des tests que j'ai concoctés; évidemment, j'espère que vous pourrez utiliser cette approche pour tester vos résultats par rapport à votre schéma, sur votre matériel et avec vos modèles d'accès aux données.