Be Software Architecture Wise
Be Wise Apps

Read in English · Leer en español · Auf Deutsch lesen

Section 9.1 sur 9.8 · Intermédiaire · 28 min de lecture

Stratégies de passage à l’échelle : verticale, horizontale et cube de scalabilité

Leçon vidéo

5 min · narré en anglais, avec sous-titres
Écouter seulement (audio)
Lire la transcription

Bienvenue dans la section neuf point un. Quand davantage de personnes utilisent un système, il faut ajouter de la capacité. Cette leçon porte sur les choix pratiques : quelles ressources ajouter, où, quand, et avec quel degré d’automatisation.

Pensez à un café très fréquenté. Vous pouvez servir plus de clients en achetant une machine à café plus grande et plus rapide. C’est la scalabilité verticale. Ou vous pouvez ouvrir davantage de comptoirs avec davantage de personnel. C’est la scalabilité horizontale. Elle ne fonctionne que si n’importe quel comptoir peut servir n’importe quel client. Vous pouvez aussi répartir le travail : un comptoir pour les boissons et un pour la nourriture, ou un café par quartier. Et comme le personnel supplémentaire met du temps à arriver, un gérant avisé le planifie avant le coup de feu de midi.

La scalabilité verticale, ou scaling up, donne plus de puissance à une seule machine, et rien ne change dans le logiciel. Mais il existe une machine plus grande que toutes les autres, les prix grimpent fortement en haut de gamme, et une machine constitue un seul domaine de défaillance. La scalabilité horizontale, ou scaling out, exécute davantage d’instances derrière un équilibreur de charge. Avec huit instances, en perdre une coûte un huitième de la capacité, pas sa totalité. Le prix à payer, c’est que n’importe quelle instance doit pouvoir traiter n’importe quelle requête, et que le travail doit être divisible. Avant tout, faites passer à l’échelle le goulot d’étranglement actuel. Doubler le niveau web quand la base de données est occupée à quatre-vingt-quinze pour cent n’apporte rien d’autre qu’une facture plus lourde.

Martin Abbott et Michael Fisher décrivent trois façons différentes de passer à l’échelle horizontalement avec le cube de scalabilité. Son coin d’origine est une instance unique qui fait tout. L’axe X, c’est le clonage : des copies identiques derrière un équilibreur de charge, chacune faisant tout le travail. C’est l’axe le moins coûteux, mais chaque clone parle toujours aux mêmes données. L’axe Y découpe par fonction : des services distincts pour les cartes, les commandes, l’affectation et les paiements, chacun passant à l’échelle et tombant en panne séparément. C’est l’axe qui sous-tend les microservices. L’axe Z découpe par données : le même code, mais chaque copie gère une tranche des clients, par exemple une cellule ou un shard par ville. Une faute dans une cellule n’affecte que ses clients. Les systèmes réels combinent les trois. Utilisez l’axe le moins coûteux qui résout le problème réel.

Le clonage ne fonctionne que si n’importe quel clone peut servir n’importe quelle requête : le service doit donc être sans état. L’état se cache dans les sessions conservées en mémoire, les fichiers sur le disque local, les tâches planifiées, les verrous internes au processus et les compteurs. Chacun passe dans un stockage partagé. L’absence d’état coûte peu à intégrer dès la conception, et cher à ajouter après coup.

L’autoscaling ferme une boucle de rétroaction. Les utilisateurs envoient du trafic aux instances en passant par un équilibreur de charge. Les instances signalent leur charge à une base de métriques, après un délai. L’autoscaler compare la métrique à sa cible et choisit un nombre d’instances. L’orchestrateur ou l’API cloud lance ou arrête alors des instances. Et chaque nouvelle instance doit démarrer, chauffer et passer sa sonde de disponibilité avant de recevoir le moindre trafic. Entre une hausse de la charge et une capacité utilisable, il s’écoule souvent de deux à dix minutes. La scalabilité réactive gère donc les changements progressifs. Les hausses soudaines exigent une marge de capacité, des files d’attente ou une capacité planifiée à l’avance.

