Aller au contenu

Impact sur les choix d'architecture

Synthèse — comment les contraintes matérielles guident chaque décision logicielle.


Le matériel dicte les limites

Les chapitres précédents ont couvert le processeur, la mémoire, les I/O, les multiprocesseurs, les clusters et la virtualisation. Ce dernier chapitre fait la synthèse : comment ces réalités physiques se traduisent en décisions d'architecture logicielle.

Un architecte qui ignore les contraintes matérielles concoit des systèmes dont les performances sont imprévisibles. Un architecte qui les connait peut dimensionner avant de construire, identifier les goulots avant qu'ils n'apparaissent en production, et choisir les bons compromis entre coût, performance et complexité.


Les nombres que tout architecte doit connaître

Ces latences de référence, popularisees par Jeff Dean (Google), sont les constantes fondamentales de l'architecture système. Les ordres de grandeur sont stables depuis une decennie — les valeurs absolues evoluent lentement.

Opération Latence Ratio vs L1 cache
Référence cache L1 1 ns 1x
Branch mispredict 3 ns 3x
Référence cache L2 4 ns 4x
Référence cache L3 10 ns 10x
Mutex lock/unlock 17 ns 17x
Référence mémoire principale (DRAM) 100 ns 100x
Compression 1 KB avec Snappy 3 us 3 000x
Envoi 2 KB sur réseau 1 Gbps 20 us 20 000x
Lecture aléatoire SSD NVMe 100 us 100 000x
Lecture séquentielle 1 MB SSD 200 us 200 000x
Round-trip réseau même datacenter 500 us 500 000x
Lecture séquentielle 1 MB HDD 2 ms 2 000 000x
Seek disque HDD 5 ms 5 000 000x
Round-trip réseau inter-region 50 ms 50 000 000x
Round-trip réseau intercontinental 150 ms 150 000 000x

Warning

Un saut réseau au sein du même datacenter (500 us) coute 5 000x plus cher qu'un accès mémoire (100 ns). Un saut intercontinental coute 1 500 000x plus cher. Chaque appel réseau dans le chemin critique d'une requête se paie. C'est pourquoi les microservices trop fins (nano-services) degradent les performances : on remplacé des appels mémoire par des appels réseau.


Identifier la nature du goulot

Tout système est limite par l'une de trois ressources. Identifier laquelle est la première étape de tout travail de dimensionnement ou d'optimisation.

CPU-bound

Le processeur est sature. Les symptomes : utilisation CPU a 90-100%, temps d'attente I/O faible.

Causes typiques : calcul mathématique, serialisation/deserialisation JSON/Protobuf, compression, encryption, garbage collection, parsing.

Solutions : algorithme plus efficace, mise en cache des résultats, passage a un langage compile, ajout de cœurs (scale-up ou scale-out).

Memory-bound

La mémoire est le facteur limitant — soit en capacité (working set trop grand pour la RAM), soit en bande passante (le processeur attend les données).

Causes typiques : cache applicatif trop petit, base de données in-memory qui débordé, structures de données avec mauvaise localité, heap GC surdimensionne.

Solutions : augmenter la RAM, optimiser les structures de données pour la localité, compresser les données en mémoire, réduire le working set.

I/O-bound

Le disque ou le réseau est sature. C'est le cas le plus frequent en production.

Causes typiques : requêtes base de données lentes, appels API externes, lectures disque aléatoires, écriture de logs synchrone.

Solutions : cache (Redis, Memcached), I/O asynchrone, batch des écritures, SSD NVMe au lieu de HDD, read replicas.

graph TD
    START["Systeme lent"] --mesurer--> CPU{"CPU > 80% ?"}
    CPU --oui--> CPUB["CPU-bound<br>Optimiser le calcul"]
    CPU --non--> MEM{"RAM saturee ?<br>Swapping ?"}
    MEM --oui--> MEMB["Memory-bound<br>Augmenter RAM ou<br>reduire working set"]
    MEM --non--> IO{"I/O wait > 20% ?<br>Disque/reseau sature ?"}
    IO --oui--> IOB["I/O-bound<br>Cache, async I/O,<br>meilleur stockage"]
    IO --non--> LOCK["Contention<br>Locks, GC, coordination"]

Loi de Little

