mardi 26 octobre 2010

Un Iterateur pour colonnes

Afin d'illustrer le développement que l'on peut faire sous Cassandra je vous propose de découvrir le code d'un itérateur pour Cassandra. Ce genre d'itérateurs est particulièrement utile pour des Column Family triées par TimeUUID avec un grand nombre de colonnes pour chaque clef.

Le constructeur prend 3 arguments :
La ColumnFamily
La clef qui vous intéresse
Le nombre de colonnes à récupérer maximum par itération.

Quelques explications avant le code :

Ce boût de code fonctionne avec Cassandra 0.6.X, sûrement avec Cassandra 0.7 (je n'ai pas encore testé) et utilise hector.
La méthode get(long start, long end) permet de renvoyer les colonnes entre "start" et "end".
La méthode next() renvoit au maximum le nombre de colonnes à renvoyer. La connexion à la base de données se fait uniquement dans cette méthode, comme cela la connexion reste ouverte très peu de temps.
Les constructeurs permettent de définir la columnFamily la key ainsi que le nombre de colonnes à récupérer à chaque itération. Ils sont appelés par l'intermédiaire d'une fonction statique getColIterator( ).


Attention les classes ne fonctionneront pas de suite si vous faites un copier/coller ( il faudra faire quelques modifications notamment dans le nom des packages) et le code n'est pas optimal, je le changerai dans les jours à venir mais c'est fonctionnel (Il y a un bug mais il est possible que cela vienne de Cassandra)


Classe ColumnIterator

package monpackage;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import monpackage.Connector;
import me.prettyprint.cassandra.service.Keyspace;
import me.prettyprint.cassandra.service.PoolExhaustedException;
import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.ColumnParent;
import org.apache.cassandra.thrift.NotFoundException;
import org.apache.cassandra.thrift.SlicePredicate;
import org.apache.cassandra.thrift.SliceRange;
import org.apache.thrift.TException;

/**
 *
 * @author victork
 */
public class ColumnIterator {

    private String columnFamily;
    private String key;
    private int nb_colToRetrieve;

    private long index;

    private byte[] startCol;
    private byte[] endCol;

    /**
     * Constructor
     * @param CF Column Family
     * @param rel_key
     * @param maxCol number of column that is going to be retrieved at each iteration.
     */

    private ColumnIterator(String CF, String rel_key,int maxCol){
        columnFamily=CF;
        key=rel_key;
        nb_colToRetrieve=maxCol;
        index=0l;
        startCol=new byte[]{};
        endCol=new byte[]{};
    }

    /**
     *
     * @param CF
     * @param rel_key
     * @return
     */

    public static ColumnIterator getColIterator(String CF, String rel_key){
        return new ColumnIterator(CF, rel_key,500);
    }

    /**
     *
     * @param CF
     * @param rel_key
     * @param maxCol
     * @return
     */

    public static ColumnIterator getColIterator(String CF, String rel_key, int maxCol){
        return new ColumnIterator(CF, rel_key,maxCol);
    }

    /**
     *
     * @return
     */

    public List next(){
        Connector connector=new Connector();
        ArrayList listCol=new ArrayList();
        try {
            Keyspace ks = connector.getKeyspace();
            SlicePredicate sp = new SlicePredicate();
            SliceRange sliceR = new SliceRange();
            sliceR.setCount(nb_colToRetrieve);
            sliceR.setStart(startCol);
            sliceR.setFinish(endCol);
            sliceR.setReversed(true);
            sp.setSlice_range(sliceR);
            ColumnParent cp = new ColumnParent();
            cp.setColumn_family(columnFamily);
            listCol = (ArrayList) ks.getSlice(key, cp, sp);
            startCol = listCol.get(listCol.size() - 1).getName();
            index = index + listCol.size();
        } catch (IllegalArgumentException ex) {
            Logger.getLogger(ColumnIterator.class.getName()).log(Level.SEVERE, null, ex);
        } catch (NotFoundException ex) {
            Logger.getLogger(ColumnIterator.class.getName()).log(Level.SEVERE, null, ex);
        } catch (TException ex) {
            Logger.getLogger(ColumnIterator.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalStateException ex) {
            Logger.getLogger(ColumnIterator.class.getName()).log(Level.SEVERE, null, ex);
        } catch (PoolExhaustedException ex) {
            Logger.getLogger(ColumnIterator.class.getName()).log(Level.SEVERE, null, ex);
        } catch (Exception ex) {
            Logger.getLogger(ColumnIterator.class.getName()).log(Level.SEVERE, null, ex);
        }finally{
            Connector.releaseClient(connector);
        }
        return listCol;
    }

    /**
     *
     * @param start
     * @param end
     * @return
     */

    public List get(long start, long end) {
        ArrayList result = new ArrayList();
        while (index + nb_colToRetrieve <= start ) {
            if(next().isEmpty())
                break;
        }
        while (index < end) {
            ArrayList temp = (ArrayList) next();
            if (temp.isEmpty()) {
                break;
            } else {
                int begin_index = 0;
                int end_index = temp.size();
                int reduced_index= (int) (index - temp.size());
                begin_index = Math.max((int) (start - reduced_index),0);
                end_index = Math.min( (int) ( end -reduced_index ),temp.size());
                result.addAll(temp.subList(begin_index, end_index));
            }
        }
        return result;
    }

}


Class Connector :


package monpackage;

import java.util.logging.Level;
import java.util.logging.Logger;
import me.prettyprint.cassandra.service.CassandraClient;
import me.prettyprint.cassandra.service.CassandraClientPool;
import me.prettyprint.cassandra.service.CassandraClientPoolFactory;
import me.prettyprint.cassandra.service.Keyspace;
import me.prettyprint.cassandra.service.PoolExhaustedException;
import org.apache.cassandra.thrift.NotFoundException;
import org.apache.thrift.TException;

/**
 *
 * @author victork
 */
public class Connector {

    private static final int CASSANDRA_PORT = 9160;
    private static final String CASSANDRA_KEYSPACE = "MonKeyspace";
    private static final String CASSANDRA_HOST = "localhost";
    private CassandraClientPool pool;
    private CassandraClient client;

    /**
     *
     * @return a client borrowed. Do not forget to release it afterwards
     * @throws IllegalStateException
     * @throws PoolExhaustedException
     * @throws Exception
     */

    private CassandraClient getClient() throws IllegalStateException,
            PoolExhaustedException, Exception{
        pool= CassandraClientPoolFactory.getInstance().get();
        return pool.borrowClient(CASSANDRA_HOST, CASSANDRA_PORT);
    }

    /**
     *
     * @param client
     * @return a keyspace
     * @throws IllegalArgumentException
     * @throws NotFoundException
     * @throws TException
     */

    public Keyspace getKeyspace() throws IllegalArgumentException,
            NotFoundException, TException, IllegalStateException, PoolExhaustedException, Exception{
        client=getClient();
        return client.getKeyspace(CASSANDRA_KEYSPACE);
    }

    /**
     * Release the client borrowed
     * @param connector
     * @throws Exception
     */

    public static void releaseClient(Connector connector) {

        if (connector != null) {
            try {
                connector.pool.releaseClient(connector.client);
            } catch (Exception ex) {
                Logger.getLogger(Connector.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

}

N'hésitez pas à me poser des questions.
A bientôt.

jeudi 21 octobre 2010

Cassandra 0.6.6 de sortie

Bonjour tout le monde,

J'écris ce court billet pour vous annoncer la sortie de Cassandra 0.6.6, il me semble que c'est la dernière version avant la 0.7.0 !
Le téléchargement est ici.
Elle inclut quelques corrections de bugs mais peu de vraies nouveautés, qui devraient être mises dans la journée

A bientôt,

lundi 18 octobre 2010

Cassandra - Ecriture des données

Edit : En raison d'une erreur je me permets d'editer ce qui a été fait. Veuillez m'excuser pour cette erreur, j'ai écrit l'article hier soir tard. Les MemTables sont bien évidemment, comme leur nom l'indique, en mémoire. Toutes les sections de l'article sont concernées par les changements.

Je pense qu’il est particulièrement important de comprendre ce qui se passe dans Cassandra, surtout si l’on souhaite comprendre d’où viennent les problèmes.
Deux notion importantes que nous allons essayer de comprendre aujourd’hui sont le « commitlog » et les  « Memtables » et enfin les SSTable.
Tout d’abord je vous invite à regarder cette vidéo si vous en avez le temps : http://riptano.blip.tv/file/4012133/ . Cette vidéo explique comment résoudre les principaux problèmes que l’on peut rencontrer en utilisant Cassandra. C’est assez difficile à comprendre et cela présente l’outil « nodetool », outil auquel je consacrerai mon prochain billet.

1) Le CommitLog

Le CommitLog est la première barrière que rencontrent les insertions lorsqu’elles sont passées à Cassandra. Au lieu d’insérer directement les données ce qui pourrait se révéler coûteux dans les cas de bases de données très utilisées, Cassandra écrit toutes ces opérations dans le CommitLog qui se trouve sur le disque dur.
L’écriture dans cette structure de données est très rapide.
Lorsque vous démarrez Cassandra si vous regardez ce qui s’affiche et que vous avez déjà lancé auparavant cassandra vous verrez la ligne suivante :
INFO HH : mm ss,xxx Replaying $CASSANDRAHOME/commitlog/CommitLog-XXXXXXXXXX.log
Cette ligne indique que Cassandra reconstruit l’ensemble des operations qui lui ont été passées afin de reconstruire les précieuses MemTable.

2) MemTable

Les Memtables sont construites en même temps que le CommitLog et résident en mémoires.
Lors d'une insertion les MemTables se chargent d'organiser les différents writes en mémoire (notamment en effectuant un tri des colonnes), c'est la raison de la rapidité des insertions sur Cassandra, puisque dans les bases de données la plupart du temps nécessaire est consacré à la recherche sur le disque dur puis à l'insertion à la place correcte.
L'opération de Flushing peut se révéler assez coûteuse  il faut faire attention à effectuer les réglages nécessaires dans le storage-conf.xml

3) SSTable

Les SSTables sont la forme sur le disque dur des données sous Cassandra Cassandra. Les SSTables sont découpées par ColumnFamily et par Row/Key. 
Une fois qu'une MemTable a atteint une certaine taille en mémoire ou qu'elle a résidé pendant trop longtemps en mémoire Cassandra estime qu'il est temps de sauvegarder les données sur le disque dur. Cette opération a pour nom le Flushing des MemTables. Pendant cette opération un fichier est créé sur le disque dur contenu l'ensemble des données de la MemTable tandis qu'une nouvelle MemTable est créée.
Plusieurs SSTables peuvent résider sur le disque dur de façon concurrent pour une même ColumnFamily et Row.


Une fois un nombre donné de SSTables écrites sur le disque dur (4 par défaut dansa le storage-conf.xml) Cassandra effectue une compaction (compression) des données. Toutes les opérations des 4 SSTable sont comparées et seules les plus nouvelles sont gardées, puis ensuite écrites sur le disque dur dans une SSTable finale. L'opération a de nouveau lieu lorsque le nombre de SSTable retrouve sa valeur maximale. Cette opération peut être assez couteuse, il faut donc bien la déclencher en fonction des besoins.

4)Illustration 

Pour illustrer tout ce qu’on vient de voir voici un exemple de ce qui arrive, en très simplifié. Les concepts restent les mêmes.
Nous avons l’école Ecole1 qui insère les notes d’espagnol dans une Row/Key « Espagnol ». La ColumnFamily trie par UTF8-Type donc on insère en tant que nom de colonne le nom de l’élève et en tant que value sa note.
On va supposer que le prof qui insère les notes est lent et ne sait pas se servir de son clavier, il se trompe souvent et à besoin de renvoyer la note de ses élèves. La configuration de Cassandra est la suivante le Flushing est régulier et est basé sur le temps : il a lieu toutes les 10 minutes, on fixe le nombre de SSTables maximal à 3.
Voici les opérations qui vont être faites :

10h10 Insère Pierre : 7
10h12 Insère Marie : 8
[Flushing de la MemTable]
10h21 Insère Pierre : 9
10h25 Insère Jean : 10
[Flushing de la MemTable]
10h32 Insère Pierre : 12
10h37 Insère Jean : 3
[Flushing de la MemTable]
etc...

Etudions ce qui va se passer : Entre 10h10 et 10h20 : Aucun MemTable, tout est dans le CommitLog
10h20 : SSTable1 Créée avec comme valeurs {Pierre:7} et {Marie:8}
10h30 : SSTable2 Créée avec comme valeurs {Pierre:9} et {Jean:10}
10h40 : SSTable3 Créée avec comme valeurs {Pierre:12} et {Jean:3}
10h40 : Compaction car  Cassandra détecte qu'il y a 3 MemTables, création de la SSTable1 avec comme valeurs :
{Pierre:12} {Jean:3} {Marie:8}
Et ainsi de suite !

J'espère que ce que j'ai dit vous a été utile et dans le cas où ça l'aurait été n'hésitez pas à faire partager ce blog !
Je vous remercie.

jeudi 14 octobre 2010

Message a tous

En attendant mon prochain message que je posterai sûrement demain ou après-demain (je le dédierai probablement à « nodetool » ou un article sur le fonctionnement interne de Cassandra et les MemTables), j’aimerais vous dire un petit message.

Vous êtes quelques-uns à lire ce blog et je vous en remercie, cela fait toujours plaisir d’avoir un lectorat et des personnes que l’on peut aider. J’espère d’ailleurs que tout ce que je vous dis ici est intéressant et vous apprend des choses.

Donc si vous trouvez ce blog intéressant et ce que je dis juste s’il vous plait essayer de faire connaître un peu ce blog. Je ne fais pas ça pour l’argent (je n’ai même pas de publicités) mais en améliorant mon score sur Google d’autres personnes pourront lire ce que j’écris et apprendre à leur tour ce qu’il y a à tirer de ce blog.
En passant j’aimerais lister les articles de ce blog et les faire apparaître dans un ordre intéressant pour tout le monde. Les articles seront triés par ordre de difficulté.

-> Debutant :

Cassandra - 1) Présentation

Cassandra - 2) Les clefs d'un succès

