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 !