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