lundi 2 août 2010

Moteur de Blog NoSQL - Parte 1 : Cassandra

Idée

L'idée est de réaliser un moteur de blog très minimaliste (création de post et gestion des tags uniquement), en se basant sur les technos NoSQL et PHP.
Le moteur doit pouvoir switcher de repository facilement.

Présentation du moteur

Le moteur de blog remplit donc les fonctionnalités suivantes :
  • Écriture d'un post
  • Ajout de tag aux posts
  • Affichage des derniers posts
  • Affichage des posts liés à un tag
C'est très insuffisant pour un vrai moteur de blog, mais bien assez pour se faire la main sur les technos ciblées.

Le moteur se base sur une architecture MVC elle aussi très minimale.
L'accès aux données se fait à travers des repositories, design pattern issu du DDD (Domain Design Development).



Cassandra


Introduction à Cassandra

Cassandra est certainement le plus populaire des NoSQL. Initié par Facebook, il est actuellement utilisés chez les plus grands du web comme Digg ou Twitter, et est supporté par la fondation apache.
Apache Cassandra est une solution issue de Dynamo d'Amazon pour les notions d'"Eventually consistent" et l'approche Master-Master des requêtes et BigTable de Google pour la modélisation "Column-oriented".

La modélisation "column-oriented"
Le modèle "column-oriented" est plus complexe à appréhender que le modèle Clef/Valeur utilisée par exemple par memcache. Dans un modèle Clef/Valeur, une valeur est identifiée uniquement par une clé et la valeur peut éventuellement être structurée (au format JSON par exemple).

Les bases de données orientées colonnes sont organisées en Column Family. Ce type de regroupement se rapproche du concept de table dans une base de données relationnelle.
Bien qu'elles soient organisées, leur disposition est totalement différente d'une table dans un modèle relationnel. Alors que les colonnes d'une base de données relationnelle sont statiques et présentes pour chaque ligne, celles d'une base de données column oriented sont dynamiques et présentes uniquement pour les lignes concernées.

