Minulý týden jsem psal o omezeních Always Encrypted a také o dopadu na výkon. Po provedení dalších testů jsem chtěl zveřejnit následnou zprávu, především kvůli následujícím změnám:
- Přidal jsem test pro místní, abych zjistil, zda byla režie sítě významná (dříve byl test pouze vzdálený). I když bych měl uvést „síťovou režii“ do vzduchových uvozovek, protože se jedná o dva virtuální počítače na stejném fyzickém hostiteli, takže ve skutečnosti nejde o skutečnou analýzu holých kovů.
- Do tabulky jsem přidal několik dalších (nešifrovaných) sloupců, aby byla realističtější (ale ve skutečnosti ne tak realistická).
DateCreated DATETIME NOT NULL DEFAULT SYSUTCDATETIME(), DateModified DATETIME NOT NULL DEFAULT SYSUTCDATETIME(), IsActive BIT NOT NULL DEFAULT 1
Poté odpovídajícím způsobem změňte postup vyhledávání:
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
- Přidána procedura pro zkrácení tabulky (dříve jsem to dělal ručně mezi testy):
CREATE PROCEDURE dbo.Cleanup AS BEGIN SET NOCOUNT ON; TRUNCATE TABLE dbo.Employees; END GO
- Přidán postup pro záznam časování (dříve jsem ručně analyzoval výstup konzole):
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
- Přidal jsem pár databází, které používaly kompresi stránek – všichni víme, že zašifrované hodnoty se špatně komprimují, ale toto je polarizační funkce, kterou lze použít jednostranně i na tabulkách se zašifrovanými sloupci, takže jsem si řekl, profilujte i tyto. (A přidal dva další připojovací řetězce do
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>
- Provedl jsem mnoho vylepšení kódu C# (viz příloha) na základě zpětné vazby od tobiho (což vedlo k této otázce Code Review) a skvělé pomoci od kolegyně Brooke Philpott (@Macromullet). Patří mezi ně:
- odstranění uložené procedury pro generování náhodných jmen/platů a místo toho to v C#
- pomocí
Stopwatch
místo nemotorných řetězců data/času - důslednější používání
using()
a odstranění.Close()
- trochu lepší konvence pojmenování (a komentáře!)
- změna
while
smyčky dofor
smyčky - pomocí
StringBuilder
místo naivního zřetězení (které jsem původně zvolil záměrně) - konsolidace připojovacích řetězců (ačkoli stále záměrně vytvářím nové připojení v rámci každé iterace smyčky)
Potom jsem vytvořil jednoduchý dávkový soubor, který by spustil každý test 5krát (a opakoval to na místním i vzdáleném počítači):
for /l %%x in (1,1,5) do ( ^ AEDemoConsole "Normal" & ^ AEDemoConsole "Encrypt" & ^ AEDemoConsole "NormalCompress" & ^ AEDemoConsole "EncryptCompress" & ^ )
Po dokončení testů by bylo měření doby trvání a použitého prostoru triviální (a vytváření grafů z výsledků by vyžadovalo jen malou manipulaci v Excelu):
-- 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');
Výsledky trvání
Zde jsou nezpracované výsledky z výše uvedeného dotazu na dobu trvání (CANUCK
je název počítače, který je hostitelem instance SQL Server, a HOSER
je stroj, který spustil vzdálenou verzi kódu):
Nezpracované výsledky dotazu na trvání
Zjevně bude snazší si to představit v jiné formě. Jak ukazuje první graf, vzdálený přístup měl významný dopad na dobu trvání vložek (nárůst více než 40 %), ale komprese měla vůbec malý dopad. Samotné šifrování zhruba zdvojnásobilo dobu trvání jakékoli testovací kategorie:
Délka (milisekundy) pro vložení 100 000 řádků
U čtení měla komprese mnohem větší dopad na výkon než šifrování nebo vzdálené čtení dat:
Doba trvání (milisekundy) na přečtení 100 náhodných řádků 1000krát
Výsledky v prostoru
Jak jste možná předpověděli, komprese může výrazně snížit množství místa potřebného k uložení těchto dat (zhruba na polovinu), zatímco u šifrování lze pozorovat dopad na velikost dat v opačném směru (téměř ztrojnásobit ji). A komprimovat zašifrované hodnoty se samozřejmě nevyplácí:
Použitý prostor (KB) k uložení 100 000 řádků s kompresí nebo bez ní a s nebo bez šifrování
Shrnutí
To by vám mělo poskytnout přibližnou představu o tom, jaký dopad bude mít implementace Always Encrypted. Mějte však na paměti, že se jednalo o velmi konkrétní test a že jsem používal rané sestavení CTP. Vaše data a vzorce přístupu mohou přinést velmi odlišné výsledky a další pokroky v budoucích CTP a aktualizace rozhraní .NET Framework mohou některé z těchto rozdílů snížit i v tomto testu.
Také si všimnete, že výsledky zde byly vesměs trochu jiné než v mém předchozím příspěvku. To lze vysvětlit:
- Časy vkládání byly ve všech případech rychlejší, protože již nepotřebuji další zpáteční cestu do databáze, abych vygeneroval náhodné jméno a plat.
- Doby výběru byly ve všech případech rychlejší, protože již nepoužívám nedbalou metodu zřetězení řetězců (která byla zahrnuta jako součást metriky trvání).
- Použitý prostor byl v obou případech o něco větší, mám podezření, že kvůli odlišnému rozložení náhodných řetězců, které byly vygenerovány.
Příloha A – Kód aplikace konzoly C#
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(); } } } } }