sql >> Database >  >> RDS >> Database

Altijd versleutelde prestaties:een vervolg

Vorige week schreef ik over de beperkingen van Always Encrypted en de impact op de prestaties. Ik wilde een follow-up plaatsen nadat ik meer tests had uitgevoerd, voornamelijk vanwege de volgende wijzigingen:

  • Ik heb een test toegevoegd voor lokaal, om te zien of de netwerkoverhead significant was (voorheen was de test alleen op afstand). Ik zou echter "netwerkoverhead" tussen luchtaanhalingstekens moeten zetten, omdat dit twee VM's op dezelfde fysieke host zijn, dus niet echt een echte bare metal-analyse.
  • Ik heb een paar extra (niet-versleutelde) kolommen aan de tabel toegevoegd om het realistischer (maar niet echt realistisch) te maken.
      DateCreated  DATETIME NOT NULL DEFAULT SYSUTCDATETIME(),
      DateModified DATETIME NOT NULL DEFAULT SYSUTCDATETIME(),
      IsActive     BIT NOT NULL DEFAULT 1

    Wijzig vervolgens de ophaalprocedure dienovereenkomstig:

    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
  • Een procedure toegevoegd om de tabel af te kappen (voorheen deed ik dat handmatig tussen tests):
    CREATE PROCEDURE dbo.Cleanup
    AS
    BEGIN
      SET NOCOUNT ON;
      TRUNCATE TABLE dbo.Employees;
    END
    GO
  • Een procedure toegevoegd voor het opnemen van timings (voorheen was ik de uitvoer van de console handmatig aan het parseren):
    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
  • Ik heb een paar databases toegevoegd die paginacompressie gebruikten - we weten allemaal dat versleutelde waarden niet goed worden gecomprimeerd, maar dit is een polariserende functie die eenzijdig kan worden gebruikt, zelfs op tabellen met versleutelde kolommen, dus ik dacht dat ik het gewoon zou doen deze ook profileren. (En nog twee verbindingsreeksen toegevoegd aan 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>
  • Ik heb veel verbeteringen aangebracht in de C#-code (zie de bijlage) op basis van feedback van tobi (wat leidde tot deze codebeoordelingsvraag) en geweldige hulp van collega Brooke Philpott (@Macromullet). Deze omvatten:
    • het elimineren van de opgeslagen procedure om willekeurige namen/salarissen te genereren en dat in plaats daarvan in C# te doen
    • met behulp van Stopwatch in plaats van onhandige datum/tijd-strings
    • consistenter gebruik van using() en verwijdering van .Close()
    • iets betere naamgevingsconventies (en opmerkingen!)
    • veranderen while lussen naar for lussen
    • met behulp van een StringBuilder in plaats van naïeve aaneenschakeling (waar ik aanvankelijk bewust voor had gekozen)
    • consolideren van de verbindingsreeksen (hoewel ik nog steeds opzettelijk een nieuwe verbinding maak binnen elke lus-iteratie)

Vervolgens heb ik een eenvoudig batchbestand gemaakt dat elke test 5 keer zou uitvoeren (en dit herhaald op zowel de lokale als externe computers):

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

Nadat de tests waren voltooid, zou het meten van de duur en de gebruikte ruimte triviaal zijn (en het maken van grafieken van de resultaten zou slechts een kleine manipulatie in Excel vergen):

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

Duurresultaten

Hier zijn de onbewerkte resultaten van de bovenstaande duurquery (CANUCK is de naam van de machine die het exemplaar van SQL Server host, en HOSER is de machine die de externe versie van de code heeft uitgevoerd):

Onbewerkte resultaten van duurquery

Uiteraard zal het gemakkelijker zijn om het in een andere vorm te visualiseren. Zoals blijkt uit de eerste grafiek, had toegang op afstand een significante impact op de duur van de inserts (meer dan 40% toename), maar compressie had helemaal geen impact. Alleen al versleuteling verdubbelde de duur voor elke testcategorie:

Duur (milliseconden) om 100.000 rijen in te voegen

Voor het lezen had compressie een veel grotere invloed op de prestaties dan codering of het op afstand lezen van de gegevens:

Duur (milliseconden) om 100 willekeurige rijen 1000 keer te lezen

Ruimteresultaten

Zoals je misschien had voorspeld, kan compressie de hoeveelheid ruimte die nodig is om deze gegevens op te slaan aanzienlijk verminderen (ongeveer de helft), terwijl codering de gegevensgrootte in de tegenovergestelde richting kan beïnvloeden (bijna verdrievoudigen). En natuurlijk loont het comprimeren van versleutelde waarden niet:

Gebruikte ruimte (KB) om 100.000 rijen op te slaan met of zonder compressie en met of zonder encryptie

Samenvatting

Dit zou u een globaal idee moeten geven van wat u de impact kunt verwachten bij het implementeren van Always Encrypted. Houd er echter rekening mee dat dit een zeer specifieke test was en dat ik een vroege CTP-build gebruikte. Uw gegevens en toegangspatronen kunnen zeer verschillende resultaten opleveren, en verdere vooruitgang in toekomstige CTP's en updates van het .NET Framework kunnen sommige van deze verschillen zelfs in deze test verminderen.

Je zult ook merken dat de resultaten hier over de hele linie iets anders waren dan in mijn vorige bericht. Dit kan worden uitgelegd:

  • De invoegtijden waren in alle gevallen sneller omdat ik niet langer een extra retourtje naar de database maak om de willekeurige naam en het salaris te genereren.
  • De geselecteerde tijden waren in alle gevallen sneller omdat ik niet langer een slordige methode van aaneenschakeling van tekenreeksen gebruik (die was opgenomen als onderdeel van de duurstatistiek).
  • De gebruikte ruimte was in beide gevallen iets groter, vermoed ik vanwege een andere verdeling van willekeurige reeksen die werden gegenereerd.

Bijlage A – C#-consoletoepassingscode

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. Postgres-tabelkolomnaambeperkingen?

  2. SQL waarbij de samengevoegde set alle waarden moet bevatten, maar mag meer bevatten

  3. Slaapfunctie in ORACLE

  4. Oracle invoegen als rij niet bestaat