Cassandra - 3) Organisation des données

Cassandra - 4) Configuration (pas totalement a jour avec la version 0.6.5, mais je rectifierai)

Cassandra - 5) Votre première base de données

Cassandra 6) Utiliser un client haut-niveau

 

-> Intermediaire :

ColumnType ou comment ranger ses colonnes

Comprendre les notions de CL et RF

Cassandra - Options supplémentaires de configuration


-> Pour aller plus loin :

Les alternatives à Cassandra

Utiliser Cassandra avec differents languages

 

A bientot, et merci de votre aide !

mardi 12 octobre 2010

ColumnType ou comment ranger ses colonnes

Comme cela faisait quelques mois que je n'avais rien écrit sur ce blog j'avais laissé passé l'introduction de nouveaux types de données pour les Columns et SuperColumns de Cassandra.

  1. Presentation
Rappelons que dans chaque clef (key) les columns et SuperColumns sont triés selon un certain schéma qu'il faut donner au début, lors de la création de la base de données. Il existe un nombre limités de schéma. Depuis la version 0.6.1 deux nouveaux schémas ont été introduits je vais donc les passer en revue les six schémas de tri de columns utilisables :
  • BytesType : Simple tri par byte et leur poids respectif. Rien de plus à dire
  • AsciiType : Une chaîne de caractères codée en ASCII. Je vois mal pourquoi cette fonctionnalité a été rajoutée mise à part pour assurer la compatibilité avec des vieux systèmes. Je lui préfère largement le columnType "UTF8Type"
  • UTF8Type Une chaîne de caractères codée en UTF8.
  • LongType : Un entier de type long (codé sur 64 bits). Dans ce cas la column 8 viendra après la column 7 qui viendra après la column 6, qui viendra après la column 1 etc...
  • LexicalUUIDType : Un UUID trié par ordre lexical. J'ai encore du mal à voir l'intérêt, mais il y en a sûrement.
  • TimeUUIDType : Une donnée qui est codée sur 128 bits : 64 bits pour un nombre random et 64 bits pour un entier qui dénote l'heure en millisecondes à laquelle le TimeUUID a été créé. Ce ColumnType est peut être le plus utile de tous
  2.Quelques cas pratiques 

    i) Tri par date :