Elles sont donc pensées pour accueillir un très grand nombre de colonnes (jusqu'à plusieurs millions) pour chaque ligne, ce qui permet donc de stocker facilement des relation 1..N.

Les requêtes possibles sur ces bases sont simples.
  • Requête par clé : Toutes les colonnes de la ligne dont la clef est 42
  • Requête par ensemble de clefs : Toutes les colonnes dont le nom est compris entre "a" et "b" pour la ligne ayant la clef 42
  • Intervalle de colonnes : Toutes les colonnes de la ligne dont la clef est comprise entre 42 et 99

Cette volonté de restreindre le requêtage, à permis de simplifier le design, au profit des performances.

Un peu de vocabulaire Cassandra-ien :
  • Column : Élément de base, tuple composé d’un timestamp (posé par le client), du nom de la colonne et de la valeur de la colonne.
  • SuperColumn :Globalement une structure permettant de stocker une liste dynamique de Columns.
  • ColumnFamily : un ensemble de Columns (Equivalent à une table dans le monde relationnel, sauf que les colonnes peuvent varier d’une ligne à l'autre).
  • KeySpace : Un ensemble de ColumnFamily.

Utilisation
Cassandra utilise Thrift, un framework RPC ayant des bindings pour de nombreux langages, en tant que protocole d'acces.
En ce qui concerne PHP, de nombreuses bibliothèques existe déjà, et permettent d'abstraire Thrift, Pandra est l'une d'entre elles.

Implémentation

La première donnée à stocker est donc le post en lui même, on doit donc créer une ColumnFamily pour la stocker.
La clef pour récupérer un post est son slug, une transformation "url friendly" de son titre.

Si l'on met de côté les tags, on peut donc s'en sortir avec une seule ColumnFamily :


  


Le format des données sera donc

BlogEntries : {
 my-first-post : {
  title: Yeah, my first blog post
  text: a little NoSQL stuff
  tags: nosql,tech
  slug:  my-first-post
 },
 ...
}

Voilà, Cassandra est prêt à recevoir les posts, il ne reste donc plus qu'à écrire le code (grâce à Pandra) permettant de les insérer, et les récupérer. bien sûr.

public function insertPost($_oPost) {
 // Insert post data
 $oCfPost = new PandraColumnFamily(($_oPost->slug, 'NoSQLBlog', 'BlogEntries', PandraColumnFamily::TYPE_STRING);
 $oCfPost = $this->getPostsColumnFamily($_oPost->slug);
 $oCfPost->addColumn('slug')->setValue($_oPost->slug);
 $oCfPost->addColumn('title')->setValue($_oPost->title);
 $oCfPost->addColumn('text')->setValue($_oPost->text);
 $oCfPost->addColumn('tags')->setValue(implode(',', $_oPost->tags));
 $oCfPost->save();
}

public function getPost($_sSlug) {
 $oCfPost = new PandraColumnFamily($_sSlug, 'NoSQLBlog', 'BlogEntries', PandraColumnFamily::TYPE_STRING);
 $oCfPost->load();
 $oPost = new Post();
 $oPost->slug = $oCfPost['slug'];
 $oPost->title = $oCfPost['title'];
 $oPost->text = $oCfPost['text'];
 $oPost->tags = explode(',', $oCfPost['tags']);
 return $oPost;
}

On peut donc maintenant insérer un post, et le récupérer grâce à son "url", mais on a aucun moyen de récupérer la liste des derniers posts.
Une des façons Cassandra-ienne de faire cela, est d'avoir une ColumnFamily avec une méthode de tri "TimeUUIDType". On va en profiter pour implémenter en même temps la récupération par tag. En effet, récupérer les x derniers posts du tag xxx ou tous les derniers posts est sensiblement identique. On a juste à créer un tag fictif auquel seront reliés tous les posts.


  
  


Les relations tag/post ont donc la structure suivante :
TaggedPosts : {
 __allposts__ : {
  timeuuid_1 : my-first-post
  timeuuid_2 : another-post
 }
 nosql : {
  timeuuid_1b : my-first-post
 }
 tech: {
  timeuuid_1c: my-first-post
 }
}

Pour insérer les relations post/tag, on ajoute donc, pour la ligne ayant comme clef le tag, une nouvelle colonne, dont le nom est un timestamp, et la valeur l'url du post.

Pour récupérer les derniers posts d'un tag, on récupère les x dernières colonnes (triés par UUID, donc chronologiquement) ayant pour clef le tag. On boucle sur les valeurs de colonnes (les slugs) et on récupère ensuite le contenu du post comme précédemment.

Le code pour implémenter cela :
public function insertPost($_oPost) {
 // Insert post data
 $oCfPost = new PandraColumnFamily(($_oPost->slug, 'NoSQLBlog', 'BlogEntries', PandraColumnFamily::TYPE_STRING);
 $oCfPost = $this->getPostsColumnFamily($_oPost->slug);
 $oCfPost->addColumn('slug')->setValue($_oPost->slug);
 $oCfPost->addColumn('title')->setValue($_oPost->title);
 $oCfPost->addColumn('text')->setValue($_oPost->text);
 $oCfPost->addColumn('tags')->setValue(implode(',', $_oPost->tags));
 $oCfPost->save();
 // Insert post in fake tag entry
 $oCfTagPost = new PandraColumnFamily('__allposts__', 'NoSQLBlog', 'TaggedPosts', PandraColumnFamily::TYPE_UUID);
 $oCfTagPost->addColumn(UUID::v1())->setValue($_oPost->slug);
 $oCfTagPost->save();

 // associate post with tags
 foreach ($_oPost->tags as $sTag) {
  $oCfTagPost = new PandraColumnFamily($_sSlug, 'NoSQLBlog', 'TaggedPosts', PandraColumnFamily::TYPE_UUID);
  $oCfTagPost->addColumn(UUID::v1())->setValue($_oPost->slug);
  $oCfTagPost->save();
 }
}

public function getLastPosts($_iCount = 5, $_iPage = 1) {
 return $this->getLastPostsByTag('__allposts__', $_iCount, $_iPage);
}

public function getLastPostsByTag($_sTag, $_iCount = 5, $_iPage = 1) {
 $oCfPostTags = $this->getTagsPostsColumnFamily($_sTag);
 $oCfPostTags->limit($_iCount * $_iPage)->load();
 $aPosts = array();
 // Bad way to paging, must use cassandra slices, but not the purpose
 $aPostsResult = array_splice($oCfPostTags->toArray(), ($_iPage - 1) * $_iCount,$_iCount);
 foreach ($aPostsResult as $sSlug) {
  $aPosts[] = $this->getPost($sSlug);
 }
 return $aPosts;
}

Le code est disponible sur github.

Le blog est donc très minimaliste mais fonctionnel, le but étant seulement de tester Cassandra avec un exemple simple.

La structure du code permet de changer simplement de base de données, ce sera d'ailleurs l'objet d'un prochain post, pour porter ce code vers Redis.

Aucun commentaire:

Enregistrer un commentaire