Database
 sql >> база данни >  >> RDS >> Database

Винаги криптирана производителност:последващо действие

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

  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Как работи MapReduce в Hadoop

  2. Разбиране на групата за повторен дневник срещу файл срещу член

  3. Няколко начина за вмъкване на разделени разделени низове в колона

  4. Изчисляване на медиана с динамичен курсор

  5. Как да изчислим възрастта от датата на раждане в SQL