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

MariaDBJavaコネクタドライバーのパフォーマンス

    MARIADBJAVAコネクタのパフォーマンス

    私たちは常にパフォーマンスについて話します。しかし、それは常に「測定する、推測しないでください!」です。

    最近、MariaDBJavaコネクタで多くのパフォーマンスの向上が行われました。では、現在のドライバーのパフォーマンスはどうですか?

    MySQL / MariaDBデータベースへのアクセスを許可する3つのjdbcドライバー(DrizzleJDBC、MySQL Connector / J、MariaDB javaコネクター)のベンチマーク結果を共有しましょう。

    ドライバーのバージョンは、このブログの執筆時点で利用可能な最新のGAバージョンです:

    • MariaDB 1.5.3
    • MySQL 5.1.39
    • ドリズル1.4

    ベンチマーク

    JMHは、Oracleによって開発されたOracleマイクロベンチマークフレームワークツールであり、openJDKツールとして提供され、公式のjava9マイクロベンチマークスイートになります。他のフレームワークに対するその際立った利点は、JIT(Just In Timeコンパイル)を実装し、マイクロベンチマークの落とし穴のほとんどを回避できるOracleの同じ人たちによって開発されていることです。

    ベンチマークソース:https://github.com/rusher/mariadb-java-driver-benchmark。

    Javaに精通している場合、テストは非常に簡単です。
    例:

    public class BenchmarkSelect1RowPrepareText extends BenchmarkSelect1RowPrepareAbstract {
    
        @Benchmark
        public String mysql(MyState state) throws Throwable {
            return select1RowPrepare(state.mysqlConnectionText, state);
        }
    
        @Benchmark
        public String mariadb(MyState state) throws Throwable {
            return select1RowPrepare(state.mariadbConnectionText, state);
        }
      
        @Benchmark
        public String drizzle(MyState state) throws Throwable {
            return select1RowPrepare(state.drizzleConnectionText, state);
        }
      
    }
    
    public abstract class BenchmarkSelect1RowPrepareAbstract extends BenchmarkInit {
        private String request = "SELECT CAST(? as char character set utf8)";
    
        public String select1RowPrepare(Connection connection, MyState state) throws SQLException {
            try (PreparedStatement preparedStatement = connection.prepareStatement(request)) {
                preparedStatement.setString(1, state.insertData[state.counter++]);
                try (ResultSet rs = preparedStatement.executeQuery()) {
                    rs.next();
                    return rs.getString(1);
                }
            }
        }
    }
    

    INSERTのクエリを使用したテストは、IOとストレージパフォーマンスへの依存を回避するために、バイナリログが無効になっているBLACKHOLEエンジンに送信されます。これにより、より安定した結果が得られます。
    (ブラックホールエンジンを使用せず、バイナリログを無効にしないと、実行時間は最大10%変動します)。

    ベンチマークは、MariaDBServer10.1.17およびMySQLCommunityServer5.7.13データベースで実行されました。次のドキュメントは、MariaDBサーバー10.1.17で3つのドライバーを使用した結果を示しています。 MySQL Server 5.7.13の結果を含む完全な結果については、ドキュメントの下部にあるリンクを参照してください。

    環境

    実行(クライアントとサーバー)は、次のパラメーターを使用して、digitalocean.com上の単一のサーバードロップレットで実行されます。

    • Java(TM)SEランタイム環境(ビルド1.8.0_101-b13)64ビット(このベンチマークを実行する場合の実際の最後のバージョン)
    • Ubuntu16.0464ビット
    • 512Mbメモリ
    • 1 CPU
    • データベースMariaDB「10.1.17-MariaDB」、MySQL Community Serverビルド「5.7.15-0ubuntu0.16.04.1」
      デフォルトの構成ファイルとこれらの追加オプションを使用:

      • max_allowed_pa​​cket =40M#exchangeパケットは最大40mbにすることができます
      • character-set-server =utf8#デフォルトとしてUTF-8を使用する
      • collat​​ion-server =utf8_unicode_ci#デフォルトとしてUTF-8を使用する

    「遠い」と表示されている場合、ベンチマークは、同じデータセンター上の2つの同一ホスト上で別々のクライアントとサーバーを使用して実行され、平均pingは0.350ミリ秒です。

    結果のサンプル説明

    Benchmark                                           Score     Error  Units
    BenchmarkSelect1RowPrepareText.mariadb              62.715 ±  2.402  µs/op
    BenchmarkSelect1RowPrepareText.mysql                88.670 ±  3.505  µs/op
    BenchmarkSelect1RowPrepareText.drizzle              78.672 ±  2.971  µs/op
    

    つまり、この単純なクエリは、MariaDBドライバーを使用して平均62.715マイクロ秒かかり、クエリの99.9%で±2.402マイクロ秒の変動があります。
    drizzleドライバーを使用した同じ実行には、平均88.670マイクロ秒かかります。 MySQLコネクタを使用した場合は78.672マイクロ秒(実行時間は短いほど良い)。

    表示されるパーセンテージは、他の結果を簡単に比較できるように、参照としてのmariadbの最初の結果(100%)に従って設定されます。

    パフォーマンスの比較

    ベンチマークでは、同じローカルデータベース(同じサーバー)と、同じデータセンター上の離れたデータベース(別の同じサーバー)を使用して、平均pingが0.450ミリ秒の3つの主要な異なる動作のパフォーマンスをテストします。

    さまざまな動作:

    テキストプロトコル

    これは、オプションuseServerPrepStmtsが無効になっていることに対応します。
    クエリはサーバーに直接送信され、クライアント側でサニタイズされたパラメータの置換が行われます。
    データはテキストのように送信されます。例:タイムスタンプは、26バイトを使用してテキスト「1970-01-0100:00:00.000500」のように送信されます

    バイナリプロトコル

    これは、オプションuseServerPrepStmtsが有効になっていることに対応します(MariaDBドライバーのデフォルトの実装)。
    データはバイナリで送信されます。タイムスタンプ「1970-01-0100:00:00.000500」の例は、11バイトを使用して送信されます。

    1つのクエリに対してサーバーとの交換は最大3つあります:

    1. PREPARE –実行するステートメントを準備します。
    2. EXECUTE –パラメータを送信
    3. DEALLOCATE PREPARE –準備されたステートメントをリリースします。

    詳細については、サーバー準備ドキュメントをご覧ください。

    PREPAREの結果は、ドライバー側のキャッシュに保存されます(デフォルトサイズは250)。 Prepareがすでにキャッシュにある場合、PREPAREは実行されず、DEALLOCATEは、PREPAREが使用されなくなり、キャッシュにない場合にのみ実行されます。つまり、一部のクエリ実行には3回のラウンドトリップがありますが、一部のクエリ実行には1回のラウンドトリップがあり、PREPARE識別子とパラメータが送信されます。

    書き換え

    これは、有効になっているオプションrewriteBatchedStatementsに対応します。
    Rewriteはテキストプロトコルを使用し、バッチのみに関係します。ドライバーは、より高速な結果を得るためにクエリを書き直します。

    例:
    最初のバッチ値[1]および[2]を使用してab(i)値(?)に挿入すると、
    ab(i)値(1)、(2)に挿入されます。

    クエリを「複数の値」で書き直すことができない場合、書き直しは複数のクエリを使用します:
    重複キーの更新col2=?でtable(col1)値(?)に挿入します値が[1,2]および[2,3]の場合、次のように書き換えられます。
    重複キー更新時にテーブル(col1)値(1)に挿入col2 =2;テーブル(col1)値(3)に挿入重複キー更新col2=4

    このオプションの欠点は次のとおりです。

    • 自動インクリメントIDは、Statement.html#getGeneratedKeys()を使用して取得できません。
    • 1回の実行で複数のクエリが有効になります。これはPreparedStatementの問題ではありませんが、アプリケーションがステートメントを使用している場合は、セキュリティが低下する可能性があります(SQLインジェクション)。

    * MariaDBとMySQLには、これら3つの動作が実装されており、Drizzleはテキストプロトコルのみです。

    ベンチマーク結果

    MariaDBドライバーの結果

    シングルセレクトクエリ

    private String request = "SELECT CAST(? as char character set utf8)";
    
    public String select1RowPrepare(Connection connection, MyState state) throws SQLException {
        try (PreparedStatement preparedStatement = connection.prepareStatement(request)) {
            preparedStatement.setString(1, state.insertData[state.counter++]); //a random 100 bytes.
            try (ResultSet rs = preparedStatement.executeQuery()) {
                rs.next();
                return rs.getString(1);
            }
        }
    }
    
    LOCAL DATABASE:
    BenchmarkSelect1RowPrepareHit.mariadb               58.267 ±  2.270  µs/op
    BenchmarkSelect1RowPrepareMiss.mariadb             118.896 ±  5.500  µs/op
    BenchmarkSelect1RowPrepareText.mariadb              62.715 ±  2.402  µs/op
    DISTANT DATABASE:
    BenchmarkSelect1RowPrepareHit.mariadb               394.354 ±  13.102  µs/op
    BenchmarkSelect1RowPrepareMiss.mariadb              709.843 ±  31.090  µs/op
    BenchmarkSelect1RowPrepareText.mariadb              422.215 ±  15.858  µs/op

    この正確なクエリのPREPARE結果がすでにキャッシュにある場合(キャッシュヒット)、テキストプロトコルを使用するよりもクエリが高速になります(この例では7.1%)。追加のリクエストPREPAREとDEALLOCATEの交換により、キャッシュミスは68.1%遅くなります。

    これは、バイナリプロトコルを使用することの利点と不便さを強調しています。キャッシュヒットは重要です。

    単一挿入クエリ

    private String request = "INSERT INTO blackholeTable (charValue) values (?)";
    
    public boolean executeOneInsertPrepare(Connection connection, String[] datas) throws SQLException {
        try (PreparedStatement preparedStatement = connection.prepareStatement(request)) {
            preparedStatement.setString(1, datas[0]); //a random 100 byte data
            return preparedStatement.execute();
        }
    }
    
    LOCAL DATABASE:
    BenchmarkOneInsertPrepareHit.mariadb                 61.298 ±  1.940  µs/op
    BenchmarkOneInsertPrepareMiss.mariadb               130.896 ±  6.362  µs/op
    BenchmarkOneInsertPrepareText.mariadb                68.363 ±  2.686  µs/op
    DISTANT DATABASE:
    BenchmarkOneInsertPrepareHit.mariadb                379.295 ±  17.351  µs/op
    BenchmarkOneInsertPrepareMiss.mariadb               802.287 ±  24.825  µs/op
    BenchmarkOneInsertPrepareText.mariadb               415.125 ±  14.547  µs/op

    INSERTの結果はSELECTの結果と似ています。

    バッチ:1000クエリの挿入

    private String request = "INSERT INTO blackholeTable (charValue) values (?)";
    
    public int[] executeBatch(Connection connection, String[] data) throws SQLException {
      try (PreparedStatement preparedStatement = connection.prepareStatement(request)) {
        for (int i = 0; i < 1000; i++) {
          preparedStatement.setString(1, data[i]); //a random 100 byte data
          preparedStatement.addBatch();
        }
        return preparedStatement.executeBatch();
      }
    }
    LOCAL DATABASE:        
    PrepareStatementBatch100InsertPrepareHit.mariadb    5.290 ±  0.232  ms/op
    PrepareStatementBatch100InsertRewrite.mariadb       0.404 ±  0.014  ms/op
    PrepareStatementBatch100InsertText.mariadb          6.081 ±  0.254  ms/op
    DISTANT DATABASE:        
    PrepareStatementBatch100InsertPrepareHit.mariadb    7.639 ±   0.476  ms/op
    PrepareStatementBatch100InsertRewrite.mariadb       1.164 ±   0.037  ms/op
    PrepareStatementBatch100InsertText.mariadb          8.148 ±   0.563  ms/op

    ここでは、バイナリプロトコルを使用する方が重要であり、テキストプロトコルを使用するよりも13%高速に結果が得られます。

    挿入は一括で送信され、結果は非同期で読み取られます(optionuseBatchMultiSendに対応します)。これにより、ローカルのパフォーマンスからそれほど遠くないパフォーマンスで、遠くの結果を得ることができます。

    Rewriteのパフォーマンスは驚くほど優れていますが、自動インクリメントIDはありません。 IDをすぐに必要とせず、ORMを使用しない場合は、このソリューションが最速になります。一部のORMでは、シーケンスを内部で処理して増分IDを提供する構成が許可されていますが、これらのシーケンスは分散されていないため、クラスターでは機能しません。

    他のドライバーとの比較

    1行の結果を含むSELECTクエリ

    BenchmarkSelect1RowPrepareHit.mariadb                58.267 ±  2.270  µs/op
    BenchmarkSelect1RowPrepareHit.mysql                  73.789 ±  1.863  µs/op
    BenchmarkSelect1RowPrepareMiss.mariadb              118.896 ±  5.500  µs/op
    BenchmarkSelect1RowPrepareMiss.mysql                150.679 ±  4.791  µs/op
    BenchmarkSelect1RowPrepareText.mariadb               62.715 ±  2.402  µs/op
    BenchmarkSelect1RowPrepareText.mysql                 88.670 ±  3.505  µs/op
    BenchmarkSelect1RowPrepareText.drizzle               78.672 ±  2.971  µs/op
    BenchmarkSelect1RowPrepareTextHA.mariadb             64.676 ±  2.192  µs/op
    BenchmarkSelect1RowPrepareTextHA.mysql              137.289 ±  4.872  µs/op

    HAは、マスタースレーブ構成を使用した「高可用性」の略です
    (接続URLは「jdbc:mysql:replication:// localhost:3306、localhost:3306 / testj」です)。

    これらの結果は、多くの異なる実装の選択によるものです。時差を説明するいくつかの理由は次のとおりです。

    • MariaDBドライバーはUTF-8用に最適化されているため、バイト配列の作成が少なくなり、配列のコピーとメモリの消費が回避されます。
    • HAの実装:MariaDBおよびMySQLドライバーは、Statementオブジェクトとソケットの間にあるJava動的Proxyclassを使用して、フェイルオーバー動作を追加できるようにします。これらの追加には、クエリごとに2マイクロ秒のオーバーヘッドがかかります(64.676マイクロ秒になることなく62.715)。
      MySQLの実装では、ほぼすべての内部メソッドがプロキシされ、フェイルオーバーとは関係のない多くのメソッドのオーバーヘッドが追加されます。すべてのクエリに対して合計50マイクロ秒のオーバーヘッド。

    (DrizzleにはPREPAREもHA機能もありません)

    「1000行を選択」

    private String request = "select * from seq_1_to_1000"; //using the sequence storage engine
    
    private ResultSet select1000Row(Connection connection) throws SQLException {
      try (Statement statement = connection.createStatement()) {
        try (ResultSet rs = statement.executeQuery(request)) {
          while (rs.next()) {
            rs.getString(1);
          }
          return rs;
        }
      }
    
    BenchmarkSelect1000Rows.mariadb                     244.228 ±  7.686  µs/op
    BenchmarkSelect1000Rows.mysql                       298.814 ± 12.143  µs/op
    BenchmarkSelect1000Rows.drizzle                     406.877 ± 16.585  µs/op

    大量のデータを使用する場合、時間は主にソケットからの読み取りと、結果をクライアントに送り返すためのメモリへの保存に費やされます。ベンチマークが結果を読み取らずにSELECTを実行するだけの場合、MySQLとMariaDBの実行時間は同等になります。 SELECTクエリの目的は結果を取得することであるため、MariaDBドライバーは結果を返すように最適化されています(バイト配列の作成を回避します)。

    「1000行を挿入」

    LOCAL DATABASE:        
    PrepareStatementBatch100InsertPrepareHit.mariadb    5.290 ±  0.232  ms/op
    PrepareStatementBatch100InsertPrepareHit.mysql      9.015 ±  0.440  ms/op
    PrepareStatementBatch100InsertRewrite.mariadb       0.404 ±  0.014  ms/op
    PrepareStatementBatch100InsertRewrite.mysql         0.592 ±  0.016  ms/op
    PrepareStatementBatch100InsertText.mariadb          6.081 ±  0.254  ms/op
    PrepareStatementBatch100InsertText.mysql            7.932 ±  0.293  ms/op
    PrepareStatementBatch100InsertText.drizzle          7.314 ±  0.205  ms/op
    DISTANT DATABASE:        
    PrepareStatementBatch100InsertPrepareHit.mariadb     7.639 ±   0.476  ms/op
    PrepareStatementBatch100InsertPrepareHit.mysql      43.636 ±   1.408  ms/op
    PrepareStatementBatch100InsertRewrite.mariadb        1.164 ±   0.037  ms/op
    PrepareStatementBatch100InsertRewrite.mysql          1.432 ±   0.050  ms/op
    PrepareStatementBatch100InsertText.mariadb           8.148 ±   0.563  ms/op
    PrepareStatementBatch100InsertText.mysql            43.804 ±   1.417  ms/op
    PrepareStatementBatch100InsertText.drizzle          38.735 ±   1.731  ms/op

    MySQLとDrizzleの一括挿入は、X INSERTのようなものです。ドライバーは1つのINSERTを送信し、挿入結果を待ってから、次の挿入を送信します。各挿入間のネットワーク遅延により、挿入が遅くなります。

    ストアドプロシージャ

    手順の呼び出し

    //CREATE PROCEDURE inoutParam(INOUT p1 INT) begin set p1 = p1 + 1; end
    private String request = "{call inOutParam(?)}";
    
    private String callableStatementWithOutParameter(Connection connection, MyState state) 
    		throws SQLException {
      try (CallableStatement storedProc = connection.prepareCall(request)) {
        storedProc.setInt(1, state.functionVar1); //2
        storedProc.registerOutParameter(1, Types.INTEGER);
        storedProc.execute();
        return storedProc.getString(1);
      }
    }
    BenchmarkCallableStatementWithOutParameter.mariadb   88.572 ±  4.263  µs/op
    BenchmarkCallableStatementWithOutParameter.mysql    714.108 ± 44.390  µs/op
    

    MySQLとMariaDBの実装は完全に異なります。 Mysqlドライバーは、多くの非表示のクエリを使用して出力結果を取得します:

    • SHOW CREATE PROCEDURE testj.inoutParam INおよびOUTパラメータを識別するため
    • SET @com_mysql_jdbc_outparam_p1 = 1 IN/OUTパラメータに従ってデータを送信する
    • CALL testj.inoutParam(@com_mysql_jdbc_outparam_p1) 呼び出し手順
    • SELECT @com_mysql_jdbc_outparam_p1 出力結果を読み取る

    MariaDBの実装は、追加のクエリなしでサーバー応答にOUTパラメーターを含める機能を使用して簡単に実行できます。 (これが、MariaDBドライバーがMariaDB / MySQLサーバーバージョン5.5.3以降を必要とする主な理由です。)

    結論

    MariaDBドライバーは素晴らしいです!

    バイナリプロトコルにはさまざまな利点がありますが、PREPAREの結果がすでにキャッシュにあることに依存しています。アプリケーションにさまざまな種類のクエリがあり、データベースが離れている場合、それはより良い解決策ではない可能性があります。

    リライトは、データをバッチで書き込むという驚くべき結果をもたらします

    ドライバーは他のドライバーと比べてしっかりと保持します。まだまだたくさんありますが、それはまた別の話です。

    生の結果:

    1. MariaDB 10.1.17データベースを使用して、ローカル、遠隔地
    2. MySQL Community Server 5.7.15データベース(ビルド5.7.15-0ubuntu0.16.04.1)ローカル

    1. MySQLINクエリで順序を維持する

    2. ロールバックスクリプトオラクルの自動化

    3. ClusterControlとProxySQLでSQLファイアウォールを簡単に

    4. DUPLICATE KEY +AUTOINCREMENTでmysqlを発行します