Accueil Nos publications Blog Base de données et architecture distribuée

Base de données et architecture distribuée

accroche_ala_02Dans un article précédent, nous avons abordé un aspect particulier des plateformes hautes disponibilités dites HA. Il s’agissait de la répartition de charge couplée à la redondance des équipements comme moyen de renforcement de cette haute disponibilité. La disponibilité des données ainsi que la rapidité de l’accès à l’information est un autre aspect qui caractérise ces plateformes HA. La qualité du stockage ainsi que les stratégies adoptées pour cet effet sont les piliers mêmes de cette disponibilité. Cette disponibilité doit être à la fois immédiate et durable.

  • Pour répondre à l’impératif de la rapidité d’accès, le stockage en mémoire, autrement dit le cache, est le choix numéro un. Les architectures actuelles n’hésitent plus à mettre en place différents niveaux de cache pour rendre fluide cet accès à l’information.
  • Pour répondre à l’autre impératif qui est de garantir la durabilité de l’information, autrement dit sa persistance, selon les cas, les bases de données et ou les systèmes de fichiers s’avèrent incontournables.

Sans prétention particulière et toujours dans le cadre de la haute disponibilité, nous allons aborder quelques architectures adoptées par le marché, parmi les plus performantes en termes d’efficacité de stockage et de restitution de l’information.

SQL OR NoSQL

Les bases de données NoSQL connaissent un essor croissant en raison de leur mise en place et de leur extensibilité (scalability), théoriquement faciles et illimitées. Les architectures associées permettent de définir un ensemble de bases de données répliquées permettant à la fois :

  • Une montée en charge transparente vis-à-vis du client
  • Une haute disponibilité de l’information (tolérance aux pannes)
  • Une gestion efficace de gros volumes de données [terabyte, petabyte]
  • Un fort débit et donc des performances très confortables.

Les bases de données relationnelles régnaient jusque récemment sans conteste et permettaient de stocker/restituer une information typée et structurée. Mais face aux montées de charges et aux besoins toujours croissants en terme de volume d’information et de rapidité d’accès, elles ont commencé à montrer leurs limites. L’apparition d’architectures basées sur le « clustering »  et/ou le partitionnement des données a permis d’atteindre une certaine extensibilité, mais ces architectures restent compliquées en terme de mise en œuvre et d’exploitation (ou exploitabilité). Certaines “études” comparatives opposent SQL et NoSQL et finissent par conclure que l’extensibilité des bases de données relationnelles est non seulement limitée mais aussi très coûteuse en temps et en moyens. Certes les deux systèmes SQL et NoSQL permettent de stocker/restituer de l’information, mais ils ne servent pas les mêmes objectifs ce qui rend toute comparaison subjective voir non justifiée. En effet, le premier système est bâti sur le respect des propriétés ACID, le second sur la haute disponibilité et la tolérance aux pannes. Le théorème CAP (Consistency, Availability and Partition tolerance) indique qu’un système distribué ne peut pas réunir simultanément les trois critères suivants :

  • Cohérence,
  • Disponibilité
  • Tolérance aux pannes.

Si nous devions classer les deux types de bases de données, relationnelle et NoSQL, les premières seraient de type CA (Cohérence et disponibilité), les secondes seraient de types AP ou CP (disponibilité et Tolérance aux pannes |Cohérence et Tolérance aux pannes). L’extensibilité (scalability) des bases NoSQL est essentiellement facilitée par la dénormalisation des données et l’affranchissement des contraintes ACID. En termes de coût, c’est assez relatif car on ne se basera pas sur le même type de machine pour installer l’un ou l’autre. Leurs mises en place finiront de toute façon par coûter cher, car pour avoir une réplication qui supporte réellement la répartition de charge et la tolérance aux pannes, il faut disposer d’instances différentes, installées sur des machines différentes. Donc, contrairement aux idées reçues, la mise en place des deux technologies (basées sur du SQL ou sur du NoSQL) finissent par coûter cher. Ces coûts peuvent être encore plus importants lorsque les serveurs/machines utilisés sont de haute gamme. Dans le secteur du « data », la haute disponibilité est souvent obtenue par réplication, parfois également par partitionnement, voir en combinant les deux. Sans aller plus loin dans la comparaison des deux technologies, car ceci mérite un article à part entière, nous allons dans ce qui suit brosser quelques architectures garantissant une haute disponibilité de l’information. Nous aborderons en particulier, le cas de certaines bases de données connues (Cassandra, MongoDB et Oracle) et leurs recettes pour garantir la haute disponibilité.