Quel signal doit piloter la boucle ? Une API de commandes passe surtout son temps à attendre la base de données et d’autres services : son CPU peut donc rester à vingt-cinq pour cent alors que tous les threads sont occupés. Un meilleur signal est lié à la ressource qui vient à manquer : les requêtes en cours par instance, ou, pour les workers de file d’attente, l’arriéré par worker. La latence augmente pour des raisons que davantage d’instances ne peuvent pas corriger, comme une base de données lente. Utilisez-la pour les alertes, pas pour le passage à l’échelle. Et pour éviter les oscillations, ajoutez des instances rapidement et retirez-les lentement, avec un nombre minimal et un nombre maximal d’instances.

Dimensionnons l’API de commandes de la plateforme de livraison de repas pour le pic de midi. La minute la plus chargée a atteint deux mille cinquante requêtes par seconde, avec une croissance attendue de quinze pour cent. Prévoyons donc deux mille quatre cents. Chaque instance dispose de trente-deux threads de travail et met cent vingt millisecondes par requête. D’après la loi de Little, cela fait deux cent quatre-vingt-huit requêtes en cours au pic. Pour garder trente pour cent de marge, faites tourner chaque instance à soixante-dix pour cent de ses threads : vingt-deux virgule quatre requêtes. Il en faut donc douze virgule neuf, soit treize instances. Mais le service doit survivre à la perte de l’une de ses trois zones. Avec une répartition cinq, quatre et quatre, en perdre cinq laisse huit instances avec trente-six requêtes chacune, plus que leurs trente-deux threads. En autorisant quatre-vingt-cinq pour cent pendant la perte d’une zone, il faut onze instances dans deux zones : six par zone, dix-huit au total. C’est la survie à la perte d’une zone, et non la charge moyenne, qui fixe la taille du parc. Enfin, vérifiez le niveau inférieur. Un maximum de trente instances avec dix connexions chacune exige trois cents connexions, mais la base de données n’en autorise que deux cents. L’équipe ajoute donc d’abord un pooler de connexions.

La section complète traite du passage à l’échelle du niveau des données et propose un guide de décision. Ensuite, dans la section neuf point deux, nous verrons l’équilibrage de charge et les sessions. À très vite.

Ce qu’il faut savoir

Ce que signifie passer à l’échelle en pratique

La section 2.4 a défini la scalabilité comme la capacité à absorber davantage de charge en ajoutant des ressources, et a montré avec la loi d’Amdahl et la loi universelle de scalabilité pourquoi ajouter des ressources n’ajoute pas toujours de la capacité. Cette section porte sur les choix pratiques : quelles ressources ajouter, où, quand et avec quel degré d’automatisation. Le point de départ est toujours la même question : quel est le paramètre de charge, et quelle ressource vient à manquer en premier ? À midi, l’API de commandes d’une plateforme de livraison de repas est limitée par les requêtes par seconde ; son service d’affectation, par le nombre de livreurs actifs dont les positions doivent être rapprochées toutes les quelques secondes ; sa base de données, par les transactions d’écriture par seconde et par les connexions. Chacun appelle une réponse différente.

Faire passer un système à l’échelle, c’est en réalité faire passer à l’échelle son goulot d’étranglement actuel, puis trouver le suivant. Doubler le niveau web d’un système dont la base de données est occupée à 95 % n’apporte rien d’autre qu’une facture plus lourde et une base de données encore plus sollicitée. Toute décision de passage à l’échelle commence donc par une mesure : taux d’utilisation et saturation de chaque ressource (la méthode USE de Brendan Gregg, présentée dans Observabilité), et un test de charge qui trouve à partir de quand la latence commence à grimper (Planification de la capacité et tests de performances).

Scalabilité verticale : une machine plus grosse

La scalabilité verticale (scaling up) consiste à donner à la machine existante plus de cœurs de CPU, plus de mémoire, des disques plus rapides ou un réseau plus rapide. Sa grande vertu est que rien ne change dans le logiciel. Une base de données relationnelle unique sur un serveur plus grand conserve ses transactions, ses jointures et sa cohérence simple ; une application conserve ses caches en mémoire et ses fichiers locaux. Dans le cloud, le redimensionnement est un appel d’API suivi d’un redémarrage. Les plus grandes machines virtuelles du cloud offrent désormais des centaines de CPU virtuels et des téraoctets de mémoire, bien plus que ce dont la plupart des systèmes de gestion auront jamais besoin, et certains sites web très fréquentés ont décrit publiquement comment ils servent un trafic énorme à partir d’un petit nombre de serveurs de bases de données puissants.

