sql >> Database teknologi >  >> RDS >> Database

Altid krypteret ydeevne:En opfølgning

I sidste uge skrev jeg om begrænsningerne af Always Encrypted samt præstationspåvirkningen. Jeg ønskede at skrive en opfølgning efter at have udført flere tests, primært på grund af følgende ændringer:

  • Jeg tilføjede en test for lokal for at se, om netværksoverhead var signifikant (tidligere var testen kun ekstern). Jeg burde dog sætte "netværk overhead" i luftanførselstegn, fordi disse er to VM'er på den samme fysiske vært, så det er ikke rigtig en sand bare metal-analyse.
  • Jeg tilføjede et par ekstra (ikke-krypterede) kolonner til tabellen for at gøre den mere realistisk (men egentlig ikke så realistisk).
      DateCreated  DATETIME NOT NULL DEFAULT SYSUTCDATETIME(),
      DateModified DATETIME NOT NULL DEFAULT SYSUTCDATETIME(),
      IsActive     BIT NOT NULL DEFAULT 1

    Ændrede derefter genfindingsproceduren i overensstemmelse hermed:

    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
  • Tilføjede en procedure til at afkorte tabellen (tidligere gjorde jeg det manuelt mellem testene):
    CREATE PROCEDURE dbo.Cleanup
    AS
    BEGIN
      SET NOCOUNT ON;
      TRUNCATE TABLE dbo.Employees;
    END
    GO
  • Tilføjede en procedure til optagelse af timings (tidligere parsede jeg manuelt konsoloutput):
    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
  • Jeg tilføjede et par databaser, som brugte sidekomprimering – vi ved alle, at krypterede værdier ikke komprimeres godt, men dette er en polariserende funktion, der kan bruges ensidigt selv på tabeller med krypterede kolonner, så jeg tænkte, at jeg bare ville profiler også disse. (Og tilføjet yderligere to forbindelsesstrenge til 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>
  • Jeg lavede mange forbedringer til C#-koden (se appendiks) baseret på feedback fra tobi (som førte til dette kodegennemgang-spørgsmål) og en god hjælp fra kollega Brooke Philpott (@Macromullet). Disse omfattede:
    • eliminerer den lagrede procedure for at generere tilfældige navne/lønninger, og gør det i C# i stedet
    • ved hjælp af Stopwatch i stedet for klodsede dato-/tidsstrenge
    • mere konsekvent brug af using() og eliminering af .Close()
    • lidt bedre navnekonventioner (og kommentarer!)
    • ændrer while sløjfer til for sløjfer
    • ved at bruge en StringBuilder i stedet for naiv sammenkædning (som jeg oprindeligt havde valgt med vilje)
    • konsolidering af forbindelsesstrengene (selvom jeg stadig med vilje laver en ny forbindelse inden for hver loop-iteration)

Derefter oprettede jeg en simpel batchfil, der ville køre hver test 5 gange (og gentog dette på både de lokale og eksterne computere):

for /l %%x in (1,1,5) do (        ^
AEDemoConsole "Normal"          & ^
AEDemoConsole "Encrypt"         & ^
AEDemoConsole "NormalCompress"  & ^
AEDemoConsole "EncryptCompress" & ^
)

Efter at testene var afsluttet, ville det være trivielt at måle varigheden og den brugte plads (og at bygge diagrammer ud fra resultaterne ville bare kræve lidt manipulation i 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');

Varighedsresultater

Her er de rå resultater fra varighedsforespørgslen ovenfor (CANUCK er navnet på den maskine, der er vært for forekomsten af ​​SQL Server, og HOSER er den maskine, der kørte fjernversionen af ​​koden):

Rå resultater af varighedsforespørgsel

Det vil naturligvis være lettere at visualisere i en anden form. Som vist i den første graf havde fjernadgang en signifikant indflydelse på varigheden af ​​indsatserne (over 40 % stigning), men komprimering havde slet ingen indflydelse. Alene kryptering fordoblede varigheden for en hvilken som helst testkategori groft:

Varighed (millisekunder) for at indsætte 100.000 rækker

For læsningerne havde komprimering en meget større indflydelse på ydeevnen end enten kryptering eller fjernlæsning af data:

Varighed (millisekunder) for at læse 100 tilfældige rækker 1.000 gange

Space-resultater

Som du måske har forudsagt, kan komprimering betydeligt reducere mængden af ​​plads, der kræves for at gemme disse data (omtrent i det halve), hvorimod kryptering kan ses påvirke datastørrelsen i den modsatte retning (næsten tredoble den). Og selvfølgelig kan det ikke betale sig at komprimere krypterede værdier:

Plads brugt (KB) til at gemme 100.000 rækker med eller uden komprimering og med eller uden kryptering

Oversigt

Dette skulle give dig en nogenlunde idé om, hvad du kan forvente, at virkningen vil være, når du implementerer Always Encrypted. Husk dog, at dette var en meget speciel test, og at jeg brugte en tidlig CTP-build. Dine data og adgangsmønstre kan give meget forskellige resultater, og yderligere fremskridt i fremtidige CTP'er og opdateringer til .NET Framework kan reducere nogle af disse forskelle selv i netop denne test.

Du vil også bemærke, at resultaterne her var lidt anderledes over hele linjen end i mit tidligere indlæg. Dette kan forklares:

  • Indsættelsestiderne var i alle tilfælde hurtigere, fordi jeg ikke længere pådrager mig en ekstra rundrejse til databasen for at generere det tilfældige navn og løn.
  • De udvalgte tider var hurtigere i alle tilfælde, fordi jeg ikke længere bruger en sjusket metode til strengsammenkædning (som var inkluderet som en del af varighedsmetrikken).
  • Den brugte plads var lidt større i begge tilfælde, formoder jeg på grund af en anden fordeling af tilfældige strenge, der blev genereret.

Bilag A – C#-konsolapplikationskode

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

  1. Formater et tal som en procentdel i Oracle

  2. Hvorfor understøtter MySQL ikke millisekund / mikrosekund præcision?

  3. Java:Indsæt flere rækker i MySQL med PreparedStatement

  4. Sådan starter du PostgreSQL Server på Mac OS X via Homebrew