NoSQL – CASSANDRA

Cassandra est née sous le drapeau de Facebook, en associant le meilleur de Google Big Table (modèle de données orienté colonnes + mécanisme de persistance performant) et d’Amazon DynamoDB (architecture distribuée où tous les nœuds sont équivalents). Gérée actuellement par la fondation Apache, son architecture a été pensée pour ne présenter aucun point de défaillance SPOF (Single Point Of Failure).

L’architecture Cassandra est basée sur un modèle distribué et décentralisé de type « peer-to-peer ». Tous les éléments d’une plateforme sont supposés être identiques du moins d’un point de vue client. L’ensemble des éléments ou nœuds est maintenu dans un état de service cohérent grâce à un protocole de communication interne dit « gossip ». Le but de l’architecture Cassandra est de garantir une haute disponibilité via une grande tolérance aux pannes doublée d’une extensibilité (scalabilité) facile et flexible, le tout avec une promesse de hautes performances aussi bien à l’écriture qu’à la lecture. Le schéma ci-dessous inspiré de la documentation officielle de DataStax illustre le lien linéaire entre les capacités d’une plateforme Cassandra et le nombre de nœuds associés.

  BD_HauteDispo_011

La scalabilité de la plateforme Cassandra est telle qu’il suffit d’augmenter progressivement la taille de la ferme de serveurs pour pouvoir absorber plus de charge, ceci avec l’avantage de ne rien modifier à l’architecture applicative en place. Pour remplir ce cahier des charges, Cassandra utilise différentes techniques assez sophistiquées, parmi lesquelles nous pouvons citer:

  • La communication inter nœuds
  • Les filtres « Bloom »
  • Le « read/repair »  et la compaction
  • Le partitionnement et la réplication

Les deux dernières techniques rentrent dans le cadre de la haute disponibilité. Nous les détaillerons ci-dessous.

Partitionnement

Cassandra a été pensée pour être déployée sur différentes machines. Chacune d’elles représente un nœud du cluster avec une certaine capacité d’hébergement et de traitement. Pour des raisons de haute performance, les données sont automatiquement distribuées sur l’ensemble des nœuds. L’opération de partitionnement est basée sur un système de hashage qui génère automatiquement des clés uniques de stockage. Les littératures parlent de « autosharding » et/ou de « key based sharding ». La génération automatique de clé (fonction de hash) a été pensée pour avoir à la fois une bonne gestion de la répartition des données et un support performant lors des opérations de lecture (scans par bloc). Le protocole de communication interne « gossip » participe également à cette gestion efficace. Il permet à un nœud de savoir rapidement où est située une donnée dans le cluster.

Trois logiques de partitionnement existent et peuvent être inter changées par configuration. Les dernières versions de Cassandra préconisent et utilisent par défaut un nouveau répartiteur plus performant dont les clés de stockage (64bits) permettent une plus grande capacité d’hébergement. Dans les faits, chaque nœud est associé, par configuration, à un « Range Token », définissant un intervalle de clés de stockage. Les dernières versions de Cassandra permettent d’associer un ou plusieurs « num_Token ou range_Token » en fonction des capacités d’un nœud (CPU, RAM, DISQUE). C’est cet intervalle qui permet au répartiteur de savoir exactement sur quel nœud une donnée sera stockée.

Réplication

Le partitionnement permet de répartir l’information et son traitement sur différents nœuds, mais ne garantit pas sa disponibilité à tout moment. Pour résoudre ce problème, Cassandra renforce son système par de la réplication. Cette dernière est pilotée par deux piliers : Le facteur de réplication (replication factor) et la stratégie de réplication. Le premier détermine le nombre de copies à conserver pour une donnée ; le second la manière dont sera répliquée l’information entre les nœuds du cluster. Un facteur de réplication RF = 1 est déconseillé car implique qu’une seule copie existera dans le système. Il constitue, donc, une rupture au principe de la tolérance aux pannes. Autrement dit, en cas d’indisponibilité d’un nœud, toute l’information qu’il héberge ne pourra plus être restituée. Par défaut, le facteur de réplication est fixé à 3 et doit être adapté, avec intelligence, à la taille de la ferme de serveurs. En effet, une valeur trop élevée peut pénaliser les performances car le système consacrera beaucoup de ses ressources (CPU, RAM) à la réplication et à la synchronisation des données entre nœuds.