Les limites sont pourtant réelles. Il existe une machine plus grande que toutes les autres, et le prix par unité de capacité augmente fortement en haut de la gamme. Une machine constitue un seul domaine de défaillance : quand elle tombe en panne, ou qu’elle est redémarrée pour être redimensionnée ou corrigée, tout ce qu’elle héberge s’arrête ; la scalabilité verticale seule n’apporte donc rien à la disponibilité. Le logiciel ne peut souvent pas exploiter pleinement une machine plus grosse : un processus monothread, un verrou global ou une ligne chaude de la base de données limitent l’accélération exactement comme le prédit la loi d’Amdahl. Et la capacité vient par grandes marches lentes, si bien que vous payez le pic jour et nuit. La scalabilité verticale reste néanmoins le bon premier geste bien plus souvent que la mode ne le laisse croire, en particulier pour les bases de données, où la scalabilité horizontale coûte cher en effort de conception.

Scalabilité horizontale : plus de machines

La scalabilité horizontale (scaling out) consiste à exécuter davantage d’instances et à répartir le travail entre elles, généralement avec un équilibreur de charge en amont. Elle va beaucoup plus loin, par petites marches peu coûteuses, et la redondance qui ajoute de la capacité permet aussi de tolérer les défaillances : avec huit instances, en perdre une coûte un huitième de la capacité plutôt que sa totalité. Les plateformes cloud permettent d’ajouter une instance en quelques secondes ou minutes, ce qui est le fondement de l’autoscaling.

Le prix à payer est que le logiciel doit être conçu pour cela. N’importe quelle instance doit pouvoir traiter n’importe quelle requête, ce qui signifie que les instances ne doivent rien conserver dont dépendrait une requête ultérieure (voir la section suivante). Le travail doit être divisible : une tâche qui exige un verrou global unique ne s’exécute pas plus vite sur dix machines. Les données doivent être accessibles depuis chaque instance, ce qui reporte la pression sur le niveau de données partagé. Et le système devient distribué, avec les défaillances partielles, les délais d’expiration et les nouvelles tentatives que décrit la section 6.1. La plupart des systèmes réels combinent les deux : les niveaux applicatifs sans état passent à l’échelle horizontalement, tandis que les bases de données passent à l’échelle verticalement aussi longtemps que c’est raisonnable, puis horizontalement avec des répliques et des partitions.

Scalabilité verticale et horizontale comparées

PréoccupationVerticale (scaling up)Horizontale (scaling out)
Modifications du logicielAucuneInstances sans état et travail divisible
Limite supérieureLa plus grande machine disponibleGénéralement le niveau de données partagé ou les coûts de coordination
Courbe des coûtsAugmente fortement pour les plus grandes taillesÀ peu près linéaire, par petites marches
Défaillance (failure)Un seul domaine de défaillance ; le redimensionnement exige un redémarragePerdre une instance fait perdre une fraction de la capacité
Vitesse de changementQuelques minutes plus un redémarrageDe quelques secondes à quelques minutes, peut être automatique
Cohérence et transactionsInchangées et simplesInchangées pour les niveaux sans état ; difficiles pour les données
Usage typiqueBases de données, logiciels hérités et avec état, première réponse à la croissanceNiveaux web et API, workers, tout ce qui est sans état

Le cube de scalabilité : trois façons différentes de passer à l’échelle horizontalement

Martin Abbott et Michael Fisher, qui ont dirigé la technologie chez eBay et PayPal, décrivent la scalabilité horizontale à l’aide du cube de scalabilité dans The Art of Scalability (première édition 2009, deuxième édition 2015). Il distingue trois façons indépendantes de découper un système, chacune représentée par un axe d’un cube dont l’origine est une instance unique qui fait tout.

L’axe X est la duplication horizontale, ou clonage : exécuter N copies identiques de toute l’application ou du service et répartir les requêtes entre elles. C’est l’axe le plus simple et le moins coûteux, et il convient bien au traitement de requêtes sans état et au trafic de lecture. Il n’apporte rien pour la taille des données ni pour le volume d’écriture, car chaque clone parle toujours aux mêmes données, et rien non plus pour la taille de la base de code ou le nombre d’équipes qui y travaillent.

