HBase

GNU/Linux Magazine HS n° 078 | mai 2015 | Sébastien Chazallet
  • Actuellement 0 sur 5 étoiles
  • 1
  • 2
  • 3
  • 4
  • 5
HBase est une base de données orientée colonnes conçue pour fonctionner de manière distribuée au-dessus du système de fichiers HDFS.Comme de nombreux produits issus de l'écosystème Hadoop, elle est inspirée de travaux menés par Google, en l'occurrence BigTable.

Si vous n'êtes familier qu'avec le monde des bases de données relationnelles, cet article est fait pour vous. Nous aborderons le monde NoSQL et le concept d'orientation colonnes et nous verrons la particularité de ces tables, de ces données et de leur exploitation. Cet article tentera de répondre à la question : « HBase est-il un produit fait pour mon projet ? »

1. Le monde NoSQL

Le concept du NoSQL est en quelque sorte à la mode ces derniers temps et, comme souvent, les phénomènes de mode sont relativement éphémères ou surfaits. Le NoSQL n'est absolument pas un concept nouveau, mais est bien tout simplement une redéfinition purement marketing d'un concept qui a toujours existé, un peu comme s'il s'agissait du Web 2.0 de la base de données.

Si on met de côté cet aspect marketing viral, le NoSQL représente tout ce qui permet de stocker des données structurées, de manière persistante et qui n'est pas une base de données relationnelle. Un simple fichier CSV est par conséquent du NoSQL. Le point important est que le mot NoSQL ne s'oppose pas au langage SQL, mais bien au paradigme relationnel.

Il faut admettre que la base de données relationnelle est la base de données par excellence pour structurer des données. On n'a pas vraiment fait mieux. Elles sont populaires et utilisées partout. Vraiment partout. Vous souhaitez un exemple ? Tout le monde connaît les serveurs de bases de données relationnelles, et la plupart des développeurs manient SQL tous les jours. Aussi, lorsque l'on construit une application bureautique ou mobile et que l'on souhaite stocker des données de manière persistante, au lieu d'utiliser un outil de sérialisation, la plupart des développeurs iront utiliser une solution comme SQLite.

Vous avez tous, dans vos smartphones au moins une application qui s'appuie sur SQLite pour rendre quelques données persistantes, à commencer par votre navigateur qui garde l'historique, des onglets ou encore des marques-pages en utilisant SQLite.

Le SQL étant répandu, c'est devenu avec le temps la solution à tout. Et comme on peut stocker tout ce que l'on veut, il n'est pas rare de voir des bases de données qui sont en réalité peu structurées, et contiennent en réalité des données qui seraient bien mieux exploitées avec d'autres solutions.

Si l'on colle à la définition, une base de données telle que MySQL dans sa version 3, sans contraintes et sans notion de clé étrangère est une base NoSQL. Mais c'est borderline, je l'admets. Tout le monde ne le voit pas ainsi.

Ainsi, on peut croiser quelques forks de bases de données relationnelles qui retirent des fonctionnalités de nature relationnelle pour arriver à du NoSQL. Ceci permet de bénéficier de toute l'expérience de la base de données d'origine, en particulier en termes de performances et d'améliorer ces dernières en retirant tout ce qui prend du temps. Et en effet, c'était le concept initial de MySQL : pas de contraintes, pas de clés étrangères, et de meilleures performances parce que moins d'opérations à réaliser.

Bien entendu, la destination d'une telle base n'est pas pour faire la même chose et stocker des données dont la nature même est relationnelle. Et on s'approche là du vrai sens contenu dans la notion de NoSQL.

Bien évidemment, le NoSQL ne se limite pas à cela. On distingue également, entre autres, tous les systèmes de gestion de bases de données detype clé-valeur (Redis, Memcached, BerkeleyDB, …) ou orientés documents (MongoDB, CouchDB, …), les bases de données objet (ZODB), et enfin, les bases de données orientées colonnes.

Et là, soyons clairs, ces bases de données là répondent à de tout autres problématiques. Les bases de données clé-valeur peuvent être vues comme un dictionnaire géant. Je donne une clé et j'ai en retour une valeur. Un simple get pour retrouver une valeur et un simple put pour en ajouter ou remplacer une. Ces bases sont rapides, simples et démentiellement pratiques pour tout un tas de problématiques. Ce concept est simple, performant, et peut-être plus ancien que le relationnel, mais il fait toujours ses preuves et sait se réinventer.

