Миналата седмица писах за ограниченията на Always Encrypted, както и за въздействието върху производителността. Исках да публикувам последващо действие след извършване на повече тестове, главно поради следните промени:
- Добавих тест за локален, за да видя дали мрежовите разходи са значителни (по-рано тестът беше само отдалечен). Въпреки това трябва да сложа „мрежови разходи“ във въздушните кавички, защото това са две виртуални машини на един и същ физически хост, така че всъщност не е истински анализ на голи метали.
- Добавих няколко допълнителни (некриптирани) колони към таблицата, за да я направя по-реалистична (но всъщност не толкова реалистична).
DateCreated DATETIME NOT NULL DEFAULT SYSUTCDATETIME(), DateModified DATETIME NOT NULL DEFAULT SYSUTCDATETIME(), IsActive BIT NOT NULL DEFAULT 1
След това съответно промени процедурата за извличане:
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
- Добавена е процедура за съкращаване на таблицата (преди това правех това ръчно между тестовете):
CREATE PROCEDURE dbo.Cleanup AS BEGIN SET NOCOUNT ON; TRUNCATE TABLE dbo.Employees; END GO
- Добавена е процедура за записване на времена (преди това ръчно анализирах изхода на конзолата):
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
- Добавих двойка бази данни, които използваха компресиране на страници – всички знаем, че криптираните стойности не се компресират добре, но това е поляризираща функция, която може да се използва едностранно дори в таблици с криптирани колони, така че реших, че просто профилирайте и тези. (И добави още два низа за връзка към
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>
- Направих много подобрения в кода на C# (вижте приложението) въз основа на обратна връзка от tobi (което доведе до този въпрос за преглед на кода) и голяма помощ от колегата Брук Филпот (@Macromullet). Те включват:
- елиминиране на съхранената процедура за генериране на произволни имена/заплати и правене на това в C# вместо това
- с помощта на
Stopwatch
вместо тромави низове за дата/час - по-последователно използване на
using()
и елиминиране на.Close()
- малко по-добри конвенции за именуване (и коментари!)
- промяна на
while
цикли доfor
бримки - с помощта на
StringBuilder
вместо наивна конкатенация (която първоначално бях избрал умишлено) - консолидиране на низовете за връзка (въпреки че все още умишлено правя нова връзка във всяка итерация на цикъл)
След това създадох прост пакетен файл, който ще стартира всеки тест 5 пъти (и повторих това както на локалните, така и на отдалечените компютри):
for /l %%x in (1,1,5) do ( ^ AEDemoConsole "Normal" & ^ AEDemoConsole "Encrypt" & ^ AEDemoConsole "NormalCompress" & ^ AEDemoConsole "EncryptCompress" & ^ )
След като тестовете приключат, измерването на продължителността и използваното пространство ще бъде тривиално (а изграждането на диаграми от резултатите ще отнеме малко манипулация в 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');
Резултати за продължителност
Ето необработените резултати от заявката за продължителност по-горе (CANUCK
е името на машината, която хоства екземпляра на SQL Server и HOSER
е машината, която изпълняваше отдалечената версия на кода):
Необработени резултати от заявката за продължителност
Очевидно ще бъде по-лесно да се визуализира в друга форма. Както е показано на първата графика, отдалеченият достъп имаше значително влияние върху продължителността на вмъкванията (над 40% увеличение), но компресията имаше малко влияние. Само шифроването приблизително удвои продължителността за всяка тестова категория:
Продължителност (милисекунди) за вмъкване на 100 000 реда
За четенията компресията имаше много по-голямо влияние върху производителността, отколкото криптирането или четенето на данните от разстояние:
Продължителност (милисекунди) за четене на 100 произволни реда 1000 пъти
Резултати за пространство
Както може би сте предвидили, компресията може значително да намали количеството пространство, необходимо за съхраняване на тези данни (приблизително наполовина), докато криптирането може да се види, че влияе върху размера на данните в обратна посока (почти го утроява). И, разбира се, компресирането на криптирани стойности не се изплаща:
Използвано пространство (KB) за съхраняване на 100 000 реда със или без компресия и със или без криптиране
Резюме
Това трябва да ви даде груба представа за това какво да очаквате въздействието при внедряване на Always Encrypted. Имайте предвид обаче, че това беше много конкретен тест и че използвах ранна CTP версия. Вашите данни и модели на достъп може да доведат до много различни резултати, а по-нататъшният напредък в бъдещите CTP и актуализациите на .NET Framework може да намали някои от тези разлики дори в този тест.
Също така ще забележите, че резултатите тук бяха малко по-различни в сравнение с предишната ми публикация. Това може да се обясни:
- Времената за вмъкване бяха по-бързи във всички случаи, тъй като вече нямам допълнителен двупосочно пътуване до базата данни, за да генерирам произволно име и заплата.
- Времената за избор бяха по-бързи във всички случаи, защото вече не използвам небрежен метод за конкатенация на низове (който беше включен като част от показателя за продължителност).
- Използваното пространство беше малко по-голямо и в двата случая, подозирам, че поради различно разпределение на произволни низове, които бяха генерирани.
Допълнение A – 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(); } } } } }