Ce premier cas est un cas tres courant, cela correspond a avoir les columns triées par ordre chronologique ( chronologique inversé). Dans le cas de SQL on peut par exemple insérer un Timestamp (un entier de 64 bits ) qui va correspond à la date à laquelle on a fait l'insertion. Ensuite SQL se chargerait de trier tout ça.
Dans le cas de Cassandra on peut faire la même chose. Mais comme toujours les choses ont été vues en grand et la question se pose : "Qu'arrive-t-il lorsque deux insertion se font à la même milliseconde ?". La réponse la plus simple est que le serveur va écraser une des deux données. Afin d'éviter ce genre de problème Cassandra peut utiliser un TimeUUIDType, la chance de tomber sur deux TimeUUIDType en les générant à la même seconde est de 1/(18,446,744,073,709,551,616) soit 10e-19.



    ii) Tri par rang

Cas : L'école MachinTruc utilise Cassandra pour ses élèves et elle souhaite avoir une clef "Rang" (Rappelez vous avec Cassandra les clefs sont des String) dans laquelle elle tri ses élèves par rang. On veut en une requête s'assurer qu'on obtiendra les 5 premiers élèves ou les 10 premiers.

Quel ColumnType utiliser ?
Réponse : LongType évidemment !
La structure pourrait avoir cette forme
key:"Rang"{
  Column:1{
    Value:"Pierre"}
  Column:2{
    Value:"Marie"}
  ...
}

    iii) Tri par nom noms

