Миналата седмица писах за ограниченията на 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();
}
}
}
}
}