L’axe Y découpe le système par fonction : des services différents pour les cartes, les commandes, l’affectation et les paiements, chacun n’exécutant que le code et les données de sa capacité. Chaque service peut alors passer à l’échelle, tomber en panne et être déployé séparément, et chacun est plus petit et plus facile à comprendre. C’est l’axe qui sous-tend les microservices (section 6.3), avec tous leurs coûts. L’axe Z découpe par données : chaque copie exécute le même code, mais chacune n’est responsable que d’une tranche des clients, des locataires, des régions ou des enregistrements, choisie selon une clé comme la ville ou l’identifiant client. Le sharding de base de données est l’axe Z appliqué au stockage (section 7.4) ; une architecture cellulaire l’applique à des piles entières, de sorte qu’une faute ou un mauvais déploiement dans une cellule n’affecte que les clients de cette cellule.

Le cube de scalabilité

Le cube de scalabilité : l’axe X clone des instances identiques, l’axe Y découpe le système par fonction et l’axe Z par données, chaque copie servant une trancheorigine : uneinstance uniqueles trois axes combinésX : clonerY : parfonctionZ : pardonnéeAxe X : clonerN copies identiques derrière unéquilibreur, chacune fait tout :14 copies de l’API de commandesAxe Y : par fonctionServices par capacité :cartes, commandes, paiementset affectationAxe Z : par donnéesMême code, chaque copie gèreune part des clients ou données :cellule ou shard par ville
Le cube de scalabilité d’Abbott et Fisher : chaque axe est une façon différente de découper un système, et les systèmes réels combinent généralement les trois.

Les trois axes dans la plateforme de livraison de repas

AxeCe qui est découpéExempleRésoutCoûts et limites
X : clonerLes requêtes, entre des copies identiques14 instances de l’API de commandes derrière un seul équilibreur de chargeDébit de requêtes, défaillances d’instancesExige l’absence d’état ; tous les clones partagent une seule base de données
Y : fonctionCode et données, par capacitéServices distincts pour les cartes, les commandes, l’affectation et les paiementsBesoins différents en passage à l’échelle et en défaillances ; taille des équipesAppels réseau, données distribuées, charge d’exploitation
Z : donnéesClients ou enregistrements, par cléUne cellule par groupe de villes, ou commandes partitionnées par villeTaille des données, volume d’écriture, rayon d’impactCouche de routage, requêtes entre tranches, rééquilibrage, clés chaudes

Les axes se combinent. Le service de commandes de la plateforme (un découpage sur l’axe Y) s’exécute sous forme de nombreux clones (X) dans chaque cellule régionale (Z). Le conseil pratique d’Abbott et Fisher est d’utiliser l’axe le moins coûteux qui résout le problème réel : X pour le volume de requêtes, Y quand des parties du système ont des besoins réellement différents en matière de passage à l’échelle, de défaillances ou d’équipes, et Z quand un jeu de données ou un rayon d’impact est devenu trop grand. Choisir Z parce que cela fait impression apporte le routage, le rééquilibrage et les requêtes entre partitions bien avant qu’on en ait besoin.

L’absence d’état : la condition préalable au clonage

La scalabilité sur l’axe X ne fonctionne que si n’importe quel clone peut servir n’importe quelle requête. Un service sans état ne conserve rien entre les requêtes dans sa propre mémoire ni sur son propre disque ; tout ce dont une requête ultérieure a besoin se trouve dans un stockage partagé ou voyage avec la requête. L’état qui se cache habituellement dans une instance est facile à recenser dès qu’on le cherche : sessions utilisateur conservées en mémoire, fichiers envoyés écrits sur le disque local, caches en mémoire traités comme la source de vérité, tâches planifiées qui doivent s’exécuter sur exactement une machine, verrous internes au processus protégeant une ressource partagée, et compteurs de limitation de débit.

Chacun a un remède classique : les sessions vont dans un stockage partagé ou dans un jeton signé (section 9.2) ; les fichiers vont dans un stockage objet ; les caches deviennent une optimisation de performances qui peut être perdue à tout moment, ou passent dans un cache partagé (section 7.6) ; les tâches planifiées utilisent un ordonnanceur avec élection de leader ou une file d’attente ; les verrous passent dans la base de données ou dans un service de coordination ; les compteurs de limitation de débit passent dans un stockage partagé. La règle de l’application douze facteurs selon laquelle les processus sont sans état et ne partagent rien (section 10.3) exprime cette même idée. L’absence d’état coûte peu à intégrer dès la conception et cher à ajouter après coup, car l’état s’infiltre dans le code en de nombreux petits endroits.

Autoscaling : réactif, planifié et prédictif

