Etant un enthousiaste des systèmes distribués, j’ai rédigé cet article dans le but de vous offrir une vision globale de cette architecture.

Dans cet article nous allons découvrir ensemble pas à pas comment créer une architecture d’un système distribué, hautement scalable et résiliente.

C’est quoi un système distribué ?

Un système distribué dans sa définition la plus simple est un groupe d’ordinateurs travaillant ensemble pour apparaître comme un seul ordinateur à l’utilisateur final.

Vous vous demandez sûrement l’intérêt d’avoir un tel groupe d’ordinateurs ? Un seul ordinateur peut très bien faire l’affaire, mais à un moment donné vous allez avoir un très grand nombre d’utilisateurs qui se connecteront à votre serveur (ordinateur), ainsi les ressources de votre machine ne permettront plus de gérer ce nombre de connexion.

Première solution : le Vertical Scaling

La première solution qu’on peut penser à faire c’est le Vertical Scaling qui consiste à augmenter les ressources de la machine (RAM, CPU). C’est ce qu’on appelle le “scale up”. Le plus grand problème de cette approche c’est que le hardware est limité. A un moment donné on ne peut plus augmenter les ressources. Prenons l’exemple des millions d’utilisateurs qui se connectent chaque jour sur la plateforme amazon.com. Peut-on vraiment avoir une machine qui supporte ce nombre de connexion ? bien sûr que non.

Le Vertical Scaling a par contre de gros avantages tel que la cohérence des données qui est facile à implémenter.

En adoptant le Vertical Scaling on perd en disponibilité, mais par contre on gagne en cohérence. Sauf que la plupart du temps on préfère la disponibilité, sinon imaginez votre serveur d’entreprise qui ne soit pas disponible pour ces clients ? 

Peut-être vous demandez vous s’il est possible d’avoir les deux à la fois ? Et bien non, et on verra plus tard pourquoi. 

Deuxième solution : le Horizontal Scaling

La deuxième solution c’est le Horizontal Scaling. Cette approche consiste à augmenter le nombre de machines selon la charge. C’est ce qu’on appelle le “scale out”. On voit très bien les avantages que sont la scalabilité et la résilience. Les problèmes qu’on aura avec cette approche c’est qu’on aura besoin d’un Load Balancer, qu’on va voir juste après, et d’essayer de trouver une façon de gérer la cohérence.


Coté coût le Horizontal Scaling est nettement moins cher que le Vertical Scaling après un certain seuil.

Load Balancing (Répartition de charge)

Le Load Balancing désigne le processus de répartition d’un ensemble de tâches sur un ensemble de machine.

Il existe plusieurs algorithmes pour faire la répartition de charge. Parmi ces algorithmes on retrouve les algorithmes statiques et les algorithmes dynamiques.

Les algorithmes statiques

Un algorithme de répartition de charge est dit statique lorsqu’il ne prend pas en compte l’état du système dans la répartition des tâches. Un exemple d’algorithme statique est le Round Robin qui est l’algorithme d’équilibrage de charge le plus largement déployé. En utilisant cette méthode, les demandes des clients sont acheminées vers les serveurs disponibles sur une base cyclique.

Spring Zuul et Eureka par exemple offrent par défaut une implémentation du Round Robin, en ajoutant juste l’annotation @loadbalanced. J’ai donné Spring Zuul et Eureka comme exemple car au moment ou j’écris cet article je travaille sur un projet en Java / Spring Boot. 

@SpringBootApplication
public class Example {

  public static void main(String[] args) {
    SpringApplication.run(Example.class, args)
  }
  
  @LoadBalanced
  @Bean
  RestTemplate template() {
    return new RestTemplate();
  }
}

L’équilibrage de charge du serveur à tour de rôle fonctionne mieux lorsque les serveurs ont des capacités à peu près identiques.

Les algorithmes dynamiques

Contrairement aux algorithmes de répartition de charge statiques, on qualifie de dynamiques ceux dont la charge de chacun des nœuds du système est pris en compte. 

Dans cette approche, les tâches peuvent se déplacer dynamiquement d’un nœud surchargé à un nœud en sous-charge, afin d’être traitées plus rapidement. 

On trouve comme algorithme les Least connections, où l’équilibreur surveille le nombre de connexions ouvertes pour chaque serveur et envoie au serveur le moins occupé.

Cet algorithme se trouve également dans Spring Boot dans une bibliothèque crée par Netflix et qui fournit l’équilibreur de charge du côté du  Client qui est Ribbon. Et parce qu’étant membre de la famille Netflix, elle peut automatiquement interagir avec  Netflix Service Discovery (Eureka).

