MariaDB
 sql >> Database >  >> RDS >> MariaDB

Prestazioni del driver del connettore Java MariaDB

PRESTAZIONI DEL CONNETTORE JAVA MARIADB

Parliamo sempre di prestazioni. Ma la cosa è sempre "Misura, non indovinare!".

Ultimamente sono stati apportati molti miglioramenti alle prestazioni su MariaDB Java Connector. Allora, quali sono le prestazioni attuali del driver?

Consentitemi di condividere un risultato del benchmark di 3 driver jdbc che consentono l'accesso a un database MySQL/MariaDB: DrizzleJDBC, MySQL Connector/J e MariaDB java Connector.

Le versioni del driver sono l'ultima versione GA disponibile al momento della stesura di questo blog:

  • MariaDB 1.5.3
  • MySQL 5.1.39
  • Pioggia 1.4

IL RIFERIMENTO

JMH è uno strumento framework di micro-benchmarking Oracle sviluppato da Oracle, fornito come strumenti openJDK, che sarà la suite di microbenchmark java 9 ufficiale. Il suo vantaggio distintivo rispetto ad altri framework è che è sviluppato dagli stessi ragazzi in Oracle che implementano JIT (Compilazione Just In Time) e consentono di evitare la maggior parte delle insidie ​​dei micro-benchmark.

Fonte del benchmark: https://github.com/rusher/mariadb-java-driver-benchmark.

I test sono piuttosto semplici se hai familiarità con java.
Esempio:

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);
            }
        }
    }
}

I test che utilizzano le query di INSERT vengono inviati a un motore BLACKHOLE con il log binario disabilitato, per evitare l'IO e la dipendenza dalle prestazioni di archiviazione. Questo permette di avere risultati più stabili.
(Senza usare il motore blackhole e disabilitare il log binario, i tempi di esecuzione varierebbero fino al 10%).

I benchmark sono stati eseguiti sui database MariaDB Server 10.1.17 e MySQL Community Server 5.7.13. Il seguente documento mostra i risultati utilizzando i 3 driver con MariaDB Server 10.1.17. Per i risultati completi, inclusi quelli con MySQL Server 5.7.13, vedere il collegamento in fondo al documento.

AMBIENTE