L’autoscaling ferme une boucle de rétroaction : mesurer la charge, la comparer à une cible, modifier le nombre d’instances. L’autoscaling réactif le fait en continu à partir d’une métrique. Le suivi de cible (target tracking) en est la forme courante : maintenir le CPU moyen à 60 %, ou les requêtes en cours par instance à 30. Le Horizontal Pod Autoscaler de Kubernetes calcule desired = ceil(current replicas × current metric ÷ target) : six pods à 45 requêtes en cours en moyenne pour une cible de 30 deviennent ceil(6 × 45 ÷ 30) = 9. Par défaut, il réévalue la situation toutes les 15 secondes et ignore les écarts de moins de 10 % par rapport à la cible.

L’autoscaling planifié modifie la capacité à des heures connues : relever le minimum à 18 instances à 11 h 15 chaque jour de semaine avant le pic de midi, le rabaisser à 14 h 00. C’est rudimentaire mais très efficace pour des schémas prévisibles, et c’est le seul moyen d’avoir la capacité en place avant un changement brusque comme une mise en vente de billets à 10 h 00 précises. L’autoscaling prédictif, proposé par les principaux fournisseurs de cloud, prévoit la charge à partir de l’historique (cycles quotidiens et hebdomadaires) et passe à l’échelle en avance, avec l’autoscaling réactif comme filet de sécurité pour les surprises.

La propriété cruciale de tout schéma réactif est le décalage. Les métriques sont collectées et moyennées sur des dizaines de secondes ; l’autoscaler agit lors de sa prochaine évaluation ; une nouvelle machine virtuelle met une minute ou plus à démarrer, une image de conteneur doit être téléchargée, un service JVM ou Python doit démarrer, chauffer ses caches et ses pools de connexions et passer sa sonde de disponibilité avant que l’équilibreur de charge ne lui envoie du trafic. Dans un cluster, il faut parfois d’abord ajouter les nœuds eux-mêmes, au moyen d’un second autoscaler, plus lent. Entre le moment où la charge augmente et celui où la nouvelle capacité devient utile, il s’écoule couramment de deux à dix minutes. L’autoscaling réactif gère donc les changements progressifs ; les changements soudains doivent être absorbés par une marge de capacité, par des files d’attente (section 9.3) ou par une capacité planifiée à l’avance.

La boucle de rétroaction de l’autoscaling et l’origine du décalage

La boucle de rétroaction de l’autoscaling : les instances signalent leur charge, l’autoscaler la compare à une cible, l’orchestrateur lance des instances, qui ne rejoignent l’équilibreur de charge qu’une fois prêtesUsagersLoad balancerInstancesen serviceBase desmesuresAutoscalerOrchestrateurou API cloud1 envoi chargeaprès un délai4 démarre, chauffesonde validée2 comparer la métrique àla cible, fixer nb répliques3 lancer, arrêterinstancesCharge en hausse → capacité utile : souvent 2 à 10 min
Chaque étape de la boucle ajoute du délai : l’autoscaling réactif suit donc la charge avec plusieurs minutes de retard ; la marge de capacité et la capacité planifiée comblent l’écart.

Sur quelle métrique passer à l’échelle, et pourquoi le CPU est souvent un mauvais choix

Le taux d’utilisation du CPU est le signal par défaut de la plupart des plateformes parce qu’il est toujours disponible. Ce n’est le bon signal que lorsque le service est limité par le CPU : redimensionnement d’images, chiffrement, sérialisation lourde. La plupart des services métier ne le sont pas. Une API de commandes passe l’essentiel de chaque requête à attendre la base de données, le prestataire de paiement et d’autres services ; son CPU peut rester à 25 % alors que tous les threads de travail sont occupés à attendre et que les nouvelles requêtes s’accumulent. Passer à l’échelle sur le CPU ne déclenche alors rien avant qu’il ne soit bien trop tard. La défaillance inverse se produit aussi : une tempête de ramasse-miettes ou un préchauffage JIT fait monter le CPU sans aucune hausse de la charge, et l’autoscaler ajoute des instances inutiles.