Malgré une vitesse d’écriture très supérieure à celle de la lecture, le mécanisme de la réplication induit une certaine incohérence transitoire et cyclique lorsqu’une donnée est répliquée ou mise à jour. Ceci est dû essentiellement au fait que la réplication est un mécanisme asynchrone et dépendant de la vitesse de propagation, donc de la latence réseau. Cette latence peut même devenir gênante si la réplication inter nœuds est réalisée entre « data center » éloignés.

Pour résoudre ce problème, Cassandra – forte d’un mécanisme de synchronisation appelé « read-repair » -, permet d’agir sur le niveau de consistance (ou cohérence) d’une donnée. Elle met à disposition du client différents niveaux de consistance (Zéro, One, ALL, Any, Quorum…) qui peuvent être ajustés aussi bien en écriture qu’en lecture. Le niveau « Zéro et Any» sont les niveaux de consistance les plus faibles ; le niveau « All » est le plus strict et donc le plus impactant de point de vue performance. Ces niveaux sont donc à utiliser avec prudence. Ce n’est qu’une fois la synchronisation achevée, que la cohérence du système est atteinte. Cette cohérence ne doit donc pas être confondue avec la cohérence garantie par les règles ACID (concept propre aux bases relationnelles).

Clustering

Dans l’écosystème Cassandra, le cluster est un groupement logique de plusieurs nœuds. Il est doté d’un nom pour assurer le cloisonnement d’un ensemble de nœuds dans un environnement multi-cluster. Un cluster est lié à un « keyspace » (équivalent du schéma de base de données relationnelle) et sa taille peut varier d’un moment à l’autre. Elle peut :

  • Baisser en cas de mise hors circuit de certaines instances (maintenance, pannes…),
  • Augmenter en cas d’ajout de nouvelles instances (augmentation de la charge à supporter).

Un cluster peut être local à un « data center » ou réparti sur plusieurs. La communication inter nœuds y est possible grâce au protocole « gossip ». Ce dernier permet entre autres la détection des défaillances. En effet, l’ensemble des nœuds s’enregistre auprès d’un manager (Gossiper) responsable de la gestion de leurs états de service. Un ensemble de nœuds liés par configuration constitue une unité appelée « racks ». Dans une topologie répartie, un « rack » est en général synchronisé avec un autre « rack » jumeau situé dans un « data center » distant.

D’un point de vue client, tous les nœuds d’un cluster sont équivalents même s’il y’a une certaine différence entre les nœuds « seed » (plus stables, plus robustes) et les autres. Concrètement, lors de la réception d’une requête client, un nœud donné traitera l’opération. Potentiellement, il peut ne pas disposer de l’information demandée. Dans ce cas de figure, il agira comme un proxy de coordination entre le client et le nœud final.

NoSQL – MongoDB

MongoDB est une base de données orientée document. Les données, au format JSON, sont stockées en interne sous un format binaire plus compact appelé BSON, et groupées en collections (l’équivalent des tables SQL). Les documents sont identifiés grâce à une clé unique et peuvent être indexés pour garantir de meilleures performances.

Nous avons choisi MongoDB comme second exemple car au même titre que Cassandra, elle jouit d’une très bonne réputation et a été adoptée et éprouvée par de grands groupes. Nous l’avons choisi aussi pour illustrer un exemple de plateforme type CP (au sens du théorème CAP (Consistency, Availability and Partition tolerance)). Son architecture est différente de celle de Cassandra (de type AP), mais tout aussi performante. La base de données peut être constituée :

  • D’une seule instance: un seul processus (mongod) est alors démarré. On parle de mode serveur.
  • Deux instances: Il s’agit d’une configuration de type « maitre-esclave ».
  • De N instances: On parle de « Replicat Set » regroupant N instances synchronisées.

Les deux premiers modes sont très pratiques pour les développements et les tests (POC et autres). Ils permettent notamment de simuler des problématiques liées au réseau et à l’absence de robustesse. Le dernier mode est généralement utilisé en production en raison d’une meilleure tolérance aux pannes.

Partitionnement

L’architecture MongoDB utilise aussi le partitionnement pour améliorer les performances du système et la disponibilité de l’information. Pour cela, elle utilise la technique appelée « sharding ». Avec cette technique, la base de données devient une unité logique répartie sur N instances ou serveurs physiques. Elle permet donc d’avoir une extensibilité horizontale adaptable à la charge du système. L’implémentation MongoDB de cette technique (automatique sharding) rend la répartition totalement transparente pour le client. Elle permet aussi d’ajuster, au sens « tuning », le niveau de performance en rajoutant des répartiteurs de requêtes (Query Router). Un « shard » peut être défini comme une unité logique constituée d’un serveur unique (déconseillé) ou d’un « replicat set ». Les données sont alors réparties sur un ensemble de nœuds en se basant sur une « shardkey ».

