sql >> Databasteknik >  >> RDS >> Database

Alltid krypterad prestanda:En uppföljning

Förra veckan skrev jag om begränsningarna av Alltid krypterad samt prestandapåverkan. Jag ville lägga upp en uppföljning efter att ha utfört fler tester, främst på grund av följande ändringar:

  • Jag lade till ett test för lokalt för att se om nätverksoverhead var signifikant (tidigare var testet bara på distans). Jag borde dock sätta "nätverksoverhead" i citat, eftersom det här är två virtuella datorer på samma fysiska värd, så det är inte riktigt en äkta blottmetallanalys.
  • Jag lade till några extra (icke-krypterade) kolumner i tabellen för att göra den mer realistisk (men inte riktigt så realistisk).
      DateCreated  DATETIME NOT NULL DEFAULT SYSUTCDATETIME(),
      DateModified DATETIME NOT NULL DEFAULT SYSUTCDATETIME(),
      IsActive     BIT NOT NULL DEFAULT 1

    Ändrade sedan hämtningsproceduren i enlighet med detta:

    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
  • Lade till en procedur för att trunkera tabellen (tidigare gjorde jag det manuellt mellan testerna):
    CREATE PROCEDURE dbo.Cleanup
    AS
    BEGIN
      SET NOCOUNT ON;
      TRUNCATE TABLE dbo.Employees;
    END
    GO
  • Lade till en procedur för inspelning av timings (tidigare analyserade jag manuellt konsolutdata):
    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
  • Jag lade till ett par databaser som använde sidkomprimering – vi vet alla att krypterade värden inte komprimeras bra, men det här är en polariserande funktion som kan användas ensidigt även på tabeller med krypterade kolumner, så jag tänkte att jag bara skulle profilera även dessa. (Och lade till ytterligare två anslutningssträngar till 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>
  • Jag gjorde många förbättringar av C#-koden (se bilagan) baserat på feedback från tobi (som ledde till denna kodgranskningsfråga) och en del bra hjälp från kollegan Brooke Philpott (@Macromullet). Dessa inkluderade:
    • eliminerar den lagrade proceduren för att generera slumpmässiga namn/löner, och gör det i C# istället
    • med Stopwatch istället för klumpiga datum-/tidssträngar
    • mer konsekvent användning av using() och eliminering av .Close()
    • något bättre namnkonventioner (och kommentarer!)
    • ändrar while loopar till for slingor
    • med en StringBuilder istället för naiv sammanlänkning (som jag från början hade valt avsiktligt)
    • konsolidera anslutningssträngarna (även om jag fortfarande avsiktligt gör en ny anslutning inom varje loop-iteration)

Sedan skapade jag en enkel batchfil som skulle köra varje test 5 gånger (och upprepade detta på både lokala och fjärrdatorer):

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

Efter att testerna var slutförda skulle det vara trivialt att mäta varaktigheterna och det använda utrymmet (och att bygga diagram från resultaten skulle bara kräva lite 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');

Längdresultat

Här är råresultaten från varaktighetsfrågan ovan (CANUCK är namnet på maskinen som är värd för instansen av SQL Server och HOSER är maskinen som körde fjärrversionen av koden):

Rå resultat av varaktighetsfråga

Uppenbarligen blir det lättare att visualisera i en annan form. Som visas i den första grafen, hade fjärråtkomst en betydande inverkan på varaktigheten av skären (över 40 % ökning), men komprimering hade liten inverkan alls. Enbart kryptering fördubblade ungefär varaktigheten för någon testkategori:

Längd (millisekunder) för att infoga 100 000 rader

För läsningarna hade komprimering en mycket större inverkan på prestanda än antingen kryptering eller fjärrläsning av data:

Längd (millisekunder) för att läsa 100 slumpmässiga rader 1 000 gånger

Uppslagsresultat

Som du kanske har förutspått kan komprimering avsevärt minska mängden utrymme som krävs för att lagra dessa data (ungefär hälften), medan kryptering kan ses påverka datastorleken i motsatt riktning (nästan tredubblar den). Och naturligtvis lönar det sig inte att komprimera krypterade värden:

Utrymme som används (KB) för att lagra 100 000 rader med eller utan komprimering och med eller utan kryptering

Sammanfattning

Detta bör ge dig en ungefärlig uppfattning om vad du kan förvänta dig effekten av när du implementerar Always Encrypted. Kom dock ihåg att detta var ett väldigt speciellt test och att jag använde en tidig CTP-version. Dina data och åtkomstmönster kan ge mycket olika resultat, och ytterligare framsteg i framtida CTP:er och uppdateringar av .NET Framework kan minska vissa av dessa skillnader även i just detta test.

Du kommer också att märka att resultaten här var något annorlunda över hela linjen än i mitt tidigare inlägg. Detta kan förklaras:

  • Infogningstiderna var snabbare i alla fall eftersom jag inte längre ådrar mig en extra tur och retur till databasen för att generera det slumpmässiga namnet och lönen.
  • De valda tiderna var snabbare i alla fall eftersom jag inte längre använder en slarvig metod för strängsammansättning (som ingick som en del av varaktighetsmåttet).
  • Utrymmet som användes var något större i båda fallen, misstänker jag på grund av en annan fördelning av slumpmässiga strängar som genererades.

Bilaga A – C# Console Application Code

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. För sista gången, NEJ, du kan inte lita på IDENT_CURRENT()

  2. Funktion för att returnera dynamisk uppsättning kolumner för given tabell

  3. Bryter MySQL mot standarden genom att tillåta att välja kolumner som inte är en del av gruppen för klausul?

  4. Ingen mer data att läsa från uttaget fel