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