MariaDB
 sql >> Base de données >  >> RDS >> MariaDB

Performances du pilote de connecteur Java MariaDB

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 :

  1. PREPARE – Prépare l'instruction pour l'exécution.
  2. EXÉCUTER – Envoyer les paramètres
  3. 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 OUT
  • SET @com_mysql_jdbc_outparam_p1 = 1 pour envoyer des données selon les paramètres IN / OUT
  • CALL testj.inoutParam(@com_mysql_jdbc_outparam_p1) procédure d'appel
  • SELECT @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 :

  1. avec une base de données MariaDB 10.1.17 locale, distante
  2. avec une base de données MySQL Community Server 5.7.15 (build 5.7.15-0ubuntu0.16.04.1) locale