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_packet =40M#exchangeパケットは最大40mbにすることができます
- character-set-server =utf8#デフォルトとしてUTF-8を使用する
- collation-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つあります:
- PREPARE –実行するステートメントを準備します。
- EXECUTE –パラメータを送信
- 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の結果がすでにキャッシュにあることに依存しています。アプリケーションにさまざまな種類のクエリがあり、データベースが離れている場合、それはより良い解決策ではない可能性があります。
リライトは、データをバッチで書き込むという驚くべき結果をもたらします
ドライバーは他のドライバーと比べてしっかりと保持します。まだまだたくさんありますが、それはまた別の話です。
生の結果:
- MariaDB 10.1.17データベースを使用して、ローカル、遠隔地
- MySQL Community Server 5.7.15データベース(ビルド5.7.15-0ubuntu0.16.04.1)ローカル