Journal Petit bench de bases de données embarquées

Posté par  (site web personnel) .
Étiquettes : aucune
0
24
jan.
2007
Bonjour à tous,

Dans le cadre de mon projet, une problématique de contrôle de doublons, afin d'éviter
de faire plusieurs fois le travail sur un même fichier.
Ce contrôle est placé dans un composant qui doit avoir une bonne réactivité,
doit être léger et robuste ; donc avec le moins de middlewares et dépendances
externes possibles. Et s'il plante, il doit n'avoir perdu aucune info : elles doivent
donc être persistées à chaque insertion/suppression. Nous avons pensé utiliser des
bases de données embarquées.
Dans la mesure où nous utilisons Java car ce composant sera utilisé sur plusieurs
OSes différents (AIX, Linux, Windows) et que c'est plus simple que porter une base
à recompiler. Les BDs embarquées que j'ai testées sont toutes écrites en Java.
J'ai testé hsql [1], h2 [2], Derby [3],McKoi [4] et SmallSql [5].

Pour avoir des points de comparaison avec les bases que nous n'utiliserons pas à cause
de cette contrainte, j'ai aussi fait mon test avec la base de données client/serveur
utilisée sur le projet, à savoir DB2 (oui je sais çapuçaipalibre, mais çaipamoikidécide),
et une autre base embarquée écrite en C, à savoir sqlite [6]. (parce que je l'aime bien).

Le test est très simple, et n'est pas du tout complet dans le cadre d'un vrai bench.
Mais dans la mesure où je fournis le code source de ce que j'ai fait [7], vous pouvez
aisément ajouter les tests qui vont intéressent, quitte à faire un bench plus varié.
Mon but était simplement de trouver, dans notre cas précis, quelle base faisait le mieux
l'affaire. Et si j'en cause ici, c'est parce que je sais que ça peut intéresser des gens,
parce que ça cause de libre, et comme ça c'est pas perdu ! :)

Passons aux choses sérieuses.

La table possède 3 colonnes, un id entier clé primaire, un digest (sorte de signature du
fichier) en varchar(256), la date d'insertion en bigint (nombre de ms depuis 1970).
Le digest pouvait tout à fait faire l'affaire de clé primaire.
J'expliquerai plus tard pourquoi j'ai laissé un id.

Le test consiste donc à exécuter une requête de type SELECT sur cette table avec comme
critère un digest. Si aucune ligne n'est retournée, alors une nouvelle est insérée, avec
un nouvel id égal au maximum de l'id courant + 1, le digest, et la date courante en ms.
C'est tout.

Pour voir comment se comportent les bases, j'ai testé sur 1000, 10000 puis 30000 fichiers
pour avoir un panel proche de nos conditions de production.
Le test est déroulé séquentiellement sur ces fichiers.

Pour simuler une demande de travail en double, de manière aléatoire mais proche d'un
pourcentage paramétrable, un fichier est pris au hasard dans la liste, sans interférer sur
le parcours séquentiel. Ainsi, si sa demande de travail a déjà été effectuée, on est bien
dans le cas d'un doublon. Si sa demande n'a pas encore été faite, elle sera refaite par
la suite, et là on aura un doublon.

Pour le fun, j'ai aussi implémenté une pseudo BD fichier, qui est en réalité une hashmap
sérialisée à chaque insertion.

Voici mes résultats sur 1000 fichiers (ce n'est pas trié)


-------------------------------- hsql ----------------------------------
46 doublons, 1000 ajoutés, nombre de boucles :1047
temps total : 1292ms
temps moyen : 1.2340019102196753ms
-------------------------------- derby ----------------------------------
66 doublons, 1000 ajoutés, nombre de boucles :1067
temps total : 5939ms
temps moyen : 5.566073102155577ms
-------------------------------- h2 ----------------------------------
56 doublons, 1000 ajoutés, nombre de boucles :1057
temps total : 2724ms
temps moyen : 2.577105014191107ms
-------------------------------- sqlite ----------------------------------
32 doublons, 1000 ajoutés, nombre de boucles :1033
temps total : 216892ms
temps moyen : 209.96321393998065ms
-------------------------------- mckoi ----------------------------------
48 doublons, 1000 ajoutés, nombre de boucles :1049
temps total : 87115ms
temps moyen : 83.04575786463299ms
-------------------------------- db2 ----------------------------------
68 doublons, 1000 ajoutés, nombre de boucles :1069
temps total : 12558ms
temps moyen : 11.747427502338635ms
-------------------------------- smallsql ----------------------------------
64 doublons, 1000 ajoutés, nombre de boucles :1065
temps total : 14611ms
temps moyen : 13.71924882629108ms
-------------------------------- hashmap ----------------------------------
59 doublons, 1000 ajoutés, nombre de boucles :1060
temps total : 5057ms
temps moyen : 4.770754716981132ms


Toutes ces bases travaillent sur le même ensemble de fichier. Si le nombre de
boucles diffère quelque peu, c'est à cause du pourcentage variable (puisqu'utilisé
dans un calcul contenant un random).

Le temps moyen n'est pas équivalent à une opération en base, mais au temps passé
dans une boucle, donc 1 select + 1 insert (le fichier n'a pas encore été traité)
ou 1 select (le fichier a été traité). Pour les bases ne gérant pas la colonne
auto increment, il y a même dans le cas où le fichier n'a pas encore été traité
un 2e select pour déterminer la valeur de l'id.
Dans mon exemple, il y a environ 5% de doublons donc environ 5% des boucles où
on ne fait qu'1 select. (ce choix est totalement arbitraire).

J'ai été assez déçu des performances de sqlite, qui pourtant est très bon en C,
j'accuse, sans aucune preuve pourtant, son driver JDBC d'être lent. Ou plutôt
l'utilisation de code C en java avec jni ou qque chose du genre qui doit être
capable de ralentir n'importe quel code.
Et très curieusement, la map se comporte très bien ! Enfin curieusement, 1000
fichier c'est pas la mer à boire non plus.

Les autres sont assez proches, à part peut être McKoi un peu en dessous.


Voici mes résultats sur 10000 fichiers


-------------------------------- hsql ----------------------------------
492 doublons, 10000 ajoutés, nombre de boucles :10493
temps total : 5168ms
temps moyen : 0.49251882207185743ms
-------------------------------- derby ----------------------------------
534 doublons, 10000 ajoutés, nombre de boucles :10535
temps total : 59436ms
temps moyen : 5.641765543426673ms
-------------------------------- h2 ----------------------------------
541 doublons, 10000 ajoutés, nombre de boucles :10542
temps total : 8752ms
temps moyen : 0.8302029975336748ms
-------------------------------- sqlite ----------------------------------
536 doublons, 10000 ajoutés, nombre de boucles :10537
temps total : 2071959ms
temps moyen : 196.63651893328273ms
-------------------------------- mckoi ----------------------------------
491 doublons, 10000 ajoutés, nombre de boucles :10492
temps total : 741476ms
temps moyen : 70.6706061761342ms
-------------------------------- db2 ----------------------------------
532 doublons, 10000 ajoutés, nombre de boucles :10533
temps total : 96139ms
temps moyen : 9.127409095224532ms
-------------------------------- smallsql ----------------------------------
515 doublons, 10000 ajoutés, nombre de boucles :10516
temps total : 1302233ms
temps moyen : 123.83349182198555ms
-------------------------------- hashmap ----------------------------------
506 doublons, 10000 ajoutés, nombre de boucles :10507
temps total : 361610ms
temps moyen : 34.41610355001428ms


Les écarts commencent à se dessiner !
Et ceci ne concerne que les temps, je n'ai pas regardé ni la taille sur le disque, ni la place
occupée en mémoire.

Voici mes résultats sur 30000 fichiers :


-------------------------------- hsql ----------------------------------
1572 doublons, 30998 ajoutés, nombre de boucles :32571
temps total : 41200ms
temps moyen : 1.2649289245033926ms
-------------------------------- derby ----------------------------------
1653 doublons, 30998 ajoutés, nombre de boucles :32652
temps total : 262448ms
temps moyen : 8.037731226264853ms
-------------------------------- h2 ----------------------------------
1663 doublons, 30998 ajoutés, nombre de boucles :32662
temps total : 58955ms
temps moyen : 1.8050027554956831ms
-------------------------------- sqlite ----------------------------------
1643 doublons, 30998 ajoutés, nombre de boucles :32642
temps total : 5890079ms
temps moyen : 180.44479504932295ms
-------------------------------- mckoi ----------------------------------
1629 doublons, 30998 ajoutés, nombre de boucles :32628
temps total : 2286808ms
temps moyen : 70.08728699276695ms
-------------------------------- db2 ----------------------------------
1636 doublons, 30998 ajoutés, nombre de boucles :32635
temps total : 306561ms
temps moyen : 9.393626474643787ms
-------------------------------- smallsql ----------------------------------
1648 doublons, 30998 ajoutés, nombre de boucles :32647
temps total : 11981409ms
temps moyen : 366.9987747725672ms
-------------------------------- hashmap ----------------------------------
1575 doublons, 30998 ajoutés, nombre de boucles :32574
temps total : 3917683ms
temps moyen : 120.27024620863266ms


Je vous laisse apprécier les résultats.

Je tiens à préciser qu'à part l'ajout d'un index sur la colonne digest (chose que
SmallSql ne gère pas), et l'utilisation quand cela était possible d'un auto increment,
je n'ai pas du tout cherché à tuner les bases. (enfin si, McKoi un peu, mais ca n'a
rien changé).