L'esecuzione (client e server) viene eseguita su un singolo server droplet su digitalocean.com utilizzando i seguenti parametri:

  • Ambiente runtime Java(TM) SE (build 1.8.0_101-b13) 64 bit (ultima versione effettiva durante l'esecuzione di questo benchmark)
  • Ubuntu 16.04 64 bit
  • 512Mb di memoria
  • 1 CPU
  • database MariaDB “10.1.17-MariaDB”, MySQL Community Server build “5.7.15-0ubuntu0.16.04.1”
    utilizzando i file di configurazione predefiniti e queste opzioni aggiuntive :

    • max_allowed_packet =40 milioni di pacchetti di #exchange possono arrivare fino a 40 MB
    • server-set-caratteri =utf8 #per utilizzare UTF-8 come predefinito
    • server-collation =utf8_unicode_ci #per usare UTF-8 come predefinito

Quando indicato come "distante", i benchmark vengono eseguiti con client e server separati su 2 host identici sullo stesso data center con un ping medio di 0,350 ms.

RISULTATI ESEMPI DI SPIEGAZIONI

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

Ciò significa che questa semplice query impiegherà un tempo medio di 62,715 microsecondi utilizzando il driver MariaDB con una variazione di ± 2,402 microsecondi per il 99,9% delle query.
La stessa esecuzione utilizzando il driver drizzle richiederà un tempo medio di 88,670 microsecondi e 78.672 microsecondi utilizzando il connettore MySQL (tempo di esecuzione minore, meglio è).

Le percentuali visualizzate sono impostate in base al primo risultato di mariadb come riferimento (100%), consentendo di confrontare facilmente altri risultati.

CONFRONTO DELLE PRESTAZIONI

Il benchmark testerà le prestazioni dei 3 principali comportamenti differenti utilizzando uno stesso database locale (stesso server) e un database distante (altro server identico) sullo stesso datacenter con un ping medio di 0,450 ms

Comportamenti diversi:

Protocollo di testo

Ciò corrisponde all'opzione useServerPrepStmts disabilitata.
Le query vengono inviate direttamente al server con la sostituzione dei parametri sanificati eseguita sul lato client.
I dati vengono inviati come testo. Esempio:verrà inviato un timestamp come il testo "1970-01-01 00:00:00.000500" utilizzando 26 byte

Protocollo binario

Ciò corrisponde all'opzione useServerPrepStmts enabled (implementazione predefinita sul driver MariaDB).
I dati vengono inviati in formato binario. Il timestamp di esempio "1970-01-01 00:00:00.000500" verrà inviato utilizzando 11 byte.

Ci sono fino a 3 scambi con il server per una query:

  1. PREPARE – Prepara l'istruzione per l'esecuzione.
  2. ESEGUI – Invia parametri
  3. DEALLOCATE PREPARE – Rilascia una dichiarazione preparata.

Per ulteriori informazioni, consulta la documentazione relativa alla preparazione del server.

I risultati di PREPARE sono archiviati nella cache sul lato del driver (dimensione predefinita 250). Se Prepare è già nella cache, PREPARE non verrà eseguito, DEALLOCATE verrà eseguito solo quando PREPARE non viene più utilizzato e non nella cache. Ciò significa che l'esecuzione di alcune query avrà 3 round trip, ma alcune avranno solo un round trip, inviando un identificatore PREPARE e parametri.

Riscrivi

Ciò corrisponde all'opzione rewriteBatchedStatements abilitata.
La riscrittura utilizza il protocollo di testo e riguarda solo i batch. Il driver riscriverà la query per risultati più rapidi.

Esempio:
Inserisci in ab (i) i valori (?) con i primi valori batch [1] e [2] verranno riscritti in
Inserisci in ab (i) i valori (1), (2).

Se la query non può essere riscritta in "multi-values", la riscrittura utilizzerà le multi-query :
Inserisci nei valori della tabella(col1) (?) sull'aggiornamento della chiave duplicata col2=? con i valori [1,2] e [2,3] verrà riscritto in
Inserisci nei valori della tabella(col1) (1) su aggiornamento chiave duplicata col2=2;Inserisci nei valori della tabella(col1) (3) su aggiornamento chiave duplicata col2=4

Gli svantaggi di questa opzione sono:

  • Gli ID di incremento automatico non possono essere recuperati utilizzandoStatement.html#getGeneratedKeys().
  • Le query multiple in un'unica esecuzione sono abilitate. Questo non è un problema perPreparedStatement, ma se l'applicazione utilizza Statement può essere un degrado della sicurezza (SQL injection).

* MariaDB e MySQL hanno implementato questi 3 comportamenti, Drizzle solo il protocollo Text.

RISULTATI DI RIFERIMENTO

Risultati del driver MariaDB

QUERY SELEZIONA SINGOLA

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

Quando il risultato PREPARE per questa query esatta è già nella cache (cache hit), la query sarà più veloce (7,1% in questo esempio) rispetto all'utilizzo del protocollo di testo. A causa della richiesta aggiuntiva PREPARE e DEALLOCATE scambi, la cache miss è più lenta del 68,1%.

Questo enfatizza i vantaggi e gli inconvenienti dell'utilizzo di un protocollo binario. La cache HIT è importante.

QUERY INSERTO SINGOLO

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

I risultati di INSERT sono simili ai risultati di SELECT.

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

L'uso del protocollo binario è qui più significativo, avendo risultati il ​​13% più veloci rispetto all'utilizzo del protocollo di testo.

Gli inserti vengono inviati in blocco e i risultati vengono letti in modo asincrono (che corrisponde a optionuseBatchMultiSend). Questo permette di avere risultati lontani con prestazioni non distanti da quelle locali.

Rewrite ha prestazioni sorprendenti, ma non avrà ID di incremento automatico. Se non hai bisogno di ID immediatamente e non usi ORM, questa soluzione sarà la più veloce. Alcuni ORM consentono alla configurazione di gestire la sequenza internamente per fornire ID di incremento, ma tali sequenze non sono distribuite, quindi non funzioneranno sui cluster.

CONFRONTO CON ALTRI PILOTI

SELECT query con un risultato di riga

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 sta per "High Availability" utilizzando la configurazione Master-Slave
(l'URL di connessione è "jdbc:mysql:replication://localhost:3306,localhost:3306/testj").

Questi risultati sono dovuti a molte scelte di implementazione diverse. Ecco alcuni motivi che spiegano le differenze di orario:

  • Il driver MariaDB è ottimizzato per UTF-8, consentendo una minore creazione di array di byte, evitando la copia dell'array e il consumo di memoria.
  • Implementazione HA:i driver MariaDB e MySQL utilizzano una classe proxy dinamica java situata tra gli oggetti Statement e i socket, consentendo di aggiungere un comportamento di failover. Queste aggiunte costeranno un sovraccarico di 2 microsecondi per query (62.715 senza diventare 64.676 microsecondi).
    Nell'implementazione di MySQL, quasi tutti i metodi interni sono proxy, aggiungendo un sovraccarico per molti metodi che non hanno nulla a che fare con il failover, aggiungendo un sovraccarico totale di 50 microsecondi per ogni query.

(Drizzle non ha PREPARE, né funzionalità HA)

"Seleziona 1000 righe"

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

Quando si utilizzano molti dati, il tempo viene dedicato principalmente alla lettura dal socket e alla memorizzazione dei risultati in memoria per il loro invio al client. Se il benchmark eseguisse solo SELECT senza leggere i risultati, i tempi di esecuzione di MySQL e MariaDB sarebbero equivalenti. Poiché l'obiettivo di una query SELECT è ottenere risultati, il driver MariaDB è ottimizzato per restituire risultati (evitando la creazione di array di byte).

"Inserisci 1000 righe"

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

L'inserimento in blocco di MySQL e Drizzle è come quello di X INSERT:il driver invia 1 INSERT, attendi il risultato dell'inserimento e invia l'inserto successivo. La latenza di rete tra ogni inserimento rallenterà gli inserimenti.

Procedure di negozio

PROCEDURA CHIAMATA

//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

Le implementazioni di MySQL e MariaDB differiscono completamente. Il driver MySQL utilizzerà molte query nascoste per ottenere il risultato di output:

  • SHOW CREATE PROCEDURE testj.inoutParam per identificare i parametri IN e OUT
  • SET @com_mysql_jdbc_outparam_p1 = 1 per inviare dati secondo i parametri IN/OUT
  • CALL testj.inoutParam(@com_mysql_jdbc_outparam_p1) procedura di chiamata
  • SELECT @com_mysql_jdbc_outparam_p1 per leggere il risultato di output

L'implementazione di MariaDB è semplice utilizzando la capacità di avere il parametro OUT nella risposta del server senza ulteriori query. (Questo è il motivo principale per cui il driver MariaDB richiede MariaDB/MySQL server versione 5.5.3 o successiva).

CONCLUSIONE

Il driver MariaDB è fantastico!

Il protocollo binario ha diversi vantaggi ma si basa sull'avere i risultati PREPARE già nella cache. Se le applicazioni hanno molti tipi diversi di query e il database è distante, potrebbe non essere la soluzione migliore.

Rewrite ha risultati sorprendenti per scrivere i dati in batch

Il pilota tiene bene rispetto agli altri piloti. E c'è molto da fare, ma questa è un'altra storia.

Risultati grezzi:

  1. con un database MariaDB 10.1.17 locale, distante
  2. con un database MySQL Community Server 5.7.15 (build 5.7.15-0ubuntu0.16.04.1) locale