La courte échelle

Comment faire passer à l'échelle un composant ? Comment accroître les ressources et capacités à sa disposition ainsi que sa robustesse ? Comment lui faire tenir une charge d'utilisation plus importante ?

Voici une liste de compromis et de grands principes.

Vertical vs Horizontal

Un composant d'un système peut passer à l'échelle verticalement ou horizontalement.

Le passage à l'échelle vertical revient à utiliser une machine plus puissante, avec plus de capacités (RAM, SSD, CPU, ...). Il est simple à mettre en œuvre, mais se heurte à la limite physique imposée par une seule machine et à la présence d'un point de défaillance unique (single point of failure).

Le passage à l'échelle horizontal revient à utiliser plusieurs machines en parallèle les unes des autres. Il supprime le point de défaillance unique, augmente la robustesse du composant, et découple ses ressources et capacités de celles d'une seule machine. Nous permettant de les augmenter toujours plus.

Pour mieux comprendre l'impact du passage à l'échelle horizontal sur la robustesse d'un composant, un parallèle peut être fait entre le point de défaillance unique et le facteur bus qui est une mesure du degré de partage d'information et de compétences entre les membres d'une équipe. Plus précisément, il s'agit du nombre de personnes qu'un bus devrait écraser pour mettre fin définitivement à un projet.

Sans État vs Avec État

Un composant d'un système peut être sans état (stateless).

Son fonctionnement ne dépend alors pas du temps qui passe. Peu importe ce qui arrive, il marche toujours de la même manière. Ne sauvegardant aucune donnée, et traitant chaque nouvelle requête comme si c'était la première fois qu'il la voyait.

La méthode pour passer à l'échelle horizontalement un composant sans état est la réplication. On réplique le composant sur plusieurs machines et un répartiteur de charge répartit la charge de façon homogène entre les différentes répliques qui n'ont besoin ni de se partager de données, ni de se coordonner d'une quelconque manière. Elles sont indépendantes les unes des autres.

Un composant d'un système peut être avec état (stateful).

Son fonctionnement dépend alors du temps qui passe. Il intègre et enregistre des données au fur et à mesure. La façon dont il traite une nouvelle requête dépend de toutes celles qu'il a déjà traitées précédemment. Son état interne dérive. Il ne marche pas toujours de la même manière. Il est la somme de toutes les interactions que l'on aura eu avec lui.

Les méthodes pour passer à l'échelle horizontalement un composant avec état sont la réplication et le sharding.

Pour la réplication, ce n'est pas aussi simple que pour un composant sans état. Les différentes répliques vont devoir se partager des données et se synchroniser afin que chacune maintienne le même état interne. Elles sont couplées les unes aux autres et vont devoir interagir pour fonctionner normalement.

Quant au sharding, cela consiste à éclater, fragmenter, partitionner les données du composant. Chaque shard n'aura qu'une partie des données et ne se souciera pas des autres.

Nous pouvons bien sûr combiner les deux techniques. Dans tous les cas, il faudra mettre en place une stratégie de répartition de la charge plus complexe que celle utilisée pour les composants sans état.

Lecture vs Écriture

La proportion entre les lectures (reads) et les écritures (writes) est importante dans l'optimisation d'un système.

Quand il y a beaucoup plus de lectures que d'écritures, on dit que le système est orienté lecture (read-heavy). Dans ce cas, on pourra :

Quand il y a beaucoup plus d'écritures que de lectures, on dit que le système est orienté écriture (write-heavy). Dans ce cas, on pourra :

On peut aussi aller plus loin en concevant séparément le traitement des lectures (queries) de celui des écritures (commands), en utilisant des composants spécifiques pour l'un et pour l'autre. Ce motif s'appelle la Command Query Responsibility Segregation (CQRS).

Si l'on regarde un système de l'extérieur, qu'on le considère comme une boîte noire, alors :

Il y a un compromis à faire entre le travail nécessaire pour écrire dans le système et celui pour y lire.

Cohérence vs Disponibilité

Dans un système distribué sur plusieurs machines, d'après le Théorème CAP, on ne peut garantir que deux contraintes parmis les trois suivantes :

Par définition, un système distribué se doit de tolérer des partitionnements arbitraires du réseau. Nous devons donc choisir entre cohérence et disponibilité.

Les bases de données relationnelles (SQL) qui respectent les propriétés ACID préfèrent la cohérence à la disponibilité.

Les bases de données modernes (NoSQL) conçues autour de la philosophie BASE préfèrent la disponibilité à la cohérence.

ACID vs BASE

ACID est un ensemble de propriétés que respectent les transactions (écritures) d'une base de données relationnelle (SQL) :

BASE est un ensemble de principe s'opposant à l'ACID (d'où le jeu de mots chimique), utilisé par les base de données modernes (NoSQL) :

ACID est fondé sur la cohérence immédiate, alors que BASE est fondé sur la cohérence dans un certain temps.

La cohérence d'un système se trouve sur un spectre.

Nom Description
Weak consistency Après une écriture, les lectures pourront ou ne pourront pas la voir.
Eventual consistency Après une écriture, les lectures la verront au bout d'un certain temps.
Strong consistency Après une écriture, les lectures la verront immédiatement.

Temps vs Espace

En échangeant du temps contre de l'espace ou inversement, nous pouvons optimiser un composant. C'est particulièrement important pour les appels réseaux ou la conception des schémas d'une base de données.

Voici quelques exemples :

Antérieures vs Postérieures

Un composant doit être compatible avec ses versions antérieures (backward) et postérieures (forward).

Autrement dit, on doit pouvoir le substituer par l'une de ses versions antérieures ou postérieures indépendamment du reste du système.

Requête Réseau vs Appel de Fonction

Un appel de fonction peut réussir, échouer ou ne jamais se terminer. Une requête réseau peut réussir, échouer ou ne jamais recevoir de réponse.

Lorsqu'une requête réseau ne reçoit pas de réponse. Est-ce la requête qui n'est pas parvenue jusqu'au serveur ? Ou bien la réponse qui n'est pas parvenue jusqu'à nous ?

Autrement dit, lorsqu'on envoie une lettre à quelqu'un par la poste et que l'on ne reçoit pas de réponse. Est-ce notre lettre qui n'est pas parvenue jusqu'à notre destinataire ? Ou bien sa réponse qui n'est pas parvenue jusqu'à nous ?

Doit-on envoyer une nouvelle requête au risque qu'elle soit traitée deux fois par le serveur si la première avait bel et bien été reçue par lui ?

Ce sont autant de questions qui ne se posent pas dans un appel de fonction mais qui deviennent centrales lors d'une requête réseau.

Le concept d'idempotence, qui signifie qu'appliquer une opération une ou plusieurs fois revient au même, nous permettra de gérer ces situations au mieux, en renvoyant sans risque la requête qui n'a pas reçu de réponse.

Résumé

Le passage à l'échelle vertical est plus simple que celui horizontal mais cela pourrait ne pas suffire. Attention tout de même à ne pas faire d'optimisation prématurée. La complexité d'un système distribué est sans commune mesure avec celle d'un système qui ne l'est pas. Bien qu'elle permet une haute disponibilité, la mise à l'échelle horizontale vient avec tout un lot de problèmes qui n'existaient pas lorsque tout fonctionnait sur une seule machine.

Préférez des composants sans état qu'avec état.

Analysez les lectures et les écritures du système :

Pensez toujours à la compatibilité d'un composant avec ses versions antérieures et postérieures.

Préférez des opérations idempotentes pour les requêtes réseau.

Et surtout. N'oubliez pas. Avant toutes optimisations. Mesurez, mesurez, MESUREZ !