sql >> データベース >  >> RDS >> Sqlserver

SQL Server 2016:常に暗号化されることによるパフォーマンスへの影響

    T-SQL Tuesday#69の一環として、Always Encryptedの制限についてブログを書きました。そこで、パフォーマンスがその使用によって悪影響を受ける可能性があることを説明しました(ご想像のとおり、セキュリティの強化にはトレードオフが伴うことがよくあります)。この投稿では、これらの結果がCTP 2.2コードに基づいているため、開発サイクルの非常に早い段階であり、必ずしもパフォーマンスを反映しているとは限らないことを念頭に置いて、これを簡単に確認したいと思います。 RTMが来るのを見てください。

    まず、SQL Server 2016の最新バージョンがインストールされていなくても、AlwaysEncryptedがクライアントアプリケーションから機能することを示したいと思いました。ただし、Column Encryption Settingをサポートするには、.NET Framework 4.6プレビュー(最新バージョンはここにあり、変更される可能性があります)をインストールする必要があります。 接続文字列属性。 Windows 10を実行している場合、またはVisual Studio 2015をインストールしている場合は、.NET Frameworkの最新バージョンがすでにあるはずなので、この手順は必要ありません。

    次に、AlwaysEncrypted証明書がすべてのクライアントに存在することを確認する必要があります。 Always Encryptedチュートリアルで示されているように、データベース内にマスターと列の暗号化キーを作成します。次に、そのマシンから証明書をエクスポートし、アプリケーションコードが実行される他のマシンにインポートする必要があります。 certmgr.mscを開きます 、[証明書] –[現在のユーザー]>[個人]>[証明書]を展開します。Always Encrypted Certificateと呼ばれるものがあります。 。それを右クリックし、[すべてのタスク]> [エクスポート]を選択して、プロンプトに従います。秘密鍵をエクスポートしてパスワードを入力すると、.pfxファイルが生成されました。次に、クライアントマシンで反対のプロセスを繰り返すだけです。certmgr.mscを開きます。 、[証明書-現在のユーザー]> [個人]を展開し、[証明書]を右クリックして、[すべてのタスク]> [インポート]を選択し、上記で作成した.pfxファイルをポイントします。 (ここでの公式ヘルプ。)

    (これらの証明書を管理するためのより安全な方法があります。すぐに何が重要かを自問するので、このような証明書をすべてのマシンに展開したいとは思わないでしょう。私はこれを隔離された環境でのみ行っていました。このデモの目的のために–アプリケーションがローカルメモリだけでなく、ネットワーク経由でデータを取得していることを確認したかったのです。)

    2つのデータベースを作成します。1つは暗号化されたテーブルあり、もう1つは暗号化されていないテーブルです。これは、接続文字列を分離し、スペース使用量を測定するために行います。もちろん、暗号化が有効な接続を使用する必要があるコマンドを制御するためのよりきめ細かい方法があります。この記事の「パフォーマンスへの影響の制御…」というタイトルのメモを参照してください。

    表は次のようになります:

    -- encrypted copy, in database Encrypted
     
    CREATE TABLE dbo.Employees
    (
      ID INT IDENTITY(1,1) PRIMARY KEY,
      LastName NVARCHAR(32) COLLATE Latin1_General_BIN2 
        ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC,
    	ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256',
    	COLUMN_ENCRYPTION_KEY = ColumnKey) NOT NULL,
      Salary INT
        ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED,
    	ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256',
    	COLUMN_ENCRYPTION_KEY = ColumnKey) NOT NULL
    );
     
    -- unencrypted copy, in database Normal
     
    CREATE TABLE dbo.Employees
    (
      ID INT IDENTITY(1,1) PRIMARY KEY,
      LastName NVARCHAR(32) COLLATE Latin1_General_BIN2 NOT NULL,
      Salary INT NOT NULL
    );

    これらのテーブルを配置したら、非常に単純なコマンドラインアプリケーションをセットアップして、暗号化されたバージョンと暗号化されていないバージョンの両方のテーブルに対して次のタスクを実行したいと思いました。

    • 一度に1人ずつ100,000人の従業員を挿入します
    • ランダムな100行を1,000回読みます
    • 各ステップの前後のタイムスタンプを出力します

    したがって、給与を表すランダムな整数と、さまざまな長さのランダムなUnicode文字列を生成するために使用される、完全に別個のデータベースにストアドプロシージャがあります。独立して発生する100,000の挿入の実際の使用をより適切にシミュレートするために、これを一度に1つずつ実行します(ただし、マルチスレッドC#アプリケーションを適切に開発および管理したり、調整したりする勇気がないため、同時にではありません)単一のアプリケーションの複数のインスタンスを同期します。

    CREATE DATABASE Utility;
    GO
     
    USE Utility;
    GO
     
    CREATE PROCEDURE dbo.GenerateNameAndSalary
      @Name NVARCHAR(32) OUTPUT,
      @Salary INT OUTPUT
    AS
    BEGIN
      SET NOCOUNT ON;
      SELECT @Name = LEFT(CONVERT(NVARCHAR(32), CRYPT_GEN_RANDOM(64)), RAND() * 32 + 1);
      SELECT @Salary = CONVERT(INT, RAND()*100000)/100*100;
    END
    GO

    数行のサンプル出力(文字列の実際の内容は気にせず、変化するだけです):

    酹2׿ዌ륒㦢㮧羮怰㉤盿⚉嗝䬴敏⽁캘♜鼹䓧
    98600
     
    贓峂쌄탠❼缉腱蛽☎뱶
    72000

    次に、アプリケーションが最終的に呼び出すストアドプロシージャ(Always Encryptedをサポートするためにクエリを変更する必要がないため、これらは両方のデータベースで同じです):

    CREATE PROCEDURE dbo.AddPerson
      @LastName NVARCHAR(32),
      @Salary INT
    AS
    BEGIN
      SET NOCOUNT ON;
      INSERT dbo.Employees(LastName, Salary) SELECT @LastName, @Salary;
    END
    GO
     
    CREATE PROCEDURE dbo.RetrievePeople
    AS
    BEGIN
      SET NOCOUNT ON;
      SELECT TOP (100) ID, LastName, Salary 
        FROM dbo.Employees
        ORDER BY NEWID();
    END
    GO

    ここで、App.configのconnectionStrings部分から始まるC#コードです。重要な部分はColumn Encryption Settingです 暗号化された列を持つデータベースのみのオプション(簡潔にするために、3つの接続文字列すべてに同じData Sourceが含まれていると仮定します 、および同じSQL認証User ID およびPassword ):

    <connectionStrings>
      <add name="Utility" connectionString="Initial Catalog=Utility;..."/>
      <add name="Normal"  connectionString="Initial Catalog=Normal;..."/>
      <add name="Encrypt" connectionString="Initial Catalog=Encrypted; Column Encryption Setting=Enabled;..."/>
    </connectionStrings>

    そしてProgram.cs(申し訳ありませんが、このようなデモの場合、論理的に名前を変更するのはひどいです):

    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Configuration;
    using System.Data;
    using System.Data.SqlClient;
     
    namespace AEDemo
    {
        class Program
        {
            static void Main(string[] args)
            {
                using (SqlConnection con1 = new SqlConnection())
                {
                    Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff"));
                    string name;
                    string EmptyString = "";
                    int salary;
                    int i = 1;
                    while (i <= 100000)
                    {
                        con1.ConnectionString = ConfigurationManager.ConnectionStrings["Utility"].ToString();
                        using (SqlCommand cmd1 = new SqlCommand("dbo.GenerateNameAndSalary", con1))
                        {
                            cmd1.CommandType = CommandType.StoredProcedure;
                            SqlParameter n = new SqlParameter("@Name", SqlDbType.NVarChar, 32) 
                                             { Direction = ParameterDirection.Output };
                            SqlParameter s = new SqlParameter("@Salary", SqlDbType.Int) 
                                             { Direction = ParameterDirection.Output };
                            cmd1.Parameters.Add(n);
                            cmd1.Parameters.Add(s);
                            con1.Open();
                            cmd1.ExecuteNonQuery();
                            name = n.Value.ToString();
                            salary = Convert.ToInt32(s.Value);
                            con1.Close();
                        }
     
                        using (SqlConnection con2 = new SqlConnection())
                        {
                            con2.ConnectionString = ConfigurationManager.ConnectionStrings[args[0]].ToString();
                            using (SqlCommand cmd2 = new SqlCommand("dbo.AddPerson", con2))
                            {
                                cmd2.CommandType = CommandType.StoredProcedure;
                                SqlParameter n = new SqlParameter("@LastName", SqlDbType.NVarChar, 32);
                                SqlParameter s = new SqlParameter("@Salary", SqlDbType.Int);
                                n.Value = name;
                                s.Value = salary;
                                cmd2.Parameters.Add(n);
                                cmd2.Parameters.Add(s);
                                con2.Open();
                                cmd2.ExecuteNonQuery();
                                con2.Close();
                            }
                        }
                        i++;
                    }
                    Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff"));
                    i = 1;
                    while (i <= 1000)
                    {
                        using (SqlConnection con3 = new SqlConnection())
                        {
                            con3.ConnectionString = ConfigurationManager.ConnectionStrings[args[0]].ToString();
                            using (SqlCommand cmd3 = new SqlCommand("dbo.RetrievePeople", con3))
                            {
                                cmd3.CommandType = CommandType.StoredProcedure;
                                con3.Open();
                                SqlDataReader rdr = cmd3.ExecuteReader();
                                while (rdr.Read())
                                {
                                    EmptyString += rdr[0].ToString();
                                }
                                con3.Close();
                            }
                        }
                        i++;
                    }
                    Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff"));
                }
            }
        }
    }

    次に、次のコマンドラインを使用して.exeを呼び出すことができます。

    AEDemoConsole.exe "Normal"
    AEDemoConsole.exe "Encrypt"

    また、呼び出しごとに3行の出力が生成されます。開始時間、100,000行が挿入された後の時間、および100行が1,000回読み取られた後の時間です。これが私のシステムで見た結果で、それぞれ5回の実行で平均されました:

    データの書き込みと読み取りの時間(秒)

    データの書き込みには明らかな影響があります。2倍ではありませんが、1.5倍以上です。少なくともこれらのテストでは、データの読み取りと復号化に関してはるかに低いデルタがありましたが、それも無料ではありませんでした。

    スペースの使用に関しては、暗号化されたデータの保存には約3倍のペナルティがあります(ほとんどの暗号化アルゴリズムの性質を考えると、これは衝撃的なことではありません)。これは、クラスター化された主キーが1つしかないテーブル上にあることに注意してください。数字は次のとおりです:

    データの保存に使用されるスペース(MB)

    したがって、セキュリティ関連のソリューションのほぼすべてに通常あるように、Always Encryptedを使用することには明らかにいくつかのペナルティがあります(「無料の昼食なし」という言葉が思い浮かびます)。これらのテストはCTP2.2に対して実行されたことを繰り返します。これは、SQL Server 2016の最終リリースとは根本的に異なる可能性があります。また、私が観察したこれらの違いは、私が作成したテストの性質のみを反映している可能性があります。明らかに、このアプローチを使用して、スキーマ、ハードウェア、およびデータアクセスパターンに対して結果をテストできることを望んでいます。


    1. MySQL、PostgreSQL、SQLiteで結果を制限する方法

    2. 文字列に基づいて動的にPHPオブジェクトを作成する

    3. MySQLのインストール

    4. MySQLを使用した大規模な結果セットのストリーミング