Un meilleur signal est un signal directement lié à la ressource qui vient à manquer. Pour les services requête-réponse, il s’agit généralement de la concurrence : le nombre de requêtes en cours par instance, comparé au nombre de threads de travail ou de connexions dont elle dispose. La loi de Little rend ce lien exact (en cours = taux d’arrivée × temps de réponse), et ce signal réagit à la fois à un trafic accru et à des réponses plus lentes. Pour les workers de file d’attente, c’est l’arriéré par worker ou l’âge du plus ancien message, qui exprime le temps que la file mettra à se résorber. La latence est tentante mais dangereuse comme signal de passage à l’échelle : elle augmente pour des raisons que davantage d’instances ne peuvent pas corriger, comme une base de données lente, et ajouter des instances à un service dont une dépendance est en difficulté envoie encore plus de charge à cette dépendance. Utilisez la latence pour les alertes, et passez à l’échelle sur une mesure du travail.

Les signaux de passage à l’échelle et quand ils trompent

SignalConvient àTrompe quand
Taux d’utilisation du CPUTravail limité par le CPU : traitement d’images, chiffrement, sérialisation lourdeLe travail attend des E/S, des threads ou des connexions ; le ramasse-miettes ou le préchauffage gonflent le CPU
Requêtes par seconde par instanceServices dont les requêtes coûtent à peu près la même choseLa répartition des requêtes change, par exemple plus de recherches et moins de lectures de la carte
Requêtes en cours par instanceLa plupart des services requête-réponse ; correspond aux limites de threads et de connexionsUne dépendance en aval ralentit ; ajouter des instances lui ajoute alors de la charge, donc plafonnez le maximum
Arriéré de la file par worker, ou âge du plus ancien messageWorkers asynchronesLes workers sont limités par une limite de débit en aval plutôt que par leur nombre
LatenceAlertes et SLO (section 9.5)La cause est une dépendance partagée que davantage d’instances ne peuvent pas corriger
Métrique métier (commandes par minute, sessions actives)Charge ayant une relation stable et mesurée avec la capacitéLa relation dérive à mesure que le produit évolue ; recalibrez avec des tests de charge

Cooldowns, stabilisation et flapping

Un autoscaler qui réagit à chaque soubresaut va osciller (flapping) : ajouter des instances quand une rafale arrive, les retirer une minute plus tard quand elle est passée, puis les rajouter, en payant chaque fois le coût du démarrage et en perturbant les caches et les pools de connexions. Il peut aussi courir après sa propre queue : les nouvelles instances sont froides et lentes, donc la latence ou le CPU par requête augmente brièvement après l’ajout d’instances, ce qui ressemble à davantage de charge.

Les défenses classiques sont asymétriques. Ajoutez des instances rapidement, car un manque de capacité pénalise les utilisateurs ; retirez-les lentement, car un excès de capacité ne coûte que de l’argent. Un cooldown, ou fenêtre de stabilisation, fait attendre l’autoscaler après une action, ou lui fait retenir la recommandation la plus élevée sur une fenêtre récente avant de réduire : par défaut, l’autoscaler de Kubernetes utilise une fenêtre de cinq minutes (300 secondes) pour la réduction et aucune pour l’augmentation. Une bande de tolérance ignore les petits écarts. Des nombres minimal et maximal d’instances bornent la boucle : le minimum conserve assez de capacité pour survivre à la perte d’une zone et aux premières minutes d’une hausse soudaine, et le maximum protège la base de données, les services en aval et le budget contre un autoscaler qui prendrait un problème pour de la charge. La réduction doit aussi se faire en douceur : une instance choisie pour être retirée doit cesser de recevoir de nouvelles requêtes et terminer celles en cours (drainage des connexions, section 9.2).

Exemple commenté

Exemple commenté : dimensionner l’API de commandes pour le pic de midi

L’API de commandes de la plateforme de livraison de repas doit absorber le déjeuner en semaine. Au cours des huit dernières semaines, la minute la plus chargée a atteint 2050 requêtes par seconde (rps) vers 12 h 30 ; le marketing prévoit une croissance de 15 % ce trimestre. Un test de charge sur une instance (4 vCPU, 32 threads de travail) a mesuré un temps de réponse moyen de 120 ms et une latence qui commence à grimper vers 260 rps. Le service tourne dans trois zones de disponibilité et doit survivre à la perte d’une zone au pic.