Réplication

L’architecture MongoDB dédie le nœud maître à l’écriture et permet de répartir les opérations de lecture sur l’ensemble des nœuds. Les données sont donc cohérentes à tout moment au niveau du nœud maître. De là, elles sont répliquées sur l’ensemble des nœuds de façon asynchrone, pour garantir partout la cohérence des données. En effet, les opérations d’écriture sont toutes tracées (oplog) pour être répercutées sur l’ensemble du cluster à l’exception du nœud arbitre. La hiérarchie nœuds primaire/secondaire(s) ainsi que les opérations de synchronisations entre nœuds ont bien évidement un coût. Nous l’avons vu pour Cassandra, c’est également vrai pour MongoDB. Pour réduire l’impact de ce fonctionnement, différentes options d’écriture (secondary, secondaryPreferred, nearest…) ont été prévues et sont supportées par le Driver MongoDB.

Clustering

A la différence de l’architecture Cassandra où tous les nœuds sont supposés être équivalents, l’architecture MongoDB est basée sur une hiérarchie nœud primaire / secondaire(s). Cette hiérarchie est maintenue par un processus natif d’élection qui détermine en cas de panne du nœud maître le nœud secondaire qui lui succédera (automated failover). Un ensemble de nœuds constitue un « replicat set » et ne peut avoir qu’un seul nœud primaire. Lors de l’extension de ce dernier, une priorité est associée à chaque nœud et détermine sa position dans la hiérarchie globale. La priorité est directement prise en compte lors du processus d’élection. En effet, elle est souvent déterminée en fonction de la capacité de l’instance. Les « replicat set » sont étendus par ajout de nœud. Chaque nœud possède une priorité qui le situe dans la hiérarchie globale du cluster. Cette dernière est utilisée dans le processus natif d’élection. Elle est, en général, définie en fonction des capacités de l’instance ajoutée.

Figure 4.1 : Sharding – Schéma tiré de la documentation MongoDB.

Le schéma ci-dessus tiré de la documentation de MongoDB, montre certains composants essentiels du cluster dont les éléments clés sont :

  • L’unité de stockage dite « shard » qui correspond au « replicat set »
  • Le répartiteur de requêtes qui gère l’envoi des requêtes vers un « shard », ou assure leurs distributions sur l’ensemble des « shard » disponibles. Pour remplir cette tâche, il utilise les informations contenues dans l’unité de configuration. Il s’agit d’un ensemble de métadonnées utilisées par le répartiteur de requêtes pour gérer la répartition sur les différents « shard ».
  • La réplication se fait de façon asynchrone entre le nœud primaire et les nœuds secondaires associés.

MongoDB utilise massivement la RAM pour assurer de très hautes performances. Nous pouvons, donc, imaginer sans difficultés les capacités que peut offrir une telle plateforme. Cependant, il faut rester vigilant car toutes les données n’ont pas vocation à être montées en mémoire. Un travail de rationalisation (indexation et de catégorisation …) est, et demeurera nécessaire malgré tout.

Notons simplement que, comparé au modèle distribué adopté par Cassandra, le modèle MongoDB est parfois décrit comme étant moins tolérant aux pannes. Concrètement, lorsqu’un nœud du cluster Cassandra tombe, l’information reste disponible. Elle est restituée par un nœud détenant l’information répliquée à moins d’avoir :

  • Opté, pour une raison obscure, pour un RF=1,
  • Une répartition non réfléchie des nœuds sur l’ensemble des « data center ».

Ce niveau de tolérance aux pannes n’est pas garanti par l’architecture MongoDB. En effet, si un nœud primaire ou un « shard (cf. figure 4.1)» venait à tomber, l’information qui y est détenue pourrait ne pas être immédiatement disponible en lecture/écriture.  La continuité du service dépendra de :

  • La rapidité du processus d’élection qui basculera un nœud secondaire en primaire (de quelques millisecondes à une vingtaine de secondes),
  • La répartition des nœuds entre « data center ». Une bonne pratique est de  repartir les nœuds d’un même « shard » sur des « data center » différents comme le montre le schéma ci-dessous tiré de la documentation officielle. Ainsi, au pire, quelques requêtes d’insertion seront non honorées mais toute l’information restera disponible en lecture. Ce qui est plus qu’acceptable même par les SLA les plus strictes.