La loi de Little relie trois métriques fondamentales d'un système en regime permanent :

L = lambda * W
  • L : nombre moyen de requêtes dans le système (en cours de traitement + en attente)
  • lambda : débit d'arrivee (requêtes par seconde)
  • W : temps moyen de sejour dans le système (latence)

Cette loi est universelle — elle s'applique a une file d'attente, un serveur web, un pool de connexions ou un pipeline de données.

Application pratique

Si un serveur traite 1000 req/s (lambda) et que chaque requête prend 50 ms (W) en moyenne :

L = 1000 * 0.050 = 50 requetes en cours simultanement

Il faut donc au minimum 50 threads (ou goroutines, ou connexions) pour soutenir cette charge. Si le pool est dimensionne a 30, les requêtes s'accumulent dans la file d'attente — la latence augmente, ce qui augmente L, ce qui sature davantage la file : c'est le cercle vicieux de la surchargé.


Vertical scaling vs horizontal scaling

Vertical scaling (scale-up)

Augmenter les ressources d'une seule machine : plus de CPU, plus de RAM, stockage plus rapide.

Avantage Inconvénient
Simple — pas de distribution Limite physique (un serveur a un nombre max de cœurs/RAM)
Pas de complexité réseau Coût non-lineaire (2x RAM ne coute pas 2x prix)
Transactions ACID simples Single point of failure
Latence minimale (tout est local) Indisponibilite pendant les upgrades matériels

Horizontal scaling (scale-out)

Ajouter des machines au cluster.

Avantage Inconvénient
Pas de limite théorique Complexité distribuée (CAP, consensus)
Tolérance aux pannes (si bien conçu) Latence réseau entre nœuds
Coût lineaire (commodity hardware) Partitionnement des données
Upgrades sans interruption (rolling) Opérations cross-partition coûteuses

Quand choisir quoi

graph TD
    Q1{"Charge actuelle<br>< 50% capacite<br>d'un serveur ?"} --oui--> SCALE_UP["Scale-up<br>Plus simple, moins cher"]
    Q1 --non--> Q2{"Donnees<br>partitionnables ?"}
    Q2 --oui--> SCALE_OUT["Scale-out<br>Shared-nothing"]
    Q2 --non--> Q3{"Forte coherence<br>requise ?"}
    Q3 --oui--> SCALE_UP2["Scale-up<br>(ou shared-disk)"]
    Q3 --non--> SCALE_OUT2["Scale-out<br>avec eventual consistency"]

Tip

La règle pragmatique : commencez par scale-up tant que c'est viable. Un seul serveur PostgreSQL avec 64 cœurs, 512 GB de RAM et des NVMe peut gérer une charge surprenante — des dizaines de milliers de requêtes par seconde pour des lectures, des milliers pour des écritures. La complexité du distribué n'est justifiee que quand on a atteint les limites du vertical scaling ou qu'on a des exigences de disponibilité qui imposent la redondance.


Capacity planning : calcul de coin de table

Le back-of-the-envelope calculation est l'outil de l'architecte pour dimensionner un système avant d'écrire une ligne de code. L'objectif n'est pas la précision, mais l'ordre de grandeur.

Méthode

  1. Estimer le débit cible (requêtes par seconde, volume de données par jour)
  2. Estimer la latence par requête (combien de temps chaque opération prend)
  3. Appliquer la loi de Little pour déterminer le parallélisme nécessaire
  4. Multiplier par un facteur de sécurité (2x a 3x pour absorber les pics)
  5. Vérifier que le stockage, la bande passante et la mémoire tiennent

Règles de conversion rapides

Quantite Approximation
1 million de secondes ~11.5 jours
1 milliard de secondes ~31.7 ans
86 400 secondes 1 jour
2.5 million de secondes 1 mois
1 KB/s pendant 1 jour 86 MB
1 MB/s pendant 1 jour 86 GB
1 GB/s pendant 1 jour 86 TB

Cas pratique : dimensionner un système a 10 000 req/s

On concoit un service API qui doit soutenir 10 000 requêtes par seconde en regime nominal.

Étape 1 : profil de la requête