Cas : L'école MachinTruc désire avoir une clef qui représente une matière et une année dans laquelle elle range ses élèves par nom et dans la valeur de la columne la note qu'a eu l'élève.
On crée donc une ColumnFamily avec comme ColumnType UT8Type et on aura les données ordonnées de cette façon (après insertion bien sûr) :


key:"Biologie2010"{
  Column:"Marie"{
    Value:"20"}
  Column:"Pierre"{
    Value:"15"}
Column:"Zora"{
    Value:"16"}
  ...

}


key:"Physique2010"{
  Column:"Marie"{
    Value:"12"}
  Column:"Pierre"{
    Value:"11"}
Column:"Zora"{
    Value:"7"}
  ...

}



____________________________
Vous pouvez m'envoyer vos idées de cas d'utilisation dans les commentaires et j'essaierai de les étudier et de poster la réponse.

A bientôt !

    samedi 9 octobre 2010

    Utiliser Cassandra avec differents languages

    Ayant eu une question à propos de l'utilisation de Cassandra avec d'autres langages de programmation que JAVA je tiens à donner une réponse précise :


    Oui bien sûr il est possible d'utiliser Cassandra avec d'autres langages que JAVA. Si j'ai choisi ce langage c'est parce que je le connais bien et que la programmation avec lui me semble la plus "naturelle". Je donnerai quelques petits exemples plus tard avec Ruby très certainement.
    Cassandra est écrit en JAVA, mais pour communiquer avec le monde extérieur Thrift ( Thrift chez Apache ) est utilisé et Thrift est compatible avec les langages de programmations suivants : C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk et enfin OCaml. A priori donc, vous ne serez pas limité en terme de choix du langage par Thrift.

    Cependant dans les billets précédents j'ai utilisé un client plus haut niveau. Pour tout ceux qui se lance dans l'aventure Thrift il faut savoir que c'est assez bas niveau. Mais la communauté autour de Cassandra s'est agrandie et propose des clients haut niveau pour interagir avec Cassandra.
    Je vous invite à découvrir la liste de ces derniers en cliquant sur :
    Pour les réfractaires aux clients haut niveau voici quelques exemples de code Thrift dans différents langages, suivez ce lien.

    A bientôt,
    Victor

    mercredi 6 octobre 2010

    Comprendre les notions de CL et RF

    Le fait que Cassandra soit une base de données distribuée implique plusieurs contraintes. En effet si chacun des nodes possèdes X% des données comment s'assurer qu'on ne va pas tomber sur les (100 -X) % qu'un node ne possède pas ? Comment s'assurer aussi que les données que l'on reçoit sont correctes ?

    Cassandra possède deux mécanismes séparés pour assurer la consistence des donées et leur "avaibility" (si quelqu'un a une bonne traduction en français je remplacerai avec plaisir).

    Dans ce qui va suivre nous allons prendre l'exemple d'un parc de 3 serveurs ayant installé Cassandra.



    1) Replication Factor

    Nous possédons notre parc de 3 serveurs et nous avons configuré un keyspace. Le replication factor indique à Cassandra comment diviser le keyspace et distribuer les données. Cette vision quoique pas 100 % exacte permet de bien comprendre le principe.
    Si on fixe RF(replication factor)=1 le keyspace va être divisé en 3 et chacun des nodes recevra 33 % des données.
    De même si on fixe RF=2, on divise toujours le keyspace en 3 mais cette fois on donnera 2 morceaux à chacun des nodes.
    Enfin en fixant RF=3 chacun des nodes contiendra l'intégralité des données.


    2) Constitency level

    2.1) Constitency Level en Read

    Le niveau de consistence (abrégé CL) indique comment Cassandra va choisir une donnée valide ou invalide.
    Prenons l'exemple avec les 3 serveurs. Nous voulons trouver la valeur enregistrée dans la clef "utilisateur1" Colonne : nom
    On a la configuration suivante :

    SERVEUR1 : valeur colonne : "Marie"
    SERVEUR2 :  valeur colonne : "Julie"
    SERVEUR3 :  valeur colonne : "Marie"

    Si l'on fixe le consistency level à 1 Cassandra va considérer comme valide toute donnée qui est retournée en premier. Si SERVEUR2 répond en premier alors la valeur récupérée sera Julie. Si c'est SERVEUR1 ou SERVEUR3 la valeur sera "Marie".
    Si l'on fixe CL=QUORUM, Cassandra va atteindre qu'une majorité de serveur réponde la même valeur. Ici ce sera Marie car 2 serveurs (66% du total) vont répondre Marie.
    Enfin CL=ALL signifie que tous les serveurs doivent répondre la même chose afin que Cassandra valide une donnée. Ici ... et bien la requête échouerait !





    2.2 Consistency Level en Write


    Il existe un peu plus de possibilités pour le CL en write. Le consistency level en write indique la fiabilité avec laquelle on souhaite voir les données écrites. Un exemple vaudra mieux que cette définition.
    Nous sommes en mode Write et on insère une valeur column "Pierre" dans la clef "utilisateur1"

    Si l'on fixe CL=NONE : C'est le coup de poker. Cassandra n'attend même pas de savoir si la donnée a bien été insérée, il envoie la requête aux serveurs et clot la connexion.
    Si l'on fixe CL=ANY ou CL=ONE on attend au moins une réponse d'un des serveurs. La différence entre les deux m'échappe encore, mais je promets de corriger cette partie lorsque j'aurai compris de quoi il en retourne.
    CL=QUORUM : Ce consistency level indique au serveur cassandra qu'il attend qu'au moins une majorité de serveurs aient répondu avant de valider l'insertion.
    CL=ALL : Dans ce mode on attend les réponses de TOUS les serveurs.



    Encore une fois vous pouvez m'écrire un message ou rédiger un commentaire si je suis passé trop vite sur une notion ou si je me suis trompé.

    De retour

    Bonjour a tous et a toutes.

    Plusieurs choses :
    _Tout d'abord vous constaterez la disparition des accents et autres signes francais dans mes messages, tout ceci a une raison : je suis maintenant aux Etats-Unis. Cette longue absence est due au fait que j'ai pas mal travaille sur un projet que je vous presenterai dans les jours qui viennent.
    _Je recommencerai a faire des tutorials et a continuer a ecrire sur ce meme blog ( et je vais passer au clavier francais aussi) tres probablemement la semaine prochaine. Je vais essayer d'expliquer le principe d'Hector un client haut niveau. Puis nous verrons les fonctionnalites de la v1 avant qu'elle ne soit remplacee par la v2 d'hector qui demarrera en meme temps qu'Apache Cassandra v0.7

    Signalons au passage que Apache Cassandra est maintenant en version v0.6.5 qui est l'avant derniere release (la derniere etant la 0.6.6) avant la branche 0.7 qui change pas mal de choses.

    A bientot,
    Victor