Pour en revenir à la colonne d'auto incrémentation; elle ne concerne que les tests,
la colonne digest pouvant tout à fait faire office de clé primaire.
Dans la mesure où le composant est sensé être robuste, j'ai voulu faire des tests
sur la durée. Or je n'ai pas la possibilité de laisser l'ordinateur de test allumé
24h/24 7j/7. Donc je voulais pouvoir couper mon application et la relancer plus tard
et avoir un compteur persistant.
Pour le moment, h2 et hsql (qui sont les 2 bases que j'ai retenues), ont toutes deux
dépassées plusieurs centaines de milliers d'enregistrements, avec en moyenne 30 000
lignes en même temps. (il y a une purge régulière, conformément à nos specs).

Concernant le code source que je fournis gracieusement, mais pour la gloire qd même,
je tiens à préciser qu'il s'agit d'une version allégée de celui que j'utilise en
réalité pour mon projet. Donc le calcul de digest est devenu une fonction qui renvoie
une chaîne aléatoire, de taille fixe.
Il est possible de définir plusieurs threads qui attaquent simultanément la même base,
mais mes logs deviennent inadaptées pour calculer des temps, et par les petites expériences
que j'ai menées sur ce sujet au départ, ça n'apporte rien en terme de perf. (normal
puisqu'au final il y a bien une exécution des requêtes en série.)
Il reste sûrement qques valeurs en dur, du code en commentaire, mais bon, ca devrait
être une bonne base pour qqu'un qui souhaiterait approfondir un peu.

Dernière chose, j'ai placé le niveau d'alerte des logs à FATAL volontairement car
les temps se mesurant en ms, leur incidence est relativement importante. Essayez
par curiosité!

Donc si qqu'un à le courage ou l'envie ou les deux d'agrémenter ces tests, je serai
curieux de connaître ses résultats.

Merci de m'avoir lu !

[1] http://hsqldb.sourceforge.net/ Licence maison ? mais apparemment libre
[2] http://www.h2database.com/ Licence MPL
[3] http://incubator.apache.org/derby Licence Apache Version 2.0
[4] http://mckoi.com/database/ Licence GPL
[5] http://www.smallsql.de/ Licence LGPL
[6] http://sqlite.org Domaine public !!
[7] http://yawks.free.fr/testdb.zip
Il s'agit d'une archive d'un projet Eclipse, il doit pouvoir être importé en tant
que tel, mais je n'ai pas testé. Tous les jars nécessaires sont inclus, à l'exception
du driver jdbc de DB2 pour lequel il doit y avoir une licence proprio, donc par
précaution je ne l'ai pas inclus.
  • # Fichiers

    Posté par  (site web personnel) . Évalué à 1.

    Hey,

    On pourrait avoir les fichiers de données (dans c:/temp/20070122 ;) ) pour lancer les tests nous mêmes ?

    Merci,
    • [^] # Re: Fichiers

      Posté par  (site web personnel) . Évalué à 1.

      en fait, comme le calcul du digest est bidon, tu n'as même pas besoin du parcours de la liste des fichiers.

      J'ai laissé ça pour l'exemple; et puis un calcul de digest sur un fichier, quel que ce soit le fichier, ca reste un digest ! :)
  • # SQLite est peut-être en C++, mais a un driver JDBC

    Posté par  . Évalué à 1.

    Il y a même un driver 100% java, donc sans une seule ligne de code C.
    (mais il est moins performant)

    http://freshmeat.net/projects/sqlitejdbc/
    • [^] # Re: SQLite est peut-être en C++, mais a un driver JDBC

      Posté par  . Évalué à 1.

      Oups, j'ai mal lu l'article (faut dire, il est tellement long)...

      Il y a deux facon d'utiliser le driver JDBC de sqlite:
      1) utiliser le driver JDBC 100% full java
      2) utiliser une interface JAVA + JNI + librairie sqlite en C.

      La deuxième solution est environ 5 fois plus rapide.
      • [^] # Re: SQLite est peut-être en C++, mais a un driver JDBC

        Posté par  (site web personnel) . Évalué à 1.

        Oups, j'ai mal lu l'article (faut dire, il est tellement long)...


        :-/ désolé, je voulais être complet sans trop en dire pourtant...

        Pour Sqlite, j'ai voulu tester les 2 méthodes, mais seulement l'une des deux à fonctionné. J'avoue ne pas avoir trop insisté puisque de toutes façons je n'aurai pas retenu cette solution, dans la mesure où elle n'était pas 100% java.

        L'interface java + jni + librairie sqlite en C c'est une interface particulière, pas JDBC, on est bien d'accord ?
        • [^] # Re: SQLite est peut-être en C++, mais a un driver JDBC

          Posté par  (site web personnel) . Évalué à 3.

          Sinon tu as pensé à tester db4o ? Ok, c'est pas vraiment SQL, mais justement, ca serait pertinent de voir si y'a des grosses différences de performance, le but étant bien entendu de stocker le même genre d'information.
          • [^] # Re: SQLite est peut-être en C++, mais a un driver JDBC

            Posté par  . Évalué à 2.

            Ca a l'air sympa, db4o. Je ne connaissait pas. Merci :)

            Est-ce qu'il existe le même genre de truc, mais pas limité à Java et .NET ?
          • [^] # Re: SQLite est peut-être en C++, mais a un driver JDBC

            Posté par  (site web personnel) . Évalué à 1.

            non, effectivement je n'y avais pas pensé;
            mais tu sais un peu comment ça s'utilise ? Visiblement il faut s'inscrire pour accèder aux forums...
            Je te demande parce que voilà les résultats :

            -------------------------------- db40 ----------------------------------
            17 doublons, 200 ajoutés, nombre de boucles :218
            temps total : 7140ms
            temps moyen : 32.75229357798165ms


            et c'est pas top top...

            et voilà le code source (j'ai pas accès au ftp d'où je suis pour mettre à jour l'archive)


            package dup.dbengines;

            import java.sql.Connection;
            import java.sql.SQLException;
            import java.util.Date;

            import com.db4o.Db4o;
            import com.db4o.ObjectContainer;
            import com.db4o.ObjectSet;

            import dup.CheckDup;

            public class Db4oCheckDup extends CheckDup {

            class Dup {
            private byte[] digest ;
            private Long date_insert ;

            public Dup(byte [] digest) {
            this.digest = digest ;
            this.date_insert = null ;
            }

            public Dup(byte [] digest,long date_insert) {
            this.digest = digest ;
            this.date_insert = new Long(date_insert) ;
            }

            public long getDatedate_insert() {
            return date_insert!=null?date_insert.longValue():0 ;
            }
            public byte[] getDigest() {
            return digest;
            }

            }

            public static final String DB_ENGINE_NAME = "db40" ;

            private ObjectContainer db=Db4o.openFile(DB_ENGINE_NAME);

            public Connection openConnection() throws ClassNotFoundException,
            SQLException {
            return null;
            }

            public void shutdown() throws SQLException {
            }

            public synchronized void beginTransaction() throws SQLException {

            }

            public synchronized void commit() throws SQLException {
            db.commit() ;
            }

            public synchronized void rollback() throws SQLException {
            db.rollback() ;
            }

            public void closeConnection() throws SQLException {
            db.close() ;
            }

            public synchronized boolean isDup(byte[] digest) throws SQLException {
            Dup d = new Dup(digest) ;
            ObjectSet result=db.get(d);
            return result.size() > 0 ;

            }

            public synchronized void add(byte[] digest) throws SQLException {
            Dup d = new Dup(digest, new Date().getTime()) ;
            db.set(d) ;
            }

            public synchronized void createTableDup() throws SQLException {
            }

            public synchronized void dropTableDup() throws SQLException {
            }

            public synchronized void purge() throws SQLException {
            }


            }
        • [^] # Re: SQLite est peut-être en C++, mais a un driver JDBC

          Posté par  . Évalué à 1.

          La plus simple à utiliser est celle qui utilise le driver 100% java. Il n'y a rien à faire, juste à avoir le jar dans le classpath.

          L'autre nécessite une compilation, des arguments sur la ligne de commande de java, ... enfin, plein de bazar.

          Donc, je suppose que tu a utilisé le driver 100% java.
          Donc, tu as probablement utililisé la solution la plus lente, ce qui explique en partie tes résultats. :)
  • # Provider db4o

    Posté par  (site web personnel) . Évalué à 2.

    Re-Salut,

    J'ai implémenté un "provider" db4o pour tes tests.
    C'est pas très propre à cause de l'architecture du framework de test, mais ça a des performances sympathiques, et surtout, pas de SQL!

    Le code est ici: http://evain.net/public/Db4oCheckDup.html

    Jb
    • [^] # Re: Provider db4o

      Posté par  (site web personnel) . Évalué à 1.

      effectivement on est obligé de redéfinir toutes les méthodes avec un contenu vide, mais au départ, je ne pensais tester que des bases SQL.

      ceci dit, il faut reconnaitre que ça fonctionne pas mal.

      merci!
  • # sqlite

    Posté par  . Évalué à 1.

    -------------------------------- sqlite ----------------------------------
    32 doublons, 1000 ajoutés, nombre de boucles :1033
    temps total : 216892ms
    temps moyen : 209.96321393998065ms


    Je suis étonné par ce résultat...
    dis moi, tu n'aurais pas oublié de faire un "BEGIN;" et un "COMMIT;" à la fin de ton bloc de requète ?
    Perso, en terme de performance, je suis passé de 5 min à 2 secondes pour 10.000 insertions.
    • [^] # Re: sqlite

      Posté par  . Évalué à 2.

      Et s'il plante, il doit n'avoir perdu aucune info : elles doivent
      donc être persistées à chaque insertion/suppression.
      • [^] # Re: sqlite

        Posté par  (site web personnel) . Évalué à 3.

        Ben alors c'est complètement inutile de tester sqlite...

        Vu que a chaque écriture il va locker la table + écriture atomique + unlock de la table...

        Une petite question stupide, mais ne serait-il pas plus simple de mettre en place ton BEGIN+COMMIT et en cas d'erreur de refaire transaction par transaction jusqu'à celle qui pose problème.
        Mes vagues souvenirs de SQL semblent me mentionner que si ça foire lors d'une transaction tu pourrit pas la db et elle est comme avant la transaction.
        • [^] # Re: sqlite

          Posté par  (site web personnel) . Évalué à 1.

          Je ne comprends pas ce que tu veux dire.

          Quelle que soit la technique utilisée par la base pour pouvoir revenir à un état cohérent en cas de plantage (journaux avant/ après ...), il faut bien écrire qque chose sur le disque à un moment donnée.

          Or mon problème n'est pas de savoir si ma table reste cohérente en cas de plantage, pour ça je fais confiance au SGBD, ce que je souhaite c'est surtout être sûr que si une demande de travail est passée, elle a été stockée et persistée sur le disque dur.

          Et pour ça, je doute que le système des transactions m'apportent réellement qque chose, même si c'est ce que j'ai codé. Et le problème existe pour sqlite mais pour toutes les autres bases. (excepté DB2 puisqu'elle est distante, et pas dans la JVM de l'appli).
          • [^] # Re: sqlite

            Posté par  . Évalué à 2.

            ce que je souhaite c'est surtout être sûr que si une demande de travail est passée
            Si tu entend par 'être passée' : 'que la sgbd m'a rendu la main en me disant , oki c'est bon' -> c'est bien le role d'un sgbd.
            Si c'est juste que tu as envoyé tes données, et que le sgbd ne t'as encore rien dis, alors la je pense pas que ce soit possible.
            • [^] # Re: sqlite

              Posté par  (site web personnel) . Évalué à 1.

              Je suis entièrement d'accord avec toi lorsqu'il s'agit d'un SGBD classique. C'est à dire, lorsqu'on passe par le réseau.
              Mais dans mon cas, le SGBD fait partie intégrante du processus de l'application. Donc si processus plante, rien ne me garantit que si ma transaction a été comittée, elle a été effectivement écrite sur le disque dur. Le commentaire (anglais !!!?) https://linuxfr.org/comments/798151.html#798151 me laisse même songeur quant aux délais, et laisse présager d'un sacré boulot de réglage si finalement nous optons pour la solution d'une BD embarquée.


              C'est pour ca que je disais en réponse à Raphaël que la problématique était la même pour toutes les BDs embarquées dans le cadre de mon test.
  • # Suggestions

    Posté par  . Évalué à 4.

    Hi,

    Thanks for sharing the benchmark results! I have some suggestions to improve the test:

    The test should keep and re-use the prepared statements, this would improve the performance for all databases.

    The test is only run once; that's OK if you run it for a long time. If not, I suggest to execute it twice (or to measure the time a little bit after starting it), to given the JVM a chance to compile the classes (JIT).

    To have 'fair' results, the same data should be used for all databases. Using a seed value in the random number generation solves this problem: Random r = new Random(0); and then use r to get 'repeatable' results. Or use r.setSeed to reset the random number generator.

    For HSQLDB, the default mode is used, which is in-memory. That means the table may not fit in the memory if it grows larger (OutOfMemoryException). An easy solution is to use the following database URL: jdbc:hsqldb:test;hsqldb.default_table_type=cached

    I hope this helps,
    Thomas
    • [^] # Re: Suggestions

      Posté par  (site web personnel) . Évalué à 1.

      Thanks for your suggestions.

      I didn't know that using PreparedStatement could improve performance.
      I'll try it !

      About HSQLDB, do you know if in the in-memory HSQLDB writes the table on the FS after each commit ? If not, this is clearly not the mode I should use for my purposes...

      Thanks!
      • [^] # Re: Suggestions

        Posté par  . Évalué à 3.

        In HSQLDB, the data is persisted (to a log file) even in this case. The problem is, the amount of data in the tables is limited by the memory. That means at some point you will run out of memory when using the default mode. When using HSQLDB, opening the database becomes slower and slower the more data is in the database.

        In HSQLDB, the log file is written up to 20 seconds after the commit, see also: http://hsqldb.org/doc/guide/ch09.html#set_write_delay-sectio(...)
        Other databases try to write the log file before the commit returns. I say 'try', because this is not actually working unless you have special hard drives and/or BIOS settings. I have tested this using a 'power off' test. For details, see also: http://www.h2database.com/html/advanced.html#acid
        The H2 database uses a default delay of up to 1 second (this can be changed).

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.