Les bases de données objet suivent, en simplifiant un peu, le même principe. À chaque clé correspond une valeur qui se trouve être un objet sérialisé. De plus, les attributs des objets peuvent simplement être des clés pointant vers d'autres objets sérialisés dans la même base, ce qui est aussi une façon de faire du relationnel. De manière surprenante, les performances sont généralement très bonnes.

Lorsqu'il s'agit de stocker de manière persistante et organiser des documents, que le document lui-même est le centre et le reste simplement des métadonnées, une base de données orientée document saura mieux remplir la tâche qu'une base relationnelle. Ce concept est beaucoup plus nouveau, est un des piliers du NoSQL (mais ne constitue pas son intégralité) et commence sérieusement à percer et à s'imposer.

2. Les bases orientées colonnes

2.1 Concepts

Une base de données orientée colonnes peut être vue à la manière d'une base de données clé-valeur, c'est-à-dire comme un immense dictionnaire. Les clés sont hashables, c'est-à-dire qu'elles peuvent être triées par ordre lexicographique et donc être retrouvées par dichotomie, ce qui est extrêmement rapide. Et retrouver la valeur à partir d'une clé est également très rapide.

On peut ajouter à cela que la clé est en réalité multidimensionnelle, une clé est la combinaison entre une row-key (identifiant de la ligne, assimilable à une clé primaire pour une base relationnelle), d'une famille de colonne, d'une colonne et d'un timestamp.

La question qui se pose maintenant est : « que faire de tout cela ? ». Imaginez que vous avez un site web contenant une FAQ, exemple classique s'il en est. Vous pouvez représenter une donnée selon la proposition suivante :

- metadonnées :

  - titre ;

  - auteur ;

  - date ;

- contenu :

  - question ;

  - réponse ;

  - liens :

  - documentation ;

  - bug-tracker.

Nous venons ici de déterminer une structure de données où les composantes métadonnées, contenu et liens sont les familles de colonnes et ce qu'elles contiennent sont les colonnes (contenant les données).

Ainsi, chaque ligne représentera une question/réponse et sera assimilée à un dictionnaire dont les clés pourront être le nom des colonnes. Voici un exemple de donnée assimilée à un dictionnaire :