Pic prévu λ2050 × 1,15 ≈ 2360, arrondi à 2400 rps
Loi de Little pour le parcL = λ × W = 2400 × 0,12 s = 288 requêtes en cours au pic
Capacité d’une instance32 threads ÷ 0,12 s ≈ 267 rps, ce qui concorde avec le coude trouvé par le test de charge (environ 260 rps)
Taux d’utilisation cible70 % de 32 threads = 22,4 requêtes en cours par instance (environ 187 rps), ce qui laisse 30 % de marge pour les rafales
Instances pour le pic288 ÷ 22,4 = 12,9, donc 13 instances (2400 ÷ 13 ≈ 185 rps chacune)
Survivre à la perte d’une zone13 instances réparties en 5/4/4 : en perdre 5 laisse 8 instances avec 288 ÷ 8 = 36 requêtes en cours chacune, plus que 32 threads : surcharge. Autoriser jusqu’à 85 % (27,2 en cours) pendant la perte d’une zone exige 288 ÷ 27,2 = 10,6, donc 11 dans deux zones : 6 par zone, 18 au total
Montée en charge et décalage de l’ajout d’instancesLa demande passe de 600 rps à 11 h 30 à 2400 à 12 h 15 : 40 rps par minute. L’ajout réactif d’instances prend environ 5 minutes (1 minute pour détecter, 4 pour démarrer et chauffer), pendant lesquelles la demande augmente de 200 rps ; la capacité disponible à 70 % est d’environ 80 rps par instance, donc l’autoscaling réactif seul pourrait suivre cette montée progressive ; ce qu’il ne peut pas apporter, c’est une protection contre la perte d’une zone
DécisionL’autoscaling planifié relève le minimum à 18 (6 par zone) à 11 h 15 et le ramène à 6 (2 par zone) à 14 h 00 ; l’autoscaling réactif sur les requêtes en cours par instance (cible 22) reste en place comme filet de sécurité, avec un maximum de 30
Vérification de la base de données18 instances × un pool de 10 connexions = 180 ; au maximum de 30 instances, 300. La base de données en autorise 200 : l’équipe ajoute donc un pooler de connexions avant de relever le maximum

La loi de Little transforme un débit prévu et un temps de réponse mesuré en un chiffre de concurrence que l’on peut comparer directement aux limites de threads et de connexions.

C’est l’exigence de survie à la perte d’une zone, et non la charge moyenne, qui fixe la taille du parc au pic : 18 instances au lieu de 13. L’autoscaling réactif ne peut pas apporter cette protection, car remplacer une zone perdue prend plusieurs minutes.

L’ajout d’instances au niveau applicatif a multiplié la charge sur les connexions de la base de données partagée, qui est le prochain goulot d’étranglement. Tout exercice de dimensionnement doit se terminer par une vérification du niveau inférieur.

Faire passer le niveau des données à l’échelle

Les niveaux applicatifs sans état sont faciles à faire passer à l’échelle ; c’est tout l’intérêt de les rendre sans état. La pression retombe alors sur les bases de données, les caches et les courtiers qu’ils partagent, et c’est pourquoi le niveau des données est généralement la véritable limite d’un système. Les outils ont été présentés au chapitre 7 et sont résumés ici dans l’ordre où les professionnels y ont généralement recours. Premièrement, réduire le travail : index, meilleures requêtes, moins d’allers-retours par requête. Deuxièmement, absorber les lectures avec la mise en cache (section 7.6) et des répliques en lecture, en acceptant le retard de réplication là où le cas d’usage le permet. Troisièmement, protéger les connexions : chaque instance applicative détient un pool, donc le total croît avec le parc, et un pooler de connexions comme PgBouncer permet à des milliers de connexions clientes de partager quelques centaines de connexions à la base (la valeur par défaut de max_connections dans PostgreSQL n’est que de 100). Quatrièmement, faire passer le serveur primaire à l’échelle verticalement, ce qui est souvent moins coûteux que toute refonte. Seulement ensuite, partitionner les écritures selon l’axe Z par une clé comme la ville ou le client (section 7.4), ou passer à une base de données SQL distribuée.

Deux mises en garde s’imposent. Premièrement, l’autoscaling du niveau applicatif peut nuire au niveau des données : chaque nouvelle instance ouvre des connexions et chauffe son cache à partir de la base de données, si bien qu’un ajout massif d’instances peut à lui seul surcharger la base. Deuxièmement, certaines limites sont logiques et non physiques : une seule ligne chaude (le stock des 500 derniers billets d’un concert, ou un numéro de séquence global) sérialise les écritures quelle que soit la taille du serveur. Ces limites se suppriment par la conception, par exemple en répartissant le stock sur plusieurs compteurs ou en réservant les places par lots, et non par le matériel.