Chaque requête :

  • Lecture en cache Redis : 0.5 ms
  • Si cache miss (20% des cas) : lecture PostgreSQL : 5 ms
  • Serialisation JSON de la réponse : 0.1 ms
  • Temps moyen par requête : 0.8 * 0.6 ms + 0.2 * 5.1 ms = 1.5 ms

Étape 2 : parallélisme (loi de Little)

L = 10 000 * 0.0015 = 15 requetes simultanees

En théorie, 15 threads suffisent. En pratique, on applique un facteur 3x pour les pics et les variations :

Threads necessaires = 15 * 3 = 45 threads

Étape 3 : ressources CPU

Un thread actif consomme un cœur CPU pendant le traitement. Avec 45 threads actifs, il faut au moins 45 vCPU... mais chaque thread passe la majorité de son temps en attente I/O (le calcul pur ne représenté que ~10% du temps). Donc :

Coeurs CPU necessaires = 45 * 0.10 = ~5 coeurs

Un serveur avec 8 cœurs physiques (16 hyper-threads) suffit largement.

Étape 4 : mémoire

  • Pool de connexions Redis : 20 connexions x 1 MB buffer = 20 MB
  • Pool de connexions PostgreSQL : 50 connexions x 2 MB = 100 MB
  • Cache applicatif local (hot data) : 2 GB
  • Overhead JVM/runtime : 1 GB
  • Total : ~4 GB

Un serveur avec 8-16 GB de RAM est confortable.

Étape 5 : bande passante réseau

  • Requête entrante : ~1 KB
  • Réponse : ~5 KB
  • Total par requête : ~6 KB
  • Débit : 10 000 * 6 KB = 60 MB/s = 480 Mbps

Une interface 1 Gbps suffit. Avec 10 Gbps (standard en datacenter), on a une marge de 20x.

Étape 6 : stockage

Avec 80% de cache hits, PostgreSQL reçoit 2 000 lectures/s. Un SSD NVMe géré facilement 100 000+ IOPS aléatoires — le stockage n'est pas le goulot.

Verdict

Un seul serveur avec 8 cœurs, 16 GB de RAM et un SSD NVMe soutient 10 000 req/s. Pour la haute disponibilité, on deploie 3 instances derriere un load balancer — chacune dimensionnee pour absorber la charge totale en cas de panne d'une instance (N+1 redundancy avec N=2).

Note

Ce calcul de coin de table ne remplacé pas un test de charge. Il sert à valider que l'ordre de grandeur est realiste avant d'investir du temps dans l'implémentation. Si le calcul donne 500 serveurs pour 10 000 req/s, il y a un problème d'architecture avant même de commencer a coder.


Synthèse des contraintes matérielles

Contrainte matérielle Conséquence architecturale
Latence mémoire (100 ns) vs réseau (500 us) Minimiser les sauts réseau, privilegier le cache local
Pipeline CPU et branch prédiction Code prévisible, éviter les branchements aléatoires dans les hot paths
Cache lines (64 octets) et false sharing Structures de données alignees, partitionnement par thread
NUMA et localité mémoire Affinite processus/mémoire, surtout pour les bases de données
Loi d'Amdahl Identifier et réduire la fraction séquentielle avant de scaler
Overhead I/O de la virtualisation Choisir virtio/SR-IOV, dimensionner avec 20-30% de marge
Bande passante SSD vs HDD NVMe pour les bases de données, HDD pour l'archivage

Les erreurs classiques

  1. Scaler horizontalement avant d'optimiser : ajouter des serveurs ne résout pas un problème de requête SQL non indexee qui prend 200 ms

  2. Ignorer le réseau : un appel inter-service ajoute 500 us minimum. Dix appels en serie dans le chemin critique = 5 ms de latence incompressible

  3. Sur-dimensionner la mémoire, sous-dimensionner l'I/O : 256 GB de RAM ne servent à rien si le disque est un HDD a 100 IOPS

  4. Confondre vCPU et cœur physique : en cloud, un vCPU est souvent un hyper-thread (50% d'un cœur). Les benchmarks bare-metal ne sont pas transposables directement

  5. Oublier le GC : les pauses GC de la JVM ou du runtime Go sont de la fraction séquentielle pure. Un heap de 32 GB avec un GC mal configuré peut causer des pauses de 500 ms


Chapitre suivant : Fondations réseau — protocoles, routage et interconnexions.