LEISTUNG DES MARIADB JAVA CONNECTORS
Wir reden immer über Leistung. Aber die Sache ist immer „Messen, nicht raten!“.
Am MariaDB Java Connector wurden in letzter Zeit viele Leistungsverbesserungen vorgenommen. Also, was ist die aktuelle Treiberleistung?
Lassen Sie mich ein Benchmark-Ergebnis von 3 jdbc-Treibern teilen, die den Zugriff auf eine MySQL/MariaDB-Datenbank ermöglichen: DrizzleJDBC, MySQL Connector/J und MariaDB Java Connector.
Die Treiberversionen sind die neueste verfügbare GA-Version zum Zeitpunkt der Erstellung dieses Blogs:
- MariaDB 1.5.3
- MySQL 5.1.39
- Nieselregen 1.4
DIE BENCHMARK
JMH ist ein von Oracle entwickeltes Oracle-Micro-Benchmarking-Framework-Tool, das als openJDK-Tools bereitgestellt wird und die offizielle Java 9-Microbenchmark-Suite sein wird. Sein entscheidender Vorteil gegenüber anderen Frameworks besteht darin, dass es von denselben Leuten bei Oracle entwickelt wird, die JIT (Just In Time Compilation) implementieren und es ermöglichen, die meisten Fallstricke von Micro-Benchmarks zu vermeiden.
Benchmark-Quelle: https://github.com/rusher/mariadb-java-driver-benchmark.
Tests sind ziemlich einfach, wenn Sie mit Java vertraut sind.
Beispiel:
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); } } } }
Tests mit INSERT-Abfragen werden an eine BLACKHOLE Engine mit deaktiviertem Binärlog gesendet, um E/A und Abhängigkeit von der Speicherleistung zu vermeiden. Dies ermöglicht stabilere Ergebnisse.
(Ohne die Verwendung der Blackhole-Engine und das Deaktivieren des Binärlogs würden die Ausführungszeiten um bis zu 10% variieren).
Benchmarks wurden auf den Datenbanken MariaDB Server 10.1.17 und MySQL Community Server 5.7.13 ausgeführt. Das folgende Dokument zeigt Ergebnisse mit den 3 Treibern mit MariaDB Server 10.1.17. Die vollständigen Ergebnisse, einschließlich derer mit MySQL Server 5.7.13, finden Sie unter dem Link am Ende des Dokuments.
UMWELT
Die Ausführung (Client und Server) erfolgt auf einem einzigen Server-Droplet auf digitalocean.com unter Verwendung der folgenden Parameter:
- Java(TM) SE Runtime Environment (Build 1.8.0_101-b13) 64 Bits (tatsächlich letzte Version beim Ausführen dieses Benchmarks)
- Ubuntu 16.04 64-Bit
- 512 MB Arbeitsspeicher
- 1 CPU
- Datenbank MariaDB „10.1.17-MariaDB“, MySQL Community Server Build „5.7.15-0ubuntu0.16.04.1“
unter Verwendung von Standardkonfigurationsdateien und diesen zusätzlichen Optionen:- max_allowed_packet =40 MB #Austauschpaket kann bis zu 40 MB groß sein
- character-set-server =utf8 #um UTF-8 als Standard zu verwenden
- collation-server =utf8_unicode_ci #um UTF-8 als Standard zu verwenden
Wenn „entfernt“ angegeben ist, werden Benchmarks mit separatem Client und Server auf zwei identischen Hosts im selben Rechenzentrum mit einem durchschnittlichen Ping von 0,350 ms ausgeführt.
ERGEBNISSE BEISPIELERKLÄRUNGEN
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
Dies bedeutet, dass diese einfache Abfrage mit dem MariaDB-Treiber durchschnittlich 62,715 Mikrosekunden dauert, mit einer Abweichung von ± 2,402 Mikrosekunden für 99,9 % der Abfragen.
Die gleiche Ausführung mit dem Drizzle-Treiber dauert durchschnittlich 88,670 Mikrosekunden und 78,672 Mikrosekunden mit MySQL-Connector (kürzere Ausführungszeit, desto besser).
Angezeigte Prozentsätze werden gemäß dem ersten mariadb-Ergebnis als Referenz (100 %) festgelegt, wodurch andere Ergebnisse leicht verglichen werden können.
LEISTUNGSVERGLEICHE
Der Benchmark testet die Leistung der 3 wichtigsten unterschiedlichen Verhaltensweisen unter Verwendung einer gleichen lokalen Datenbank (gleicher Server) und einer entfernten Datenbank (ein weiterer identischer Server) im gleichen Rechenzentrum mit einem durchschnittlichen Ping von 0,450 ms
Unterschiedliche Verhaltensweisen:
Textprotokoll
Dies entspricht der deaktivierten Option useServerPrepStmts.
Abfragen werden direkt an den Server gesendet, wobei der bereinigte Parameteraustausch clientseitig erfolgt.
Daten werden wie Text gesendet. Beispiel:Ein Zeitstempel wird als Text „1970-01-01 00:00:00.000500“ mit 26 Bytes gesendet
Binäres Protokoll
Dies entspricht der Option useServerPrepStmts enabled (Standardimplementierung auf MariaDB-Treiber).
Daten werden binär gesendet. Der Beispielzeitstempel „1970-01-01 00:00:00.000500“ wird mit 11 Byte gesendet.
Es gibt bis zu 3 Austauschvorgänge mit dem Server für eine Abfrage:
- PREPARE – Bereitet die Anweisung für die Ausführung vor.
- EXECUTE – Parameter senden
- DEALLOCATE PREPARE – Gibt eine vorbereitete Anweisung frei.
Weitere Informationen finden Sie in der Dokumentation zur Servervorbereitung.
PREPARE-Ergebnisse werden im Cache auf der Treiberseite gespeichert (Standardgröße 250). Wenn sich Prepare bereits im Cache befindet, wird PREPARE nicht ausgeführt, DEALLOCATE wird nur ausgeführt, wenn PREPARE nicht mehr verwendet wird und sich nicht im Cache befindet. Das bedeutet, dass einige Abfrageausführungen 3 Roundtrips haben, andere aber nur einen Roundtrip, wobei eine PREPARE-Kennung und Parameter gesendet werden.
Umschreiben
Dies entspricht der Option rewriteBatchedStatements enabled.
Rewrite verwendet das Textprotokoll und betrifft nur Batches. Der Treiber schreibt die Abfrage für schnellere Ergebnisse um.
Beispiel:
Einfügen in ab (i)-Werte (?) mit den ersten Stapelwerten [1] und [2] werden umgeschrieben zu
Einfügen in ab (i)-Werte (1), (2).
Wenn die Abfrage nicht in „Mehrfachwerte“ umgeschrieben werden kann, verwendet das Umschreiben Mehrfachabfragen:
Einfügen in Tabelle(col1)-Werte (?) bei doppelter Schlüsselaktualisierung col2=? mit den Werten [1,2] und [2,3] werden umgeschrieben zu
Werte in Tabelle(Spalte1) einfügen (1) bei Aktualisierung des doppelten Schlüssels Spalte2=2;Werte in Tabelle(Spalte1) einfügen (3) an Aktualisierung des doppelten Schlüssels col2=4
Nachteile dieser Option sind:
- IDs mit automatischer Erhöhung können nicht mit Statement.html#getGeneratedKeys() abgerufen werden.
- Mehrere Abfragen in einer Ausführung sind aktiviert. Das ist kein Problem für PreparedStatement, aber wenn die Anwendung Statement verwendet, kann das eine Sicherheitsverschlechterung sein (SQL-Injection).
* MariaDB und MySQL haben diese 3 Verhaltensweisen implementiert, Drizzle nur das Textprotokoll.
BENCHMARK-ERGEBNISSE
MariaDB-Treiberergebnisse
EINZELAUSWAHLABFRAGE
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
Wenn sich das PREPARE-Ergebnis für genau diese Abfrage bereits im Cache befindet (Cache-Treffer), ist die Abfrage schneller (7,1 % in diesem Beispiel) als die Verwendung des Textprotokolls. Aufgrund der zusätzlichen Anforderungsaustausche PREPARE und DEALLOCATE ist der Cache-Fehler um 68,1 % langsamer.
Dies betont die Vorteile und Nachteile der Verwendung eines binären Protokolls. Cache HIT ist wichtig.
SINGLE INSERT QUERY
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
Ergebnisse für INSERTs ähneln den Ergebnissen von SELECTs.
BATCH :1000 INSERT QUERY
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
Die Verwendung des Binärprotokolls ist hier wichtiger, da Ergebnisse 13 % schneller erzielt werden als bei der Verwendung des Textprotokolls.
Inserts werden per Bulk gesendet und Ergebnisse asynchron gelesen (das entspricht der OptionuseBatchMultiSend). Dies ermöglicht entfernte Ergebnisse mit einer Leistung, die nicht weit von der lokalen entfernt ist.
Rewrite hat eine erstaunlich gute Leistung, hat aber keine Auto-Increment-IDs. Wenn Sie IDs nicht sofort benötigen und ORM nicht verwenden, ist diese Lösung die schnellste. Einige ORM-Konfigurationen erlauben die interne Verarbeitung von Sequenzen, um Inkrement-IDs bereitzustellen, aber diese Sequenzen werden nicht verteilt und funktionieren daher nicht auf Clustern.
VERGLEICH MIT ANDEREN FAHRERN
SELECT-Abfrage mit einem Zeilenergebnis
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 steht für „High Availability“ unter Verwendung der Master-Slave-Konfiguration
(Verbindungs-URL ist „jdbc:mysql:replication://localhost:3306,localhost:3306/testj“).
Diese Ergebnisse sind auf viele verschiedene Implementierungsoptionen zurückzuführen. Hier sind einige Gründe, die Zeitunterschiede erklären:
- MariaDB-Treiber ist für UTF-8 optimiert, wodurch weniger Byte-Arrays erstellt, Array-Kopien und Speicherverbrauch vermieden werden.
- HA-Implementierung:MariaDB- und MySQL-Treiber verwenden eine dynamische Java-Proxyklasse, die zwischen Statement-Objekten und Sockets sitzt und das Hinzufügen von Failover-Verhalten ermöglicht. Diese Hinzufügung kostet einen Overhead von 2 Mikrosekunden pro Abfrage (62,715 ohne werden 64,676 Mikrosekunden).
In der MySQL-Implementierung werden fast alle internen Methoden über Proxys ausgeführt, was einen Overhead für viele Methoden hinzufügt, die nichts mit Failover zu tun haben ein Gesamtaufwand von 50 Mikrosekunden für jede Abfrage.
(Drizzle hat weder PREPARE noch HA-Funktionalität)
„1000 Zeilen auswählen“
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
Wenn viele Daten verwendet werden, wird die Zeit hauptsächlich damit verbracht, aus dem Socket zu lesen und das Ergebnis im Speicher zu speichern, um es an den Client zurückzusenden. Wenn der Benchmark nur SELECT ausführte, ohne die Ergebnisse zu lesen, wäre die Ausführungszeit von MySQL und MariaDB gleich. Da das Ziel einer SELECT-Abfrage darin besteht, Ergebnisse zu erhalten, ist der MariaDB-Treiber so optimiert, dass er Ergebnisse zurückgibt (wobei die Erstellung von Byte-Arrays vermieden wird).
"1000 Zeilen einfügen"
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
Die Masseneinfügung von MySQL und Drizzle ist wie die von X INSERT:Der Treiber sendet 1 INSERT, wartet auf das Ergebnis der Einfügung und sendet die nächste Einfügung. Die Netzwerklatenz zwischen den einzelnen Einfügungen verlangsamt die Einfügungen.
Prozeduren speichern
VERFAHRENSAUFRUF
//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- und MariaDB-Implementierungen unterscheiden sich vollständig. Der MySQL-Treiber verwendet viele versteckte Abfragen, um das Ausgabeergebnis zu erhalten:
SHOW CREATE PROCEDURE testj.inoutParam
um IN- und OUT-Parameter zu identifizierenSET @com_mysql_jdbc_outparam_p1 = 1
um Daten gemäß IN / OUT-Parametern zu sendenCALL testj.inoutParam(@com_mysql_jdbc_outparam_p1)
AufrufverfahrenSELECT @com_mysql_jdbc_outparam_p1
um das Ausgabeergebnis zu lesen
Die MariaDB-Implementierung ist unkompliziert, da die Möglichkeit besteht, OUT-Parameter in der Serverantwort ohne zusätzliche Abfragen zu haben. (Das ist der Hauptgrund, warum der MariaDB-Treiber die MariaDB/MySQL-Serverversion 5.5.3 oder höher erfordert).
SCHLUSSFOLGERUNG
MariaDB-Treiber rockt!
Das Binärprotokoll hat verschiedene Vorteile, ist jedoch darauf angewiesen, dass die PREPARE-Ergebnisse bereits im Cache vorhanden sind. Wenn Anwendungen viele verschiedene Arten von Abfragen haben und die Datenbank weit entfernt ist, ist dies möglicherweise nicht die bessere Lösung.
Rewrite hat erstaunliche Ergebnisse, um Daten im Batch zu schreiben
Der Fahrer hält sich gut im Vergleich zu anderen Fahrern. Und es wird noch viel kommen, aber das ist eine andere Geschichte.
Rohergebnisse:
- mit einer MariaDB 10.1.17-Datenbank lokal, entfernt
- mit einer MySQL Community Server 5.7.15-Datenbank (Build 5.7.15-0ubuntu0.16.04.1) lokal