@SpringBootApplication
@RestController
@RibbonClient(name = "example", configuration = ExampleConfiguration.class)
public class Example {
  
  @LoadBalanced
  @Bean
  RestTemplate restTemplate() {
    return new RestTemplate();
  }
}
example:
  ribbon:
    eureka:
      enabled: false
    listOfServers: localhost: 8081, localhost: 8082, localhost: 8083
    ServerListRefreshInterval: 15000 

La scalabilité des bases de données

Intéressons nous maintenant aux bases de données qui représentent un grand défi dans les systèmes distribués. Mais avant de commencer nous allons voir un théorème très intéressant dans les systèmes distribués.

Cap theorem

Pour répondre à la question évoquée plus haut : “Pourquoi ne pouvons nous pas avoir à la fois la cohérence avec la disponibilité ?”, intéressons nous à ce théorème.

Prouvé en 2002, ce théorème indique qu’un ensemble de données distribuées ne peuvent pas être simultanément cohérent, disponible et tolérant aux partitions

Architecture Maître / Esclave

Supposons maintenant que notre base de données devient très populaire, et le nombre de requêtes par seconde devient énorme, cela aura automatiquement un impact sur les performances de notre application. 

Mais en général le nombre de lectures est très grand comparé au nombre d’écritures dans la base de données, ainsi l’écriture est très rapide comparé à la lecture même avec les index… Alors cela nous pousse à penser à utiliser une architecture Maître / Esclave en écrivant toujours dans la base de données Maître qui va se charger de propager les modifications sur les bases de données esclaves, et pour la lecture c’est toujours à travers les esclaves.

Y’a t-il un problème dans ce que nous venons de faire ? … Et oui ! Nous venons de perdre la cohérence (la troisième lettre dans l’ACID).

les propriétés ACID (atomicité, cohérence, isolation et durabilité) sont un ensemble de propriétés qui garantissent qu’une transaction informatique est exécutée de façon fiable.

Y’a t-il un deuxième problème ici ? Effectivement, le single point of failure, et cela se traduit par le fait que si le master tombe en panne il n’y a plus d’écriture possible. Et maintenant on vient de perdre aussi la disponibilité

On peut sûrement faire mieux. Vous vous demandez peut être pourquoi ne pas créer plusieurs instances du master et gérer la communication entre eux. C’est possible mais là on aura un très grand problème à résoudre qui est le consensus pour gérer l’ordre d’exécution des requêtes, et qui est implémenté par plusieurs grandes entreprises.

Consensus

Objectif

Supposons qu’on dispose d’un système critique, par exemple une base de données utilisées par un système de commerce électronique en ligne, et on ne souhaite pas que le service s’interrompt dès lors que la base de données tombe en panne. On utilise alors un protocole de réplication de données. Au lieu d’utiliser une seule base de données on utilise plusieurs exemplaires de cette base de données en parallèle. Ce qu’il faut garantir ici c’est que ces différents réplicats soient dans un état cohérent ainsi si un réplicat tombe en panne les autres réplicats pourrons continuer à servir les requêtes et le service sera hautement disponible.

Solution

Pour résoudre ce problème il suffit de fournir aux différents réplicats les même requêtes dans le même ordre, et c’est là que le consensus intervient pour choisir cet ordre. Pour répondre à ce besoin on trouve le protocole Paxos suggéré par Leslie Lamport qui a remporté le célèbre prix Turing en 2013.

Voici quelque autres applications des algorithmes de consensus :

  • Décider de la validation d’une transaction distribuée
  • Désigner un nœud dirigeant pour une tâche distribuée
  • Synchroniser des états de machine pour assurer leur cohérence

Les algorithmes de consensus sont présents dans de nombreux systèmes : le PageRank de Google, l’équilibrage de charge, les réseaux intelligents, la blockchain, la synchronisation d’horloge et même le contrôle de drone.

La blockchain du Bitcoin par exemple utilise l’algorithme de preuve de travail (Proof-of-Work) pour assurer la sécurité sur un réseau non fiable. Sur les ordinateurs des mineurs, les logiciels utilisent leur capacité de traitement pour résoudre un problème mathématique lié aux transactions. Ainsi la seule façon d’attaquer cette blockchain est l’attaque à 51% (51 % de la puissance totale de calcul du système appartient, même brièvement, à une seule entité) ce qui parait impossible !

Partitionnement des bases de données

Avec le partitionnement, vous divisez votre base de données en plusieurs bases de données plus petites, appelées fragments. Il existe deux types de partitionnement possibles : le partitionnement horizontal et le partitionnement vertical.