Guide de décision

Guide de décision : comment ajouter de la capacité à un composant

Parcourez les questions dans l’ordre pour le composant qui limite le système aujourd’hui.

  1. Le goulot d’étranglement a-t-il été mesuré (taux d’utilisation, saturation et mise en file d’attente pour le CPU, la mémoire, les connexions, les verrous et les appels en aval) plutôt que deviné ?
    OuiContinuez avec le composant qui sature en premier.
    NonFaites d’abord un test de charge et mesurez. Faire passer à l’échelle le mauvais niveau ajoute du coût sans ajouter de capacité, et peut reporter davantage de charge sur le véritable goulot d’étranglement.
  2. Le travail lui-même peut-il être rendu moins coûteux (un index, un cache, moins d’allers-retours, une charge utile plus petite, moins de journalisation) ?
    OuiFaites-le d’abord. Cela réduit les coûts à toutes les échelles et supprime souvent tout besoin de passer à l’échelle.
    NonContinuez : il faut ajouter de la capacité.
  3. Le composant est-il sans état, ou son état (sessions, fichiers, verrous en mémoire, planifications) peut-il être déplacé vers des stockages partagés ?
    OuiPassez à l’échelle horizontalement sur l’axe X derrière un équilibreur de charge, avec un autoscaling sur un signal de charge et une marge suffisante pour couvrir le décalage de l’ajout d’instances.
    NonPassez à l’échelle verticalement pour l’instant, et planifiez le travail d’externalisation de l’état ; une instance unique avec état est aussi un point de défaillance unique.
  4. La limite se trouve-t-elle dans le niveau des données : débit d’écriture, taille de stockage ou nombre de connexions d’une base de données ?
    OuiUtilisez la mise en cache et des répliques en lecture pour les lectures, un pooler de connexions pour les connexions, et le partitionnement sur l’axe Z (shards ou cellules) pour les écritures et le stockage.
    NonContinuez.
  5. Certaines parties du composant ont-elles des besoins très différents en matière de charge, de passage à l’échelle ou de défaillances ?
    OuiEnvisagez un découpage sur l’axe Y pour que chaque partie passe à l’échelle et tombe en panne séparément, en acceptant les coûts de la distribution décrits au chapitre 6.
    NonRestez sur le clonage selon l’axe X : c’est l’axe le moins coûteux à exploiter.

Vérifiez votre compréhension

Le CPU d’une API de commandes est à 30 %, mais la latence p99 a triplé et tous les threads de travail sont occupés. Pourquoi l’autoscaler basé sur le CPU n’a-t-il pas réagi, et quel signal l’aurait fait ?

Le service attend des E/S : les threads sont donc épuisés alors que le CPU est inactif. Le nombre de requêtes en cours par instance, comparé au nombre de threads, aurait augmenté avec la charge.

Quel axe du cube de scalabilité répond au cas d’une base de données qui a dépassé la capacité d’écriture d’un seul serveur ?

L’axe Z : partitionner les données selon une clé pour que chaque shard ou cellule traite une tranche des écritures.

Pourquoi les autoscalers ajoutent-ils généralement des instances rapidement mais en retirent-ils lentement ?

Une capacité trop faible pénalise immédiatement les utilisateurs alors qu’une capacité trop grande ne coûte que de l’argent, et réduire trop vite provoque des oscillations quand la charge revient.

Pourquoi l’autoscaling réactif ne peut-il pas protéger une mise en vente de billets qui commence à 10 h 00 précises ?

La demande bondit en quelques secondes, mais détecter la hausse, démarrer des instances et les chauffer prend plusieurs minutes : la capacité doit donc être planifiée avant le début de la vente.

Suivant · 9.2Équilibrage de chargeLes équilibreurs de charge de couche 4 et de couche 7, les principaux algorithmes, dont la puissance de deux choix aléatoires, les contrôles de vivacité et de disponibilité, le drainage des connexions, les inconvénients des sessions persistantes, l’état de session externalisé et l’équilibrage de charge global.Continuer →

Sources et lectures complémentaires

Une question sur cette section ? ArchitectBot, l’assistant d’étude IA du cours, répond d’abord à partir du cours.

Demander : « Notre API de commandes passe à l’échelle sur le CPU, mais tombe quand même à l’heure du déjeuner. Sur quoi devrions-nous plutôt nous baser, et de combien d’instances avons-nous vraiment besoin ? »