La semaine dernière, j'ai écrit sur les limites d'Always Encrypted ainsi que sur l'impact sur les performances. Je voulais publier un suivi après avoir effectué plus de tests, principalement en raison des changements suivants :
- J'ai ajouté un test pour local, pour voir si la surcharge du réseau était significative (auparavant, le test n'était qu'à distance). Cependant, je devrais mettre "frais généraux du réseau" entre guillemets, car il s'agit de deux machines virtuelles sur le même hôte physique, donc pas vraiment une véritable analyse bare metal.
- J'ai ajouté quelques colonnes supplémentaires (non chiffrées) au tableau pour le rendre plus réaliste (mais pas vraiment réaliste).
DateCreated DATETIME NOT NULL DEFAULT SYSUTCDATETIME(), DateModified DATETIME NOT NULL DEFAULT SYSUTCDATETIME(), IsActive BIT NOT NULL DEFAULT 1
Modifiez ensuite la procédure de récupération en conséquence :
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
- Ajout d'une procédure pour tronquer la table (auparavant, je le faisais manuellement entre les tests) :
CREATE PROCEDURE dbo.Cleanup AS BEGIN SET NOCOUNT ON; TRUNCATE TABLE dbo.Employees; END GO
- Ajout d'une procédure pour enregistrer les minutages (auparavant, j'analyse manuellement la sortie de la 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
- J'ai ajouté une paire de bases de données qui utilisaient la compression de page. Nous savons tous que les valeurs chiffrées ne se compressent pas bien, mais il s'agit d'une fonctionnalité polarisante qui peut être utilisée unilatéralement même sur des tables avec des colonnes chiffrées. J'ai donc pensé que je ferais juste profilez-les aussi. (Et ajouté deux chaînes de connexion supplémentaires à
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>
- J'ai apporté de nombreuses améliorations au code C# (voir l'annexe) sur la base des commentaires de tobi (qui ont conduit à cette question de révision du code) et de l'aide précieuse de ma collègue Brooke Philpott (@Macromullet). Ceux-ci comprenaient :
- éliminer la procédure stockée pour générer des noms/salaires aléatoires, et le faire en C# à la place
- en utilisant
Stopwatch
au lieu de chaînes de date/heure maladroites - utilisation plus cohérente de
using()
et élimination de.Close()
- Conventions de nommage légèrement meilleures (et commentaires !)
- modifier
while
boucle versfor
boucles - à l'aide d'un
StringBuilder
au lieu de la concaténation naïve (que j'avais initialement choisie intentionnellement) - consolider les chaînes de connexion (bien que je crée toujours intentionnellement une nouvelle connexion à chaque itération de boucle)
Ensuite, j'ai créé un fichier de commandes simple qui exécuterait chaque test 5 fois (et répété cela sur les ordinateurs locaux et distants) :
for /l %%x in (1,1,5) do ( ^ AEDemoConsole "Normal" & ^ AEDemoConsole "Encrypt" & ^ AEDemoConsole "NormalCompress" & ^ AEDemoConsole "EncryptCompress" & ^ )
Une fois les tests terminés, mesurer les durées et l'espace utilisé serait trivial (et construire des graphiques à partir des résultats ne demanderait qu'une petite manipulation dans 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');
Résultats de durée
Voici les résultats bruts de la requête de durée ci-dessus (CANUCK
est le nom de la machine qui héberge l'instance de SQL Server, et HOSER
est la machine qui a exécuté la version distante du code) :
Résultats bruts de la requête de durée
Évidemment, il sera plus facile de visualiser sous une autre forme. Comme le montre le premier graphique, l'accès à distance a eu un impact significatif sur la durée des insertions (plus de 40 % d'augmentation), mais la compression a eu peu d'impact. Le chiffrement à lui seul doublait à peu près la durée de toute catégorie de test :
Durée (millisecondes) pour insérer 100 000 lignes
Pour les lectures, la compression a eu un impact beaucoup plus important sur les performances que le chiffrement ou la lecture des données à distance :
Durée (millisecondes) pour lire 100 lignes aléatoires 1 000 fois
Résultats de l'espace
Comme vous l'avez peut-être prévu, la compression peut réduire considérablement la quantité d'espace nécessaire pour stocker ces données (environ de moitié), tandis que le chiffrement peut avoir un impact sur la taille des données dans le sens opposé (la triplant presque). Et, bien sûr, compresser les valeurs chiffrées ne rapporte rien :
Espace utilisé (Ko) pour stocker 100 000 lignes avec ou sans compression et avec ou sans cryptage
Résumé
Cela devrait vous donner une idée approximative de l'impact auquel vous pouvez vous attendre lors de la mise en œuvre d'Always Encrypted. Gardez à l'esprit, cependant, qu'il s'agissait d'un test très particulier et que j'utilisais une première version CTP. Vos modèles de données et d'accès peuvent donner des résultats très différents, et de nouvelles avancées dans les futurs CTP et mises à jour du .NET Framework peuvent réduire certaines de ces différences, même dans ce test.
Vous remarquerez également que les résultats ici étaient légèrement différents dans tous les domaines que dans mon article précédent. Cela peut s'expliquer :
- Les temps d'insertion ont été plus rapides dans tous les cas, car je n'engage plus un aller-retour supplémentaire vers la base de données pour générer le nom et le salaire au hasard.
- Les temps de sélection étaient plus rapides dans tous les cas, car je n'utilise plus une méthode bâclée de concaténation de chaînes (qui était incluse dans la métrique de durée).
- L'espace utilisé était légèrement plus grand dans les deux cas, je suppose en raison d'une distribution différente des chaînes aléatoires générées.
Annexe A – Code d'application de la 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(); } } } } }