{'faq-id' : {'metadonnées' : {'titre' : 'Exemple',

'auteur' : 'sch',

'date' : '2015-01-01'},

'contenu' : {'question' : 'Comment faire ?',

'reponse' : 'En le faisant'}

'liens' : {'documentation' : 'www.readthedoc.org'}

Il faut rajouter à cela la dimension timestamp qui permet de garder un certain nombre de versions de données et donc de revenir en arrière dans l'historique. Par défaut, on garde les trois dernières versions, ceci dit ce paramètre peut être configuré. Il est cependant très fortement conseillé de ne pas dépasser 1000 au grand maximum pour ne pas mettre en difficulté la base de données. Un nombre de 25 est déjà une quantité importante.

On peut voir aussi qu'il n'est pas obligatoire de remplir toutes les colonnes et qu'une colonne vide n'est simplement pas représentée. En effet, on ne s'embête pas à mettre une clé avec une valeur null, on ne met simplement pas de clé.

2.2 Modèle de donnée

On peut maintenant imaginer une table un peu plus complexe. Imaginez que vous vouliez gérer vos utilisateurs qui ont d'une part des données, puis qui peuvent poster des commentaires sur des posts de blog, sur des forums et sur des FAQ.

Sur une base relationnelle, vous auriez plusieurs tables pour gérer cela, ainsi que des relations un à plusieurs ou plusieurs à plusieurs. Avec une base de données orientée colonnes, vous pouvez tout représenter dans une table utilisateur.

Chaque ligne représenterait un utilisateur et on pourrait imaginer les familles de colonnes suivantes :

- métadonnées :

  - e-mail ;

  - nom ;

  - mot de passe ;

- profil :

  - thème choisi ;

  - date de dernière connexion ;

  - etc.

Dans une base de données relationnelle, on pourrait imaginer avoir besoin de deux tables, afin de séparer des données qui sont sémantiquement différentes : une table métadonnées et une table profil, ayant une relation un à un entre elles.

Dans une base de données orientée colonnes, on aurait simplement deux familles de colonnes.

Si l'on souhaite gérer les commentaires de blog, les réponses ou commentaires sur des forums, ou quoi que ce soit d'autre, il suffirait également de rajouter une famille de colonne par fonctionnalité.

Ainsi, pour chaque famille de colonne, on aurait autant de colonnes que d'entrées dans laquelle l'utilisateur a répondu :

{'user-id' : {'metadonnées' : {'email' : 'moi@mon-site.org',

'nom' : 'sch',

'mot de passe' : 'ceci est un secret'},

'profil' : {'thème choisi' : 'Rose avec des poneys',

'date de dernière connexion' : '2015-10-16'}

'blog_comments' : {'blog-id1-date' : '2015-01-01',

'blog-id1-content' : 'Est-ce vrai ?',

'blog-id2-date' : '2015-01-01',

'blog-id2-content' : 'C'est pas faux !'}

Si l'on a un esprit formaté par les bases de données relationnelles, on s'imagine tout de suite une table avec autant de colonnes que de billets de blog + de forums + … Il n'en est rien : ne pensez pas base de données, mais dictionnaire. Une colonne n'existe que si on l'a créé et enregistrement par enregistrement (ligne par ligne).

Pour être plus précis : on crée une table en lui donnant un nom et en créant les familles de colonnes que l'on nomme aussi.

Les colonnes ne sont créées que lorsque l'on écrit un enregistrement. Et donc, chaque ligne a ses propres colonnes. C'est là la magie des bases de données orientées colonnes.

Donc, résumons le modèle de données :

- la table est désignée par un nom (chaîne de caractères) ;

- la famille de colonnes est désignée par un nom (chaîne de caractères) et est créée avec la table ;

- la colonne est liée à une famille de colonne, désignée par un column-qualifier (non typé, tableau d'octets) et créée au moment où l'on crée l'enregistrement ; on peut avoir deux colonnes de même nom dans deux familles différentes ;

- la ligne représente toutes les données liées à un enregistrement de la table ; l'identifiant de la ligne (row-key) est non typé. Il s'agit d'un tableau d'octets ; tous les enregistrements ont toutes les familles de colonnes (même si vide), mais n'ont que les colonnes qui ont du contenu ;

- la cellule est l'intersection entre une colonne et une ligne (laquelle est liée à une famille de colonne), soit la combinaison entre un row-key (tableau d'octets), le nom de la famille de colonne (chaîne de caractères) et d'un column-qualifier (tableau d'octets) ;

- la version : chaque cellule peut avoir plusieurs versions de données, les versions sont identifiées par leur timestamp qui est un nombre entier de type long ; le nombre de versions est configuré au niveau de la famille de colonne et vaut 3 par défaut. Le timestamp peut être fourni par le client qui adresse la requête. Si ce n'est pas le cas, il l'est par le RegionServer.

- La donnée valide est, colonne par colonne, celle qui a le timestamps le plus élevé. On peut également demander la donnée à un timestamp particulier, auquel cas on retournera toutes les colonnes contenant des données dont le timestamps est inférieur ou égal.

3. Architecture de HBase

Avant de commencer à parler d'architecture, il faut savoir que HBase est fait pour traiter beaucoup de données et aussi avoir quelques ordres d'idées sur ce que l'on entend par « beaucoup de données ». Dans les faits, si vous n'avez que peu de données, moins de 20Ti, HBase ne sera certainement pas adapté pour vous. Il n'y a pas de limites hautes. Il est à noter que pour de grandes quantités de données de 1 à 100 Ti, Cassandra est une excellente solution (également une base de données orientée colonnes) et elle rivalisera avec HBase même avec plus de données. Ce ne sont là que des ordres d'idées.

3.1 Composants

En tant que base de données orientée colonnes, HBase suit les principes précédents ainsi que d'autres qui vont être introduits progressivement. Ne prenez pas peur, le fonctionnement des composants que l'on va citer ici sera détaillé dans le 3.2 et le 3.3 lorsque nécessaire.

Pour HBase, le point central est la notion de Region. Chaque Region gère un sous-ensemble d'une table HBase et ces sous-ensembles sont divisés selon la ligne. Une ligne se trouve donc intégralement sur un et un seul serveur (modulo le fait qu'elle soit répliquée).

Chaque Region est composée de plusieurs composants Store : un par famille de colonnes. Chacun d'entre eux est divisé en StoreFile qui gère le stockage physique dans un fichier physique File utilisant les blocs HDFS. Il est à noter que ces blocs HDFS ne sont pas modifiables. Ils doivent être écrits en une fois et ne peuvent plus être touchés à moins d'être effacés et réécrits complètement. Par conséquent, une Region contient également un composant MemStore qui est un cache mémoire gérant toutes les écritures.

Le RegionServer contient un ensemble de Region et est le point d'entrée pour l'accès aux données et leur écriture. Il dispose également d'un BlockCache (cache mémoire LRU) pour optimiser les lectures et d'un WAL (logeur de requêtes d'écriture) pour sécuriser les écritures (détaillé dans les chapitres suivants).

Il peut aussi gérer toutes les opérations de maintenance. Ces opérations sont ordonnées par le MasterServer qui s'occupe de monitorer (via ZooKeeper) et de coordonner tous les RegionServer du cluster (en particulier gérer la réplication).

ZooKeeper, en plus de suivre l'état des composants du cluster, stocke des informations importantes qui sont la table ROOT qui contient les emplacements de la table META qui elle-même contient la liste des régions et de leurs emplacements.

3.2 Lire une donnée

Lire une donnée n'est en général pas le souci principal des systèmes de gestion de base de données. Cependant, dans le cas de HBase, étant donné la quantité de données, il faut se poser la question.

Comme nous l'avons vu, on va lire la donnée par rapport à un identifiant de ligne (le row-key) et on sait qu'un enregistrement est sur une et une seule Region qui va nous indiquer directement quelle est la région à aller interroger. Pour trouver cette Region concernée, le composant ZooKeeper va interroger la table ROOT pour retrouver la table META à utiliser et c'est cette dernière qui va fournir l'emplacement de la Region.

Ensuite, le RegionServeur répond directement à une requête de type get. Dans le détail, la seconde clé donnée par la requête est la famille de colonne et cela nous permet d'obtenir le Store à utiliser. Il faut alors utiliser les deux clés restantes pour trouver ce que l'on cherche.

Il faut aussi savoir que le RegionServeur utilise un BlockCache qui est un cache LRU pour restituer les données. Ceci permet d'améliorer également les performances. Il y a un et un seul BlockCache par RegionServer.

3.3 Écrire une donnée

Pour écrire une donnée, il faut également déterminer le RegionServer à utiliser, ce qui se fait exactement de la même manière. Jusque-là, pas de soucis de performance en particulier. Là où cela se complique, c'est que les opérations d'écriture sont plus coûteuses que des opérations de lecture. Pour cela, le RegionServeur va en réalité écrire dans un composant MemStore qui se trouve dans le composant Region (attention, ce n'est pas le pendant pour l'écriture du BlockCache, ce dernier étant situé au niveau du RegionServer).

Le processus qui fait la requête d'écriture se contente donc d'écrire la donnée dans le Memstore, un cache en mémoire dédié à l'écriture et se termine en renvoyant la réponse aussitôt. C'est donc une opération extrêmement rapide.

À charge pour le Memstore de rendre cette donnée persistante en réalisant l'écriture réellement sur le disque. En réalité, c'est lorsque le Memstore atteint une taille critique que l'ensemble des données triées va être écrit en une fois dans un nouveau fichier File (via un Flush). En effet, ces fichiers File ne peuvent pas être ouverts et écrits petit à petit ou être modifiés, le système de fichiers HDFS ne le permet pas.

Lorsque le client qui a demandé une écriture reçoit la réponse comme quoi l'opération est terminée, elle ne l'est peut-être pas. Mais elle le sera assurément à un moment donné.

Le problème est : « Que se passe-t-il en cas de défaillance ? ». Si l'on s'en tenait à cela, la donnée serait purement et simplement perdue. Pour éviter cela, le RegionServeur, lorsqu'il reçoit une requête d'écriture, avant d'écrire dans le Memstore, écrit la requête elle-même dans le composant WAL (Write Ahead Log) qui se charge de l'écrire dans un fichier, ce qui est tout de même coûteux, mais pas autant que l'écriture en base.

Il y a un et un seul WAL par RegionServer et comme ce dernier dispose de plusieurs régions, le fichier WAL peut rapidement grossir et devenir coûteux à utiliser. C'est la raison pour laquelle à partir d'un certain seul, il est fermé et archivé. Un nouveau fichier le remplace alors et il sera destiné à être remplacé à son tour également.

Ce mécanisme assure à lui seul la cohérence et la durabilité des données en cas de défaillance du serveur physique. En cas de problème, le serveur ira relire ce fichier pour réaliser les opérations manquantes.

Mais le processus d'écriture ne s'arrête pas là. De manière régulière, les fichiers File d'une même région sont compactés dans un fichier plus important (compression mineure) et sur une base quotidienne, chacun de ces fichiers plus importants est compacté à nouveau en un seul gros fichier (compression majeure). Le tout permet de garantir que le cluster reste performant et bien load-balancé.

En ce qui concerne les opérations de maintenance (division ou fusion de régions), le même processus est appliqué. Enfin, en ce qui concerne les opérations de suppression, les données ne sont pas réellement supprimées. En fait, on se contente de rajouter un marqueur qui indique que la donnée a été supprimée, ce qui permet de la cacher lorsque l'on fera une requête de sélection ultérieure.

3.4 Dans quels cas utiliser HBase ?

HBase offre des performances simplement étonnantes à partir du moment où on l'utilise pour ce qu'il sait faire (comme à peu près tout outil en fait). Donc la question se résume à savoir ce qu'il sait faire ou ne pas faire.

On a déjà une première réponse. 5 nœuds minimum, 3 réplications minimum, plus de 20 Ti et une très grosse charge (requêtes par seconde). Si vous n'avez pas ce besoin, HBase n'est probablement pas la solution idéale pour vous. Le pendant de cela, c'est qu'il faut des machines qui tiennent la route, puisque HBase est un très grand consommateur de CPU et de mémoire (et aussi de disque dur, par conséquent).

Si on a suivi le principe de fonctionnement de HBase, on sait qu'il est très bon lorsqu'il s'agit de rechercher une donnée à partir d'une clé (ce pour quoi il est conçu) et également pour sélectionner des données à partir d'un ensemble de clés.

Il est aussi parfaitement adapté pour gérer des schémas variables, des schémas dont les lignes ont des colonnes drastiquement différentes les unes des autres, des schémas où la plupart des colonnes sont nulles, enfin des schémas où il est possible d'ajouter ou de supprimer des colonnes dynamiquement.

Ensuite, on peut regarder ce pour quoi HBase n'est pas optimisé. Il s'agit des applications transactionnelles et de tout ce qui se rapproche de peu ou de loin du paradigme relationnel. Il n'est pas bon non plus – pour l'instant – lorsqu'il s'agit de faire des recherches textuelles permettant de coupler HBase avec Lucene ou Solr.

Enfin, il y a plusieurs manières d'utiliser HBase, en particulier via son API REST ou via PyHBase, que nous allons regarder ensemble.

4. Apprendre par la pratique

4.1 Démarrer

Si vous installez Pydoop (à l'aide du traditionnel pip), vous disposez d'un système de fichiers HDFS, des outils nécessaires (ZooKeeper et HBase et d'interfaces Web vous permettant d'administrer le tout). Pour cela, je vous renvoie à l'article sur Hadoop.

Ensuite, il faut aussi configurer HBase, ce qui se fait dans le fichier conf/hbase-site.xml :

<?xml version="1.0"?>
<configuration>
 <property>

<name>hbase.master</name>

<value>localhost:60000</value>
 </property>

 

 <property>
 <name>hbase.rootdir</name>
 <value>hdfs://localhost:9000/hbase</value>
 </property>

 

 <property>
 <name>dfs.replication</name>

<value>1</value>

</property>

 

 <property>
 <name>hbase.regionserver.class</name>
 <value>org.apache.hadoop.hbase.ipc.IndexedRegionInterface</value>
 </property>

 

 <property>
 <name>hbase.regionserver.impl</name>
 <value>org.apache.hadoop.hbase.regionserver.tableindexed.IndexedRegionServer</value>
 </property>

</configuration>

Il faut lancer HBase en utilisant le script bin/start-hbase.sh.

Maintenant, nous pouvons nous connecter à notre serveur à l'aide d'un client en ligne :

$ hbase shell

Et tester une commande :

> status

Cette commande permet de visualiser le statut du serveur. On peut avoir plus de détails ou aussi connaître sa version :

> status 'detailed'

> version

Chose importante, on peut apprendre comment utiliser une commande en utilisant :

> help <commande>

L'interface de gestion de HBase est accessible sur http://localhost:60010 et celle des RegionServer sur http://localhost:60030.

4.2 Créer une table

Pour visualiser la liste des tables, on peut procéder ainsi :

> list

Pour créer une table, la commande est la suivante :

> create 'faq', {NAME=>'info'}, {NAME=>'content'}, {NAME=>'links'})

Le premier argument est le nom de la table, les autres sont les noms des familles de colonnes écrits avec une syntaxe particulière, en utilisant une sorte de dictionnaire qui a pour clé NAME. Pour rappel, on ne précise les colonnes qu’enregistrement par enregistrement.

Le fait que l'on utilise un dictionnaire est relatif au fait que, pour chaque famille de colonne, on peut fixer des métadonnées supplémentaires :

> create 'faq', {NAME=>'info', VERSION=>3}, {NAME=>'content', VERSION=>5}, {NAME=>'links',

IN_MEMORY=>true, CONFIGURATION => {'hbase.hstore.blockingStoreFiles' => '10'})

Le paramètre important dans cet exemple et le plus souvent utilisé est VERSION, parce que l'on peut décider pour chaque famille de colonne le nombre de versions que contiendront les cellules. On voit également que l'on peut aller assez loin en permettant de positionner des éléments qui trouvent usuellement leur place dans un fichier de configuration.

Pour voir la table nouvellement créée, on peut utiliser la commande suivante (qui affiche la liste de toutes les tables) :

> describe 'faq'

Pour savoir si la table existe, on peut utiliser ceci :

> exists 'faq'

Une table créée est alors active. Si on veut faire des modifications sur le schéma, il faut d'abord la désactiver (ce qui a pour effet de la mettre hors ligne) :

> disable 'faq'

On peut faire l'opération inverse :

> enable 'faq'

Et pour savoir si une table est désactivée ou non :

> is_enabled 'faq'

On peut supprimer une table ainsi :

> drop 'faq'

Et la modifier ainsi en rajoutant une nouvelle famille de colonnes :

> alter 'faq', {NAME=>'supplement'}

La modifier à nouveau :

> alter 'faq', {NAME=>'supplement', TTL=>1000000, BLOCKCACHE=>true}

On encore la supprimer :

> alter 'faq', {METHOD=> 'delete', NAME=>'supplement'}

Mais il est possible de modifier beaucoup d'autres choses, à commencer par des paramètres généraux :

> alter 'faq', {MAX_FILESIZE=>'10000000'}

> alter 'faq', OWNER=>'sch'

> alter 'faq', METADATA=>{'clé'=>'valeur'}

Il est également possible de vider une table :

> truncate 'faq'

Enfin, il est possible de faire des opérations de maintenance, parmi lesquelles :

> compact 'faq'

> major_compact 'faq'

> compact 'faq', 'info'

> major_compact 'faq', 'info'

> flush 'faq'

> split 'faq'

4.3 Lire une donnée

Pour lire une donnée, plusieurs options sont possibles :

> major_get 'faq', '42'

> major_get 'faq', '42', 'info', {COLUMS=>'title'}

> major_get 'faq', '42', 'info', {COLUMS=>['title','author']}

> major_get 'faq', '42', 'info', {COLUMS=>['title','author'], 'TIMESTAMP'=>1326060557894}

> major_get 'faq', '42', 'info', {COLUMS=>['title','author'], 'TIMERANGE'=>[1326060557894, 1326060559994]}

> major_get 'faq', '42', 'info', {COLUMS=>'title', 'VERSION'=>4}

On peut ainsi récupérer toute la table, une seule colonne ou plusieurs colonnes de la même famille et on peut préciser le nombre de versions à récupérer ou encore un timestamp particulier ou un intervalle de timestamp.

Il est à noter que pour récupérer les colonnes de plusieurs familles de colonnes, il faudra faire plusieurs requêtes.

De plus, le résultat montre une ligne par colonne, cette ligne montre le nom de la colonne, le timestamps et la valeur :

> major_get 'faq', '42', 'info', {COLUMS=>['title','author'], 'VERSION'=>4}

COLUMN CELL

info:title timestamp=1326060557894, value=Titre

info:author timestamp=1326060557894, value=sch

info:title timestamp=1326060557894, value=Titre 2

L'autre manière de lire les données est de réaliser un scan : on va récupérer une portion de la table :

> scan 'faq' {STARTROW=>'0'}

> scan 'faq' {STOPROW=>'100'}

> scan 'faq' {STARTROW=>'0', LIMIT=>50}

> scan 'faq' {STARTROW=>'0', STOPROW=>'100',LIMIT=>50}

> scan 'faq' {COLUMNS=>'info:title', STARTROW=>'0', STOPROW=>'100',LIMIT=>50}

La ligne de départ est inclusive, celle d'arrivée exclusive. Il est possible de fixer un nombre maximal de résultats. Ce qu'il faut bien comprendre est que la clé (ici xxx) est triée de manière lexicographique, c'est-à-dire que, si on utilise des entiers, on aura un ordre qui ressemblera à ceci : 1, 10, 12, 124, 1567, 23, 3…

Ceci peut alors prêter à confusion. En effet, dans les deux derniers des exemples précédents, l'enregistrement '42' ne sera jamais retrouvé bien que l'on cherche entre les clé '0' et '100', parce que les seules clés qui se trouvent entre '0' et '100' sont les clés commençant de la forme '0xxx' ou '1' suivi d'un caractère inférieur au '0' (comme l'espace, par exemple).

La commande scan peut permettre de retrouver l'ensemble des enregistrements d'une table (attention, cela peut faire mal) :

> scan 'faq'

4.4 Écrire un enregistrement

Pour écrire un enregistrement avec l'outil en ligne de commandes, il faut utiliser la commande put :

> put('faq', '42', {COLUMN=>''info:title'}, 'Titre'

On ne peut enregistrer qu'une valeur par requête. Si on veut enregistrer plusieurs informations, il faudra écrire plusieurs requêtes :

> put 'faq', '42', {COLUMN=>'info:author'}, 'sch'

> put 'faq', '42', {COLUMN=>'info:date'}, '2015-01-01'

> put 'faq', '42', {COLUMN=>'content:question'}, 'Comment faire ?'

> put 'faq', '42', {COLUMN=>'content:reponse'}, 'En le faisant'

> put 'faq', '42', {COLUMN=>'liens:documentation'}, 'www.readthedoc.org'

On peut également préciser le timestamps via la ligne de commandes au lieu de laisser le RegionServer faire le travail, ce qui permettra d'avoir le même timestamps sur les champs qui sont mis à jour en même temps parce qu'étant faits par des requêtes différentes, ils auraient sinon un timestamp légèrement différent :

> put 'faq', '42', {COLUMN=>'info:author'}, 'sch', 1326060557894

> put 'faq', '42', {COLUMN=>'info:date'}, '2015-01-01', 1326060557894

À chaque put, on crée une nouvelle version de la cellule ou, si elle n'existe pas, la cellule elle-même et un timestamp est associé à la version écrite.

Un autre type de modification consiste à incrémenter un compteur. Pour cela, on utilise la commande incr :

> incr 'faq', '42' {COLUMN=>'info:votes'}

Par défaut, l'incrément est de 1, ce qui peut être changé :

> incr 'faq', '42' {COLUMN=>'info:votes'}, 42

Enfin, on peut supprimer une donnée par l'utilisation de la commande suivante :

> delete 'faq', '42' {COLUMN=>'info:author'}

La signature de cette méthode est exactement identique à celle de put, les implications aussi : pour supprimer plusieurs colonnes, il faudra faire plusieurs requêtes.

5. Se simplifier la vie avec PyHBase

5.1 Débuter

Pour installer un PyHBase, il suffit de procéder ainsi :

$ pip install pyhbase

À partir de ce moment-là, vous pouvez ouvrir une console Python et importer le module, puis créer un client :

>>> from pyhbase.connection import HbaseConnection

>>> host, port = 'localhost', 9090

>>> connection = HbaseConnection(host, port)

À partir de ce moment-là, on peut utiliser les fonctionnalités d'introspection de Python pour comprendre par soi-même quelles sont les possibilités :

>>> dir(connection)

On peut aussi aller voir un exemple qui se trouve être un mini-programme permettant de réaliser des instructions directement dans la console bash : https://github.com/hammer/pyhbase/blob/master/examples/pyhbase-cli.

On peut rapidement retrouver quelques informations utiles :

>>> connection.get_hbase_version()

>>> connection.get_cluster_version()

>>> connection.list_table()

5.2 Créer une table

Pour créer une table, il suffit de procéder ainsi :

>>> connection.create_table('faq', 'info', 'content', 'links')

Le premier argument est le nom de la table, les autres sont les noms des familles de colonnes.

Pour récupérer des informations sur les tables ou sur les familles de colonnes, on peut utiliser ces quatre commandes :

>>> describe_table('faq')

>>> describe_family('faq', 'info')

>>> is_table_enabled('faq')

>>> table_exists('faq')

Enfin, il est possible de faire diverses opérations sur une table :

>>> drop('faq')

>>> truncate('faq')

>>> enable_table('faq')

>>> disable_table('faq')

>>> alter('add' 'nouvelle famille')

>>> flush('region_name')

>>> split('region_name')

5.3 Lire un enregistrement

Il existe une seule méthode permettant de lire un enregistrement : la méthode get. Cette dernière permet de récupérer une seule valeur :

>>> get('faq', '42', 'info:author')

Le premier argument est le nom de la table, le second est l'identifiant de la ligne (enregistrement) et enfin, le troisième est l'association entre le nom de la famille de colonnes et la colonne, le séparateur étant les deux-points.

On peut également demander plusieurs colonnes :

>>> get('faq', '42', 'info:author', 'info:title', 'links:documentation')

Voire l'ensemble des colonnes d'une famille de colonne (seulement les colonnes réellement utilisées dans l'enregistrement (et non supprimées) :

>>> get('faq', '42', 'info')

Enfin, on peut panacher les deux :

>>> get('faq', '42', 'info:author', 'info:title', 'links')

Et récupérer toutes les données de la table :

>>> get('faq', '42')

5.4 Écrire un enregistrement

Pour écrire un enregistrement avec PyHBase, il suffit de procéder ainsi :

>>> connection.put('faq', '42', 'info:title', 'Titre', 'info:author', 'sch', 'info:date', '2015-01-01', 'content:question', 'Comment faire ?', 'content:reponse', 'En le faisant', 'liens:documentation', 'www.readthedoc.org')

Comme pour la lecture, dans toutes les méthodes d'écriture, le premier argument est le nom de la table et le second l'identifiant de la ligne. Les clés sont fabriquées de la même manière.

Pour la méthode put, les autres arguments sont une succession de clés et de valeurs (il y a donc un nombre pair d'arguments). Il ne faut pas oublier que chaque enregistrement peut contenir autant (ou aussi peu) de colonnes que souhaité.

L'opération put est également utilisée pour réaliser les mises à jour, puisqu'elle prend également l'identifiant de la ligne comme argument :

>>> connection.put('faq', '42', 'info:title', 'Titre 2', 'info:date', '2015-02-01', 'info:votes', 12)

En ce qui concerne l'opération d'incrémentation, sa signature est la suivante :

>>> incr('faq', '42', 'info:votes', 30)

Les deux premiers arguments restent le nom de la table et l'identifiant de la ligne. Suivent le couple famille de colonnes - colonne et l'incrément (paramètre optionnel, par défaut à 1).

Enfin, on peut supprimer un enregistrement ou une partie de celui-ci selon la signature de la méthode get :

>>> delete('faq', '42')

>>> delete('faq', '42', 'info')

>>> delete('faq', '42', 'info:author', 'info:title', 'links:documentation')

>>> delete('faq', '42', 'info:author', 'info:title', 'links')

Une petite précision : pour écrire un fichier, on prendra soin de respecter les règles de Python. Par exemple :

>>> with open('image.png') as f:

... connection.put('faq', '42', 'info:logo', f.read())

Faire les choses proprement peut parfois éviter certaines mauvaises surprises !

Conclusion

SI vous ne connaissiez pas du tout le concept de l'orienté colonnes avant de lire cet article, vous avez sans doute découvert un outil extrêmement puissant et totalement complémentaire aux bases de données relationnelles, documentaires ou clé-valeur, puisque chacune a une utilisation bien différente et répond à des besoins particuliers.

HBase est un outil très complet, à l'architecture complexe, mais lui permettant de disposer de performances remarquables. Son utilisation peut s'avérer également assez complexe, mais le dernier outil que l'on a vu permettra de répondre à un certain nombre de besoins sans avoir à franchir cette marche.