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:
- PREPARE – Prepara l'istruzione per l'esecuzione.
- ESEGUI – Invia parametri
- 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 OUTSET @com_mysql_jdbc_outparam_p1 = 1
per inviare dati secondo i parametri IN/OUTCALL testj.inoutParam(@com_mysql_jdbc_outparam_p1)
procedura di chiamataSELECT @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:
- con un database MariaDB 10.1.17 locale, distante
- con un database MySQL Community Server 5.7.15 (build 5.7.15-0ubuntu0.16.04.1) locale