Minggu lalu, saya menulis tentang keterbatasan Always Encrypted serta dampak kinerjanya. Saya ingin memposting tindak lanjut setelah melakukan lebih banyak pengujian, terutama karena perubahan berikut:
- Saya menambahkan tes untuk lokal, untuk melihat apakah overhead jaringan signifikan (sebelumnya, tes hanya jarak jauh). Padahal, saya harus memasukkan "overhead jaringan" dalam tanda kutip udara, karena ini adalah dua VM pada host fisik yang sama, jadi bukan benar-benar analisis bare metal.
- Saya menambahkan beberapa kolom tambahan (tidak terenkripsi) ke tabel untuk membuatnya lebih realistis (tetapi tidak terlalu realistis).
DateCreated DATETIME NOT NULL DEFAULT SYSUTCDATETIME(), DateModified DATETIME NOT NULL DEFAULT SYSUTCDATETIME(), IsActive BIT NOT NULL DEFAULT 1
Kemudian ubah prosedur pengambilan yang sesuai:
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
- Menambahkan prosedur untuk memotong tabel (sebelumnya saya melakukannya secara manual di antara pengujian):
CREATE PROCEDURE dbo.Cleanup AS BEGIN SET NOCOUNT ON; TRUNCATE TABLE dbo.Employees; END GO
- Menambahkan prosedur untuk merekam pengaturan waktu (sebelumnya saya secara manual mem-parsing keluaran konsol):
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
- Saya menambahkan sepasang database yang menggunakan kompresi halaman – kita semua tahu bahwa nilai terenkripsi tidak terkompresi dengan baik, tetapi ini adalah fitur polarisasi yang dapat digunakan secara sepihak bahkan pada tabel dengan kolom terenkripsi, jadi saya pikir saya akan melakukannya profil ini juga. (Dan menambahkan dua string koneksi lagi ke
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>
- Saya membuat banyak perbaikan pada kode C# (lihat Lampiran) berdasarkan umpan balik dari tobi (yang mengarah ke pertanyaan Tinjauan Kode ini) dan beberapa bantuan hebat dari rekan kerja Brooke Philpott (@Macromullet). Ini termasuk:
- menghilangkan prosedur tersimpan untuk menghasilkan nama/gaji acak, dan melakukannya di C# sebagai gantinya
- menggunakan
Stopwatch
alih-alih string tanggal/waktu yang canggung - penggunaan
using()
yang lebih konsisten dan penghapusan.Close()
- konvensi penamaan (dan komentar!) yang sedikit lebih baik
- mengubah
while
loop kefor
loop - menggunakan
StringBuilder
alih-alih rangkaian naif (yang awalnya saya pilih dengan sengaja) - mengonsolidasikan string koneksi (meskipun saya masih sengaja membuat koneksi baru dalam setiap iterasi loop)
Kemudian saya membuat file batch sederhana yang akan menjalankan setiap pengujian 5 kali (dan mengulanginya di komputer lokal dan jarak jauh):
for /l %%x in (1,1,5) do ( ^ AEDemoConsole "Normal" & ^ AEDemoConsole "Encrypt" & ^ AEDemoConsole "NormalCompress" & ^ AEDemoConsole "EncryptCompress" & ^ )
Setelah pengujian selesai, mengukur durasi dan ruang yang digunakan akan menjadi hal yang sepele (dan membuat bagan dari hasil hanya memerlukan sedikit manipulasi di 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');
Hasil Durasi
Berikut adalah hasil mentah dari kueri durasi di atas (CANUCK
adalah nama mesin yang menghosting instance SQL Server, dan HOSER
adalah mesin yang menjalankan versi kode jarak jauh):
Hasil mentah dari kueri durasi
Jelas akan lebih mudah untuk memvisualisasikan dalam bentuk lain. Seperti yang ditunjukkan pada grafik pertama, akses jarak jauh memiliki dampak signifikan pada durasi penyisipan (meningkat lebih dari 40%), tetapi kompresi memiliki dampak yang kecil sama sekali. Enkripsi saja secara kasar menggandakan durasi untuk kategori pengujian apa pun:
Durasi (milidetik) untuk menyisipkan 100.000 baris
Untuk pembacaan, kompresi memiliki dampak yang jauh lebih besar pada kinerja daripada enkripsi atau membaca data dari jarak jauh:
Durasi (milidetik) untuk membaca 100 baris acak 1.000 kali
Hasil Luar Angkasa
Seperti yang mungkin telah Anda prediksi, kompresi dapat secara signifikan mengurangi jumlah ruang yang diperlukan untuk menyimpan data ini (kira-kira setengahnya), sedangkan enkripsi dapat dilihat memengaruhi ukuran data ke arah yang berlawanan (hampir tiga kali lipat). Dan, tentu saja, mengompresi nilai terenkripsi tidak akan membuahkan hasil:
Ruang yang digunakan (KB) untuk menyimpan 100.000 baris dengan atau tanpa kompresi dan dengan atau tanpa enkripsi
Ringkasan
Ini akan memberi Anda gambaran kasar tentang dampak yang diharapkan saat menerapkan Always Encrypted. Namun, perlu diingat bahwa ini adalah tes yang sangat khusus, dan saya menggunakan build CTP awal. Data dan pola akses Anda mungkin menghasilkan hasil yang sangat berbeda, dan kemajuan lebih lanjut dalam CTP dan pembaruan di masa mendatang pada .NET Framework dapat mengurangi beberapa perbedaan ini bahkan dalam pengujian ini.
Anda juga akan melihat bahwa hasil di sini sedikit berbeda di seluruh papan daripada di posting saya sebelumnya. Ini bisa dijelaskan:
- Waktu penyisipan lebih cepat dalam semua kasus karena saya tidak lagi mengeluarkan biaya bolak-balik ekstra ke database untuk menghasilkan nama dan gaji acak.
- Waktu yang dipilih lebih cepat dalam semua kasus karena saya tidak lagi menggunakan metode penggabungan string yang ceroboh (yang disertakan sebagai bagian dari metrik durasi).
- Ruang yang digunakan sedikit lebih besar dalam kedua kasus, saya menduga karena distribusi string acak yang dihasilkan berbeda.
Lampiran A – Kode Aplikasi Konsol 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(); } } } } }