Partitionnement horizontal

Ce processus permet de partitionner une table de la base de données en plusieurs tables qui contient le même nombre de colonnes. Un cas d’utilisation pourrait être de diviser une table selon la localisation des utilisateurs. Supposons une table qui contient les informations des clients de plusieurs pays ( France, Maroc, Etats unis … ), on peut ainsi diviser cette table en 3 tables, une qui contient les clients français, une autre pour les clients marocains et une troisième pour les clients américains.

Avec ce partitionnement on a bien distribué notre base de données, le seul problème c’est qu’on pourra avoir plus de requête sur une base de données et beaucoup moins sur les autres, par exemple si la plupart des clients dans notre exemple sont français

Partitionnement vertical

Ce type de partitionnement divise la table verticalement, ce qui signifie qu’on change la structure de la table, les tuples sont découpés et fragmentés, et nécessitent une colonne commune (clé ou unique) dupliquée. Le scénario idéal pour ce type de partitionnement est lorsqu’on a pas besoin de toutes les informations d’un client dans une requête.   

Le partitionnement d’une base de données peut être implémenté soit au niveau applicatif, soit au niveau de la base de données. Pour le partitionnement au niveau de la base de données on trouve Cassandra, Hadoop (HDFS), Hbase et MongoDB qui sont nativement partitionnés. Pour le partitionnement au niveau applicatif on trouve Redis, et Memcached.   

A noter que le partitionnement ne se limite pas seulement aux bases de données, on le trouve dans d’autres types de systèmes distribués comme par exemple Apache Kafka.

Transactions distribuées

Une transaction distribuée est une suite d’instructions qui change deux sources différentes.

Problématique

Si un objet est écrit par une transaction et que sa valeur est lue en même temps par une autre requête, la deuxième requête doit retourner l’ancienne valeur (c’est l’isolation dans ACID), et assurer que la transaction se fait au complet ou pas du tout (c’est l’atomicité dans ACID).

Solution

Le problème évoqué plus haut est très important pour les systèmes basés sur un architecture microservice. Dans cette partie nous allons présenter deux solutions possibles pour répondre à ce besoin.

  • 2pc (two-phase commit)
  • Saga

Two-phase commit

Comme son nom l’indique cet algorithme à deux phases. Une phase de préparation et une deuxième phase de commit. Cet algorithme assure l’atomicité des transactions distribuées.

Phase de préparation : tous les microservices vont devoir se préparer pour un changement dans la base de données.

Phase de commit : une fois que tous les microservices sont prêts la phase de commit va indiquer aux microservices de faire des commit.

Pour bien comprendre l’algorithme prenons un exemple où on dispose de deux microservices CustomerMicroservice et OrderMicroservice.

Dans le cas normal où aucun microservice n’a crashé, on aura le scénario suivant :

Dans le cas ou un microservice crashe avant la phase du commit, le coordinateur doit envoyer une demande de Rollback aux autres microservices, et on aura le scénario suivant :

Dans le cas où un microservice crashe pendant la phase du commit, après sa récupération il va demander auprès du coordinateur ce qui s’est passé.

Inconvénients : Le plus grand inconvénient du protocole de validation en deux phases est qu’il s’agit d’un protocole synchrone. Si le coordinateur échoue définitivement, certains participants ne finiront jamais leurs transactions ( après qu’un participant a envoyé un message d’accord au coordinateur, il se bloquera jusqu’à ce qu’un commit ou un Rollback soit reçu ).


Le protocole Saga

Le protocole Saga est très utilisé dans les systèmes distribués. Pour bien comprendre ce protocole prenons l’exemple précédent. Le microservice OrderMicroservice va recevoir une requête pour modifier sa base de données. Premièrement il modifie sa base de données puis renvoie un message au microservice CustomerMicroservice. Ce dernier va alors modifier à son tour sa base de données. Maintenant si le CustomerMicroservice crashe avant de compléter sa transaction local, les autres microservices vont exécuter un Rollback sur leur transaction.

Inconvénients : Le plus grand inconvénient de ce protocole c’est qu’on perd l’isolation (la lettre i des propriétés ACID) sans oublier qu’il est difficile à mettre en oeuvre et à maintenir.

Conclusion

Nous avons vu dans cet article une définition des systèmes distribués, la différence entre le Vertical Scaling et l’Horizontal Scaling, puis également la différence entre les algorithmes statiques et dynamiques pour la répartition des charges et finalement la scalabilité des bases de données.


0 commentaire

Laisser un commentaire

Avatar placeholder

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

Une introduction aux systèmes distribués

par Zakaria M. temps de lecture : 10 min
0