BD_HauteDispo_042

Figure 4.2 : Sharding – Failover.

Un peu plus haut dans cet article nous avons vu que, selon le théorème CAP, Cassandra est de type AP (Availibilty & Partition-Tolerance). La disponibilité de l’information à tout moment est donc capitale. Elle prime sur la consistance des données, qui n’est obtenue qu’après synchronisation.  Nous l’avons compris, cette haute disponibilité est garantie par un modèle distribué renforcé par la réplication des données. Or MongoDB est de type CP (Consistency & Partition-Tolerance), la disponibilité de toute l’information à tout moment est moins capitale. Elle est un peu sacrifiée au profit de la consistance des données (contrairement à Cassandra), consistance qui est garantie par une écriture réservée au(x) nœud(s) primaire(s). Nous n’irons pas plus loin dans la comparaison des deux plateformes. Ceci sort du cadre de cet article, et surtout nécessitera un article dédié pour aborder correctement le sujet.

BASES DE DONNES TRADITIONNELLES

Un très grand nombre d’architectures continuent à être basées sur une base de données relationnelle centrale. L’information y est typée, structurée et maintenue cohérente à tout moment.

Faces aux problématiques de la montée de charge et de la dégradation des performances, le plus souvent, ces architectures procèdent à des améliorations en trois étapes :

La première est en général, une phase d’optimisation qui touche d’une part la base donnée elle-même (ajout d’indexes, utilisation de « tablespace » différents, activation de la compression sur les tables, PCTFREE…) et d’autre part les requêtes SQL. Ces dernières sont passées à la loupe et améliorées (parallélisation, update basé sur des vues dynamiques, combinaison curseur et bulk collect…). L’ajout d’indexes et le partitionnement, pour ne citer que ceux-là, sont parmi les techniques les plus utilisées.

La seconde étape est enclenchée lorsque les effets de la première ne sont pas assez concluants. Elle passe en général par l’ajout de mémoire, l’augmentation des capacités CPU et l’utilisation de disques plus rapides. On parle alors, d’extensibilité verticale. Cette extensibilité est efficace pour une bonne majorité des plateformes mais montre très vite ses limites pour des plateformes devant répondre à des besoins croissants en termes de charge.

La troisième phase passe par l’installation d’une seconde base (rarement plus) répliquée qui peut fonctionner soit en mode actif/actif ou en mode actif/passif. Dans le mode actif/actif, les deux bases, installées sur deux machines différentes, sont synchronisées (réplication synchrone ou asynchrone) et se répartissent la charge globale. La répartition est soit assurée par un driver dédié (ex: séquoia) ou via un répartiteur de charge (ex: HaProxy). En mode actif/passif, la seconde base agit comme base de secours. Elle prend le relais pour assurer la continuité du service en cas de panne de la base maître. D’autres configurations sont possibles, et on parle souvent de bases en mode maître/esclave(s). Le nombre de bases esclaves varie selon les plateformes et la technique de réplication. Les schémas ci-dessous montrent quelques possibilités de mise en place en cluster.   BD_HauteDispo_041

Figure 5.1 : base de données en mode actif / actif

BD_HauteDispo_051

 Figure 5.2 : base de données en mode actif/actif (HaProxy/Sequoia)

Des contrôleurs comme Sequoia ainsi que des répartiteurs comme HAProxy sont capables d’assurer non seulement un rôle de répartition mais également une gestion de la tolérance à la panne. Si un des serveurs ne répond plus, il est retiré de la liste de répartition.

Conclusion

Nous avons vu dans cet article, via des architectures différentes, quelques moyens utilisés pour assurer une haute disponibilité de l’information. La même recette est mise en œuvre via des techniques différentes (Sharding, simple synchronisation ou ReadRepear…). Les maîtres mots sont réplication, partitionnement et indexation. Nous avons également vu, que rendre une base de données extensible est :

  • Possible mais peut être plus ou moins complexe (SQL, NoSQL),
  • Se heurte encore à certaines limites, et que ceci ne va pas sans un certain impact sur les performances.

Actuellement, cet impact est le prix à payer dès que le système est soumis à une certaine rigueur ou à certaines contraintes (niveau de cohérence, concurrence, aspect transactionnel…).