Architectures des systèmes de données
BGD701 — Télécom Paris
La bonne question n’est pas « est-ce scalable ? » mais « si la charge est multipliée par 10, que faut-il changer ? »
| Opérationnel (OLTP) | Analytique (OLAP) | |
|---|---|---|
| Question | « adresse du client 4712 ? » | « chiffre d’affaires par région ? » |
| Accès | quelques lignes, par clé | des millions de lignes |
| Critère | latence faible | débit élevé |
| Exemple | PostgreSQL, MySQL | entrepôt, Hadoop, Spark |
MapReduce, que nous écrirons en séances 6 et 7, appartient au monde analytique.
C’est ce que font Parquet et ORC, partout dans Hadoop et Spark.
Un train : excellent débit, latence médiocre. Améliorer l’un peut dégrader l’autre.
999 requêtes à 10 ms, une à 5 secondes :
Le tableau de bord est vert. Un utilisateur sur mille attend 5 secondes.
Une page appelle 20 services et attend qu’ils aient tous répondu.
Chacun est lent 1 fois sur 100. La page est lente avec la probabilité :
Un défaut rare pour une requête devient fréquent pour l’utilisateur. Mesuré en exercice 5.1 : 2 % de requêtes lentes, une page sur trois lente.
PostgreSQL, MySQL, MongoDB, Kafka : c’est de loin le modèle le plus répandu.
Vous publiez un commentaire (écriture → leader). Vous rechargez la page (lecture → un suiveur en retard). Votre commentaire a disparu.
Ce n’est pas un bug
C’est la conséquence d’un choix d’architecture. Parade classique : read-your-writes, lire sur le leader quelques secondes après avoir écrit.
Il faut en élire un autre : le basculement (failover).
Trois machines : A (leader), B, C.
Il reste un exemplaire en stock. A le vend à Alice, B le vend à Bob.
On ne peut pas empêcher les coupures. On peut empêcher deux leaders d’agir en même temps.
Un leader n’agit que s’il est soutenu par plus de la moitié des machines.
Il ne peut plus y avoir deux leaders actifs. Prix : le côté minoritaire cesse de répondre.
| Machines | Majorité | Pannes tolérées |
|---|---|---|
| 3 | 2 | 1 |
| 4 | 3 | 1 |
| 5 | 3 | 2 |
| 7 | 4 | 3 |
4 machines ne tolèrent pas plus de pannes que 3. Et coupées en 2 + 2, personne n’a la majorité.
A était leader du mandat 7, B est élu au mandat 8. Quand A revient et écrit avec le 7, tout le monde le rejette.
Le bail (lease) : le leader ne l’est que 10 secondes, et doit renouveler son bail auprès de la majorité. Isolé, il s’arrête de lui-même. Suppose des horloges à peu près fiables : on le combine avec le mandat.
Abattre l’ancien leader : lui couper l’alimentation à distance avant de promouvoir le nouveau. Nom volontairement brutal : STONITH, Shoot The Other Node In The Head.
On ne réécrit pas cela à la main : on le délègue.
10 To de données, des disques de 1 To : aucune copie ne tient. Il faut découper. Les vrais systèmes font les deux : chaque partition est répliquée.
Des mesures partitionnées par date : toutes les écritures du jour sur la même machine. C’est un point chaud.
Jamais hash() de Python
Il est salé au hasard à chaque démarrage : deux machines calculeraient des partitions différentes pour la même clé. hashlib ou zlib.crc32.
10 000 clés, on passe de 10 à 11 machines (exercice 5.3, mesuré) :
| Méthode | Clés déplacées |
|---|---|
hash(cle) % N |
90 % |
| 110 partitions fixes, une table partition → machine | 10 % |
| Minimum inévitable : la part de la nouvelle machine | 1/11 ≈ 9 % |
D’où le nombre de partitions fixé à la création dans Kafka et Elasticsearch.
Même avec un hachage parfait, une clé très demandée sature une machine : le compte d’une célébrité.
Le hachage répartit les clés, pas la charge par clé.
import time
INTERVALLE = 2.0 # un signal attendu toutes les 2 s
TOLERANCE = 6.0 # mort si silence pendant 6 s
derniers_signaux = {} # machine -> instant du dernier signal
def signal_recu(machine):
derniers_signaux[machine] = time.perf_counter()
def machines_mortes():
maintenant = time.perf_counter()
return [m for m, t in derniers_signaux.items()
if maintenant - t > TOLERANCE]| Délai court | Délai long | |
|---|---|---|
| Détection | rapide | lente |
| Risque | fausses alertes : une machine lente déclarée morte | une machine morte gardée longtemps |
| Conséquence | travail refait, split brain | requêtes perdues, attente |
Exercice 5.4, mesuré : tolérance 3 s → 7 à 8 fausses alertes par minute ; 15 s → 0.
Un résultat démontré
À distance, rien ne distingue une machine morte d’une machine très lente ou d’un réseau coupé.
On choisit un compromis, et on accepte de se tromper parfois.
127.0.0.1, elles sont presque vraies : rien ne se perd, tout répond en 0,1 ms, rien ne changeUn programme testé seulement en local peut reposer sur les huit. Il échoue le jour où il tourne vraiment sur plusieurs machines.
| On croit… | En réalité | Parade |
|---|---|---|
| le réseau est fiable | messages perdus, machines injoignables | settimeout partout |
| la latence est nulle | 1000 requêtes × 100 ms = 100 s | regrouper, paralléliser |
| la bande passante est infinie | 1 To à 1 Gbit/s : plus de 2 h | amener le calcul aux données |
| le réseau est sûr | messages lus, modifiés, forgés | chiffrer, valider chaque entrée |
| On croit… | En réalité | Parade |
|---|---|---|
| la topologie ne change pas | machines ajoutées, retirées, déplacées | un annuaire, pas d’adresses en dur |
| il y a un seul administrateur | plusieurs équipes, plusieurs pare-feux | l’inventaire marche sur campus-telecom, pas sur eduroam |
| le transport ne coûte rien | sérialisation, sortie du cloud facturée | mesurer, choisir le format |
| le réseau est homogène | OS, versions, ordre des octets différents | ! de struct, UTF-8 imposé |
Avant de déclarer un programme réseau terminé, relisez les huit et demandez-vous pour chacune : que se passe-t-il si celle-ci est fausse ?
time.time() |
time.perf_counter() |
|
|---|---|---|
| Donne | une date | un temps écoulé |
| Peut reculer | oui (NTP) | jamais |
| Sert à | dater | chronométrer |
Une durée mesurée avec time.time() peut être négative.
Pour ordonner, une source unique : un compteur du maître, un numéro de séquence. En séance 7 : id_map.
Une donnée répliquée sur plusieurs machines.
Rien à voir avec le partitionnement
Ici, une partition est une panne du réseau, pas un découpage des données.
Le stock est sur deux machines, Paris et Lyon. Il reste un livre. Le lien Paris–Lyon est coupé.
Un client lyonnais veut l’acheter. Lyon ne peut pas savoir si Paris vient de le vendre.
| Refuser | Accepter | |
|---|---|---|
| Le client voit | « indisponible » | « commande confirmée » |
| On perd | la disponibilité | la cohérence (vendu deux fois ?) |
| Ensuite | rien à réparer | réconcilier, s’excuser |
| Nom | CP | AP |
Réseau normal : cohérence et disponibilité. Pendant une coupure, et seulement là : il faut sacrifier l’une des deux.
On choisit selon le coût d’une erreur.
Un même site combine les deux : panier AP, paiement et stock CP.
Pour lire : demander au NameNode où sont les blocs, puis les lire directement sur les DataNodes.
Le dilemme du délai : recopier un disque plein coûte cher, donc HDFS attend longtemps. Le NameNode est doublé, et ZooKeeper désigne l’actif pour éviter le split brain.
commandes, clics)hash(cle) % N. L’ordre n’est garanti qu’à l’intérieur d’une partitionC’est la réponse de Kafka à « quel suiveur choisir ? » : un suiveur qui a tous les messages confirmés.
Le champ qui décide sur quelle machine va chaque donnée. Très difficile à changer après coup.
| Clé | Verdict |
|---|---|
| Identifiant de client ou de commande | bonne : nombreuses, uniformes |
| Date du jour | mauvaise : point chaud du jour |
| Pays | mauvaise : trop peu de valeurs, une domine |
| Identifiant de vendeur | à surveiller : un géant sature sa partition |
Clé trop demandée : vendeur42_0 … vendeur42_9. Les écritures se répartissent ; pour tout lire, il faut interroger les dix.
hashlib, jamais hash() ; hash % N déplace presque touttime.time() pour dater, perf_counter() pour chronométrer