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