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

Performances toujours chiffrées :un suivi

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 vers for 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();
                }
            }
        }
    }
}