PERFORMANCES DU CONNECTEUR JAVA MARIADB
On parle toujours de performance. Mais la chose est toujours "Mesurez, ne devinez pas!".
De nombreuses améliorations de performances ont été effectuées récemment sur le connecteur Java MariaDB. Alors, quelles sont les performances actuelles du pilote ?
Permettez-moi de partager un résultat de référence de 3 pilotes jdbc permettant d'accéder à une base de données MySQL/MariaDB : DrizzleJDBC, MySQL Connector/J et MariaDB java connector.
Les versions du pilote sont la dernière version GA disponible au moment de la rédaction de ce blog :
- MariaDB 1.5.3
- MySQL 5.1.39
- Bruine 1.4
LA RÉFÉRENCE
JMH est un outil de framework de micro-benchmarking Oracle développé par Oracle, livré sous forme d'outils openJDK, qui sera la suite officielle de microbenchmarks Java 9. Son avantage distinctif par rapport aux autres frameworks est qu'il est développé par les mêmes gars d'Oracle qui implémentent le JIT (Just In Time compilation) et permet d'éviter la plupart des pièges des micro-benchmarks.
Source de l'analyse comparative : https://github.com/rusher/mariadb-java-driver-benchmark.
Les tests sont assez simples si vous êtes familier avec Java.
Exemple :
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); } } } }
Les tests utilisant les requêtes INSERT sont envoyés à un moteur BLACKHOLE avec le journal binaire désactivé, pour éviter les E/S et la dépendance aux performances de stockage. Cela permet d'avoir des résultats plus stables.
(Sans utiliser le moteur blackhole et désactiver le log binaire, les temps d'exécution varieraient jusqu'à 10%).
Les benchmarks ont été exécutés sur les bases de données MariaDB Server 10.1.17 et MySQL Community Server 5.7.13. Le document suivant montre les résultats en utilisant les 3 pilotes avec MariaDB Server 10.1.17. Pour les résultats complets, y compris ceux avec MySQL Server 5.7.13, veuillez consulter le lien au bas du document.
ENVIRONNEMENT
L'exécution (client et serveur) se fait sur un seul droplet serveur sur digitalocean.com en utilisant les paramètres suivants :
- Java(TM) SE Runtime Environment (build 1.8.0_101-b13) 64 bits (dernière version réelle lors de l'exécution de ce benchmark)
- Ubuntu 16.04 64 bits
- 512 Mo de mémoire
- 1 processeur
- base de données MariaDB "10.1.17-MariaDB", MySQL Community Server build "5.7.15-0ubuntu0.16.04.1"
en utilisant les fichiers de configuration par défaut et ces options supplémentaires :- max_allowed_packet =40M #le paquet d'échange peut aller jusqu'à 40Mo
- character-set-server =utf8 #pour utiliser UTF-8 par défaut
- collation-server =utf8_unicode_ci #pour utiliser UTF-8 par défaut
Lorsqu'il est indiqué "distant", les benchmarks sont exécutés avec un client et un serveur séparés sur 2 hôtes identiques sur le même centre de données avec un ping moyen de 0,350 ms.
Explications d'exemples de résultats
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
Cela signifie que cette simple requête prendra un temps moyen de 62,715 microsecondes en utilisant le pilote MariaDB avec une variation de ± 2,402 microsecondes pour 99,9 % des requêtes. 78,672 microsecondes en utilisant le connecteur MySQL (plus le temps d'exécution est court, mieux c'est).
Les pourcentages affichés sont définis en fonction du premier résultat mariadb comme référence (100%), permettant de comparer facilement d'autres résultats.
COMPARAISONS DES PERFORMANCES
Le benchmark testera les performances des 3 principaux comportements différents en utilisant une même base de données locale (même serveur), et une base de données distante (un autre serveur identique) sur le même centre de données avec un ping moyen de 0,450 ms
Différents comportements :
Protocole texte
Cela correspond à l'option useServerPrepStmts désactivée.
Les requêtes sont envoyées directement au serveur avec le remplacement des paramètres épurés effectué côté client.
Les données sont envoyées sous forme de texte. Exemple :Un horodatage sera envoyé comme le texte "1970-01-01 00:00:00.000500" en utilisant 26 octets
Protocole binaire
Cela correspond à l'option useServerPrepStmts enabled (implémentation par défaut sur le pilote MariaDB).
Les données sont envoyées en binaire. Un exemple d'horodatage "1970-01-01 00:00:00.000500" sera envoyé en utilisant 11 octets.
Il y a jusqu'à 3 échanges avec le serveur pour une requête :
- PREPARE – Prépare l'instruction pour l'exécution.
- EXÉCUTER – Envoyer les paramètres
- DEALLOCATE PREPARE – Publie une instruction préparée.
Consultez la documentation de préparation du serveur pour plus d'informations.
Les résultats PREPARE sont stockés dans le cache côté pilote (taille par défaut 250). Si Prepare est déjà en cache, PREPARE ne sera pas exécuté, DEALLOCATE ne sera exécuté que lorsque PREPARE n'est plus utilisé et n'est plus en cache. Cela signifie que certaines exécutions de requête auront 3 allers-retours, mais que d'autres n'auront qu'un seul aller-retour, en envoyant un identifiant et des paramètres PREPARE.
Réécrire
Cela correspond à l'option rewriteBatchedStatements enabled.
La réécriture utilise le protocole texte et ne concerne que les lots. Le pilote réécrira la requête pour des résultats plus rapides.
Exemple :
Insérer dans ab (i) les valeurs (?) avec les valeurs du premier lot [1] et [2] seront réécrites en
Insérer dans ab (i) les valeurs (1), (2).
Si la requête ne peut pas être réécrite en "multi-valeurs", la réécriture utilisera les multi-requêtes :
Insérer dans la table (col1) les valeurs (?) lors de la mise à jour de la clé en double col2=? avec les valeurs [1,2] et [2,3] seront réécrites
Insérer dans la table (col1) les valeurs (1) lors de la mise à jour de la clé en double col2=2 ;Insérer dans la table (col1) les valeurs (3) le mise à jour de clé en double col2=4
Les inconvénients de cette option sont :
- Les identifiants d'incrémentation automatique ne peuvent pas être récupérés à l'aide deStatement.html#getGeneratedKeys().
- Les requêtes multiples en une seule exécution sont activées. Ce n'est pas un problème pourPreparedStatement, mais si l'application utilise Statement, cela peut être une dégradation de la sécurité (injection SQL).
* MariaDB et MySQL ont ces 3 comportements implémentés, Drizzle uniquement le protocole texte.
RÉSULTATS DE RÉFÉRENCE
Résultats du pilote MariaDB
REQUETE DE SÉLECTION UNIQUE
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
Lorsque le résultat PREPARE pour cette requête exacte est déjà dans le cache (accès au cache), la requête sera plus rapide (7,1 % dans cet exemple) que d'utiliser le protocole texte. En raison des échanges de requêtes supplémentaires PREPARE et DEALLOCATE, le cache miss est 68,1 % plus lent.
Cela met l'accent sur les avantages et les inconvénients de l'utilisation d'un protocole binaire. Cache HIT est important.
REQUETE D'INSERTION UNIQUE
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
Les résultats des INSERT sont similaires aux résultats des SELECT.
LOT :1000 INSÉRER LA REQUÊTE
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'utilisation du protocole binaire est ici plus significative, avec des résultats 13 % plus rapides que l'utilisation du protocole texte.
Les inserts sont envoyés en bloc et les résultats lus de manière asynchrone (ce qui correspond à l'optionuseBatchMultiSend). Cela permet d'avoir des résultats distants avec des performances proches de celles locales.
La réécriture a de bonnes performances étonnantes, mais n'aura pas d'identifiants d'auto-incrémentation. Si vous n'avez pas besoin d'identifiants immédiatement et que vous n'utilisez pas ORM, cette solution sera la plus rapide. Certains ORM permettent à la configuration de gérer la séquence en interne pour fournir des identifiants d'incrémentation, mais ces séquences ne sont pas distribuées et ne fonctionneront donc pas sur les clusters.
COMPARAISON AVEC D'AUTRES PILOTES
Requête SELECT avec un résultat de ligne
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 signifie "Haute Disponibilité" en utilisant la configuration Maître-Esclave
(l'URL de connexion est "jdbc:mysql:replication://localhost:3306,localhost:3306/testj").
Ces résultats sont dus à de nombreux choix d'implémentation différents. Voici quelques raisons qui expliquent les décalages horaires :
- Le pilote MariaDB est optimisé pour UTF-8, permettant moins de création de tableau d'octets, évitant la copie de tableau et la consommation de mémoire.
- Mise en œuvre HA :les pilotes MariaDB et MySQL utilisent une classe proxy dynamique Java située entre les objets Statement et les sockets, ce qui permet d'ajouter un comportement de basculement. Ces ajouts coûteront une surcharge de 2 microsecondes par requête (62,715 sans devenir 64,676 microsecondes).
Dans l'implémentation de MySQL, presque toutes les méthodes internes sont proxy, ajoutant une surcharge pour de nombreuses méthodes qui n'ont rien à voir avec le basculement, ajoutant une surcharge totale de 50 microsecondes pour chaque requête.
(Drizzle n'a pas de fonctionnalité PREPARE, ni HA)
"Sélectionner 1 000 lignes"
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
Lorsque vous utilisez beaucoup de données, le temps est principalement consacré à la lecture du socket et au stockage du résultat en mémoire pour le renvoyer au client. Si le benchmark n'exécutait que le SELECT sans lire les résultats, le temps d'exécution de MySQL et de MariaDB serait équivalent. Le but d'une requête SELECT étant d'avoir des résultats, le driver MariaDB est optimisé pour restituer des résultats (évitant la création de tableaux d'octets).
« Insérer 1 000 lignes »
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 et Drizzle Bulk Insert sont comme X INSERT :le pilote envoie 1 INSERT, attend le résultat de l'insertion et envoie l'insertion suivante. La latence du réseau entre chaque insertion ralentira les insertions.
Procédures du magasin
APPEL DE PROCÉDURE
//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
Les implémentations MySQL et MariaDB diffèrent complètement. Le pilote MySQL utilisera de nombreuses requêtes cachées pour obtenir le résultat de sortie :
SHOW CREATE PROCEDURE testj.inoutParam
pour identifier les paramètres IN et OUTSET @com_mysql_jdbc_outparam_p1 = 1
pour envoyer des données selon les paramètres IN / OUTCALL testj.inoutParam(@com_mysql_jdbc_outparam_p1)
procédure d'appelSELECT @com_mysql_jdbc_outparam_p1
pour lire le résultat de sortie
L'implémentation de MariaDB est simple grâce à la possibilité d'avoir le paramètre OUT dans la réponse du serveur sans aucune requête supplémentaire. (C'est la principale raison pour laquelle le pilote MariaDB nécessite la version 5.5.3 ou ultérieure du serveur MariaDB/MySQL).
CONCLUSION
Le pilote MariaDB est génial !
Le protocole binaire a différents avantages mais repose sur le fait que les résultats PREPARE sont déjà en cache. Si les applications ont beaucoup de types de requêtes différents et que la base de données est distante, ce n'est peut-être pas la meilleure solution.
La réécriture a des résultats étonnants pour écrire des données par lots
Le pilote tient bien par rapport aux autres pilotes. Et il y a beaucoup à venir, mais c'est une autre histoire.
Résultats bruts :
- avec une base de données MariaDB 10.1.17 locale, distante
- avec une base de données MySQL Community Server 5.7.15 (build 5.7.15-0ubuntu0.16.04.1) locale