MariaDB
 sql >> Datenbank >  >> RDS >> MariaDB

Leistung des MariaDB-Java-Connector-Treibers

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:

  1. PREPARE – Bereitet die Anweisung für die Ausführung vor.
  2. EXECUTE – Parameter senden
  3. 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 identifizieren
  • SET @com_mysql_jdbc_outparam_p1 = 1 um Daten gemäß IN / OUT-Parametern zu senden
  • CALL testj.inoutParam(@com_mysql_jdbc_outparam_p1) Aufrufverfahren
  • SELECT @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:

  1. mit einer MariaDB 10.1.17-Datenbank lokal, entfernt
  2. mit einer MySQL Community Server 5.7.15-Datenbank (Build 5.7.15-0ubuntu0.16.04.1) lokal