Aller au contenu principal

MapReduce : découper le calcul

Ce que ce chapitre apporte 5 points
  • Écrire un calcul sous forme map puis reduce, et savoir ce que chaque étape produit.
  • Distinguer une opération qui se recolle d'une opération qui ne se recolle pas.
  • Transporter l'état suffisant pour reconstituer une moyenne, et savoir pourquoi il en faut deux morceaux.
  • Mesurer et réduire le volume déplacé lors du mélange.
  • Reconnaître un déséquilibre de charge et remonter à la clé qui le provoque.

MapReduce découpe un calcul en deux temps : une fonction map appliquée partout, une fonction reduce qui rassemble. Une seule machine ne suffit plus : on coupe les données, chaque machine calcule sur son morceau, et l'on rassemble. Dit ainsi, c'est une évidence. En pratique, deux choses seulement rendent l'exercice difficile, et ce sont les deux seules qui méritent d'être apprises. La moyenne de deux moyennes n'est pas la moyenne. Et les morceaux ne sont jamais de la même taille.

Le chapitre précédent a montré comment traiter des données qui ne tiennent pas en mémoire, sur une seule machine, en une seule lecture. Cette solution atteint sa limite quand la lecture elle-même devient trop longue : lire un pétaoctet à 200 mégaoctets par seconde demande deux mois, et aucun algorithme en une passe n'y changera rien.

La seule issue est alors de lire à plusieurs endroits en même temps. C'est ce que fait le motif présenté ici, qui porte le nom de ses deux étapes, map et reduce, et qui structure la quasi-totalité des systèmes de traitement réparti, quel que soit leur habillage.

Le motif, en quatre temps

Avant les trois étapes vient la découpe, et sa règle découle d'un ordre de grandeur. Rassembler 10 téraoctets sur une seule machine de calcul, par un réseau à 1 gigabit par seconde, revient à faire passer 8×10138 \times 10^{13} bits à 10910^9 bits par seconde : 80 000 secondes, environ 22 heures, avant le premier calcul. Le programme de traitement, lui, tient en quelques kilooctets et rejoint chaque machine en moins d'une milliseconde. Le motif envoie donc le calcul là où se trouvent les données, et non l'inverse.

Or des données de ce volume sont déjà réparties : un système de fichiers distribué a coupé le fichier en blocs de taille fixe, posés sur de nombreuses machines. La découpe initiale suit ces blocs, et chaque machine traite, autant que possible, ceux que porte son propre disque. Le réseau, dont le coût vient d'être chiffré, ne sert alors presque plus qu'au mélange, ce qui explique la place que ce chapitre lui donne. Le chapitre suivant, Partitionnement et réplication des données, décrit ces blocs et le catalogue qui sait où ils se trouvent.

Map, mélange, reduce

Map transforme chaque élément, indépendamment des autres, en zéro, un ou plusieurs couples clé-valeur. C'est l'étape qui se parallélise sans effort, puisque aucune machine n'a besoin de savoir ce que font les autres.

Le mélange regroupe tous les couples qui portent la même clé sur une même machine. C'est l'étape qui fait traverser le réseau à tout ce que le map a produit, et c'est donc elle qui coûte : la lecture se fait le plus souvent sur la machine qui porte les données, et le résultat, une ligne par clé, pèse en général peu.

Reduce applique une opération à toutes les valeurs d'une même clé, et produit un résultat par clé.

Le comptage de mots est l'exemple canonique du domaine, et il le mérite : il tient en trois lignes et contient tout le motif.

somme · par tourniquet

Les données sont coupées en morceaux, un par machine.

machine 1

  • le chat dort sur le mur

machine 2

  • le chien dort sous le banc

machine 3

  • le chat court après le chien
Compter les mots d'un texte sur trois machines

Les quatre phases se parcourent avec les boutons. La découpe répartit les lignes, sans regarder ce qu'elles contiennent. Le map produit un couple par mot, (mot, 1), sur chaque machine indépendamment. Le mélange rassemble par mot, et l'on voit alors les couleurs se mêler : ce qui vient de la machine 1 rejoint ce qui vient de la machine 3. La réduction additionne.

Le map ne sait rien, et c'est ce qui le rend parallélisable. Le traitement d'une ligne par le map ne dépend d'aucune autre ligne. C'est cette propriété, et elle seule, qui autorise à découper n'importe comment et à traiter en parallèle sans coordination.

Toute logique qui exige de connaître les autres lignes doit donc être repoussée dans le reduce, après le regroupement par clé. Un map qui a besoin d'un compteur global n'est pas un map.

Écrit à la main, le motif ne fait qu'une vingtaine de lignes de Python. Rien n'y est mystérieux, et le voir en entier vaut mieux que de faire confiance à un moteur.

main.py
Sortie
>_ Prêt à exécuter…
Vérification rapideon peut se reprendre

Pourquoi l'étape map se parallélise-t-elle sans coordination ?

Le mélange est ce qui coûte

Dans la figure ci-dessus, dix-huit couples traversent le réseau pour produire dix résultats. Sur un texte réel, c'est un couple par mot du corpus : des milliards de couples de la forme (le, 1), tous identiques, tous transportés séparément.

L'idée qui corrige cela est simple : réduire d'abord localement, sur chaque machine, avant d'envoyer.

Combineur

Un combineur est une réduction appliquée localement, sur chaque morceau, avant le mélange. Il ne change pas le résultat, il change ce qui circule.

Une machine qui a vu deux cents fois le mot le envoie (le, 200) au lieu de deux cents couples (le, 1). Le résultat final est identique, le volume transporté est divisé par deux cents.

somme · par tourniquet · combineur local

Les données sont coupées en morceaux, un par machine.

machine 1

  • le chat dort sur le mur

machine 2

  • le chien dort sous le banc

machine 3

  • le chat court après le chien
Le même comptage, avec réduction locale avant l'envoi

La phase de mélange annonce maintenant quinze couples au lieu de dix-huit : chaque machine envoie un seul (le, 2) au lieu de deux (le, 1), et rien d'autre ne se répète dans une même phrase. Sur trois phrases le gain est anecdotique ; sur un corpus, où le revient des milliers de fois par morceau, il fait la différence entre un traitement qui tient et un traitement qui sature le réseau.

Un combineur n'est pas une optimisation gratuite

Il n'est légitime que si la réduction admet d'être appliquée en deux temps sans changer le résultat. Pour une somme ou un maximum, c'est vrai. Pour une moyenne, c'est faux, et la section suivante montre à quel point.

Un moteur de traitement réparti applique le combineur quand il le juge utile, sans prévenir, et parfois pas du tout. Un calcul dont le résultat dépend de l'application ou non du combineur est donc un calcul faux, dont le résultat varie d'une exécution à l'autre sans que rien ne le signale.

Vérification rapideon peut se reprendre

Quelle étape fait circuler le plus de données sur le réseau ?

Ce qui se recolle, et ce qui ne se recolle pas

Opération associative et commutative

Une opération est associative quand l'ordre des regroupements ne change pas le résultat : (a ⊕ b) ⊕ c vaut a ⊕ (b ⊕ c). Elle est commutative quand l'ordre des termes ne compte pas : a ⊕ b vaut b ⊕ a.

Une réduction se calcule par morceaux quand elle a les deux propriétés : les morceaux se regroupent n'importe comment, et leurs résultats arrivent dans n'importe quel ordre. La somme, le produit, le minimum, le maximum et le comptage les ont. Une somme de sommes partielles est une somme ; un maximum de maximums partiels est un maximum. La concaténation de textes est associative sans être commutative : un découpage en tourniquet envoie la première et la quatrième ligne sur la même machine, et le texte recollé sortirait dans le désordre.

La moyenne, commutative, n'est pas associative.

L'affirmation se vérifie sur quatre valeurs.

moyenne · par tourniquet · combineur naïf

Les données sont coupées en morceaux, un par machine.

machine 1

  • maths 12
  • maths 18

machine 2

  • maths 12
  • info 10

sur le réseau

2 couples

sur 3 envoyés au mélange

réduction la plus chargée

2 couples reçus

machine 2, sur 3 en tout

résultat

faux pour 1 clé

maths : 13,5 au lieu de 14, le calcul sans découpage

couples reçus par chaque machine de réduction

Moyenne par matière, avec un combineur écrit trop vite

La machine 1 a reçu 12 et 18, et envoie leur moyenne, 15. La machine 2 a reçu 12, et envoie 12. Le réducteur fait la moyenne de 15 et de 12, soit 13,5. La bonne réponse est 14, et la figure l'affiche à côté.

L'erreur n'est pas dans l'arithmétique : elle est dans ce qui a été transporté. Une moyenne résume deux informations, un total et un effectif, et n'en envoyer qu'une seule revient à traiter un morceau de deux valeurs comme un morceau d'une seule.

Transporter de quoi refaire le calcul, et ne diviser qu'à la fin. Le combineur juste envoie le couple (somme, effectif). Le réducteur additionne les sommes, additionne les effectifs, et divise une seule fois, à la fin.

Ce raisonnement se généralise : une réduction non associative devient calculable par morceaux dès qu'on transporte un état intermédiaire qui, lui, se combine associativement et dans n'importe quel ordre.

Pour la variance, cet état comporte trois nombres. Les accumulateurs du chapitre précédent s'additionnent composante par composante, mais ils gardent leur annulation catastrophique. L'état de Welford, le compte, la moyenne et la somme des écarts au carré, se recolle aussi, par une formule à peine plus longue : avec n = na + nb et d = mb - ma, la moyenne vaut ma + d * nb / n, et M2 = M2a + M2b + d * d * na * nb / n. Pour un écart type, les mêmes. Pour une médiane, aucun état de taille fixe ne convient, ce qui est une autre façon de dire qu'elle n'est pas décomposable.

À manipuler
Dans la figure de la moyenne, plus haut, choisir l'opération « moyenne, par (somme, effectif) » : maths passe à 14, le résultat du calcul sans découpage.
Revenir aux moyennes locales et retirer le combineur : 14 aussi. Les deux moyennes ne diffèrent que par ce que le combineur envoie, et un moteur qui l'applique ou non selon sa charge rend tantôt 13,5, tantôt 14.
L'erreur ne se voit pas toujours, et c'est ce qui la rend dangereuse

Si les deux morceaux contiennent le même nombre de valeurs, la moyenne des moyennes donne la bonne réponse. Le calcul faux passe donc les essais sur des données bien réparties, et ne se met à mentir qu'en production, quand la répartition devient inégale.

Pire, l'écart est petit et vraisemblable : 13,5 au lieu de 14 ne choque personne et ne déclenche aucune alerte. C'est la signature des erreurs les plus coûteuses, celles qu'aucun symptôme ne trahit.

La parade tient en une question à poser sur chaque agrégat : puis-je le calculer sur deux moitiés et recoller le résultat ? Si la réponse est non, il faut transporter davantage.

Le même piège, écrit en Python, sur des données dont le déséquilibre est explicite.

main.py
Sortie
>_ Prêt à exécuter…
Vérification rapideon peut se reprendre

Un combineur qui envoie la moyenne locale de chaque morceau…

Les morceaux ne sont pas égaux

Le mélange impose que toutes les valeurs d'une clé finissent au même endroit. Quand les données sont déjà rangées par clé, comme une table partitionnée par ville, aucun mélange n'est nécessaire, ce qui évite de tout transporter. Cette économie a un prix, et une clé plus fréquente que les autres en fixe un second. La figure suivante tire trente ventes de trois villes, à graine fixe, et se règle : nombre de machines, combineur, répartition des lignes, poids des clés. Son bilan compte les couples qui traversent le réseau et ceux que reçoit la machine de réduction la plus chargée.

somme · par tourniquet

Les données sont coupées en morceaux, un par machine.

machine 1

  • paris 57
  • paris 20
  • nice 26
  • lille 99
  • nice 63
  • lille 37
  • nice 39
  • paris 62

et 2 de plus, 10 en tout

machine 2

  • lille 53
  • nice 24
  • paris 12
  • paris 50
  • nice 86
  • lille 82
  • lille 74
  • nice 88

et 2 de plus, 10 en tout

machine 3

  • paris 26
  • nice 65
  • nice 77
  • nice 93
  • paris 81
  • lille 93
  • paris 83
  • lille 84

et 2 de plus, 10 en tout

sur le réseau

20 couples

sur 30 envoyés au mélange

réduction la plus chargée

10 couples reçus

machine 1, sur 30 en tout

résultat

juste

le calcul sans découpage donne le même, pour les 3 clés

couples reçus par chaque machine de réduction

Des ventes par ville, sur des machines que l'on règle
À manipuler
Passer les clés sur « une dominante » : la clé paris porte alors 24 lignes sur 30, et sa machine de réduction reçoit 24 couples, contre 3 pour chacune des deux autres.
Porter les machines à 6 : la machine 4 reçoit les mêmes 24 couples, et trois machines n'en reçoivent aucun. Ajouter des machines n'a rien changé.
Passer le combineur sur « avec » : la plus chargée ne reçoit plus que 6 couples, et 9 traversent le réseau au lieu de 25.
Revenir à 3 machines, sans combineur, et répartir « par clé » : plus aucun couple ne traverse le réseau, mais la découpe donne 24 lignes à la machine 1, 2,4 fois sa part.

Quand une clé domine, une machine porte presque tout. Le calcul dure ce que dure la plus chargée : ajouter des machines n'y change rien, puisque la clé paris ne se coupe pas en deux.

Ce déséquilibre tient aussi à la façon dont les données sont rangées. Découpées sans regarder la clé, comme un fichier coupé en blocs, et réduites localement avant l'envoi, les ventes se répartissent d'elles-mêmes : chaque machine somme ses propres ventes de paris, et la réduction ne reçoit plus qu'un total par machine, trois en tout à trois machines. C'est ce que fait d'office reduceByKey dans Spark. Le remède ne vaut que pour une réduction qui se recolle : une médiane par ville garde tout son déséquilibre.

Déséquilibre de charge

Il y a déséquilibre quand la répartition par clé concentre une part disproportionnée des données sur une minorité de machines.

Le temps total d'un traitement réparti est celui de la machine la plus lente. Un déséquilibre de facteur dix annule donc le bénéfice de dix machines, et l'ajout de machines supplémentaires n'apporte plus rien.

À l'échelle d'une seule machine, le même raisonnement vaut pour des processus qui se partagent un calcul : celui qui reçoit le plus gros morceau fait attendre les autres. Le choix entre fils et processus obéit, lui, à une autre règle, que pose le chapitre Mémoire isolée ou partagée du parcours Systèmes d'exploitation.

Les données réelles sont presque toujours déséquilibrées

La répartition uniforme est l'exception, pas la règle. Les ventes se concentrent sur quelques produits, le trafic sur quelques pages, les connexions sur quelques heures, les messages sur quelques comptes. Une clé naturelle produit donc presque toujours un déséquilibre.

Quatre parades, dans l'ordre de préférence :

Réduire localement quand la réduction se recolle : un combineur fait de la clé dominante un total par machine.

Choisir une autre clé, plus fine, quand le calcul le permet. Compter par (ville, jour) plutôt que par ville multiplie le nombre de clés et disperse la charge.

Décomposer la clé dominante en ajoutant un suffixe tiré au hasard, réduire, puis réduire une seconde fois sur la clé nettoyée. Cela ne fonctionne que si la réduction est associative et commutative, ce qui ramène à la section précédente.

Traiter la clé dominante à part, avec un calcul spécifique. C'est la solution la moins élégante et souvent la plus rapide à mettre en place.

Vérification rapideon peut se reprendre

1.Quelle propriété rend une réduction calculable par morceaux, quand les résultats partiels arrivent dans n'importe quel ordre ?

2.Un traitement sur 40 machines dure 50 minutes, dont 39 finissent en 3 minutes. Relancée sur une autre machine, la tâche lente dure encore 47 minutes. La cause la plus probable ?

Où sont Hadoop et Spark

Le motif présenté ici est antérieur aux outils qui l'ont popularisé, et leur survivra. Il est utile de savoir ce que ces noms recouvrent, à condition de ne pas confondre le motif avec ses mises en œuvre.

Hadoop désigne un ensemble, dont un système de fichiers réparti qui découpe les fichiers en blocs répliqués sur plusieurs machines, et un moteur qui exécute des tâches map et reduce sur ces blocs, en écrivant sur disque entre chaque étape. Cette écriture systématique le rend robuste et lent.

Spark exécute le même motif en enchaînant sans écrire les étapes qui n'exigent pas de mélange, et en gardant en mémoire les résultats qu'on lui demande de garder, ce qui change les ordres de grandeur sur les traitements enchaînés. Le mélange, lui, passe par des fichiers intermédiaires sur disque, comme chez Hadoop. Son interface range ses opérations en transformations, dont map, et en actions, dont reduce : la mécanique du dessous est celle de ce chapitre, mélange compris.

Ce que ces outils ne dispensent pas de savoir. Aucun moteur ne détecte qu'une moyenne a été recollée à tort : il exécute ce qu'on lui décrit et rend un résultat vraisemblable.

Aucun moteur ne répare seul un déséquilibre qui tient au calcul demandé. Certains en absorbent une partie : depuis sa version 3.2, Spark découpe d'office une jointure dont une clé déborde, et une réduction qui se recolle passe par une réduction locale qui allège la clé lourde. Une médiane par clé, un décompte de distincts ou une clé fautive, inconnu ou null, restent à la charge du concepteur.

Aucun moteur ne devine que le calcul demandé aurait pu tenir sur une seule machine en une passe. Un traitement réparti sur des données de quelques gigaoctets est souvent plus lent que la boucle du chapitre précédent, à cause du temps de mise en route et du mélange.

Vérification rapideon peut se reprendre

Que ne fait aucun moteur de traitement réparti à la place du concepteur ?

Mettre en pratique

Le mélange est l'étape qui coûte, parce qu'elle fait traverser le réseau. La réduire est affaire de comptage avant d'être affaire de réglage.

Chiffrer le mélange

  • 1.

    Une étape map produit un million de lignes de 50 octets chacune. Quel volume, en mégaoctets, le mélange doit-il transporter ?

  • 2.

    Ce map tourne sur 10 machines, et le combineur de chacune regroupe ses lignes en au plus 1 000 clés, chacune portant deux nombres de 8 octets. Quel volume, en kilooctets, reste-t-il au plus à transporter, taille des clés mise à part ?

  • 3.

    Combien de nombres faut-il transporter par clé pour pouvoir recoller une moyenne ?

  • 4.

    Une clé porte à elle seule 60 % des lignes, réparties sans combineur sur 10 réducteurs. Quel pourcentage du travail total le réducteur le plus chargé traite-t-il ?

La dernière réponse est la définition même du déséquilibre. Ajouter des réducteurs ne change rien tant qu'une seule clé concentre la charge, parce que le temps total est celui du réducteur le plus lent. La solution n'est pas d'ajouter des machines : elle est de réduire localement quand la réduction le permet, sinon de changer la clé.

Exprimer sous forme map et reduce le calcul du chiffre d'affaires total par magasin, à partir de lignes (magasin, produit, montant).

Le map émet (magasin, montant) pour chaque ligne, en ignorant le produit. Le reduce somme les montants d'une même clé.

La somme étant associative et commutative, un combineur local est légitime et divise le volume transporté par le nombre moyen de lignes par magasin et par morceau.

La clé est le magasin : s'il y en a quelques centaines et que le trafic est comparable, la répartition sera acceptable. S'il existe un magasin en ligne qui pèse la moitié du chiffre d'affaires, le déséquilibre est à prévoir.

Même énoncé, mais on veut le montant moyen d'une ligne de vente par magasin. Que doit transporter le combineur ?

Le couple (somme des montants, nombre de lignes).

Le reduce additionne les deux composantes séparément, puis divise une fois. Envoyer directement la moyenne locale donnerait la moyenne des moyennes, fausse dès que les morceaux n'ont pas le même nombre de lignes, c'est-à-dire presque toujours.

Le test à retenir : un état intermédiaire est valable si l'on peut le combiner associativement, dans n'importe quel ordre. (somme, effectif) se combine composante par composante ; une moyenne seule, non.

Ce n'est pas le panier moyen, qui divise par le nombre de tickets et non de lignes. Il faudrait un numéro de ticket, et compter des tickets distincts ramène au problème des valeurs distinctes, sauf si chaque ticket tombe tout entier dans un seul morceau.

Un traitement doit donner, par magasin, le montant médian. Que faire ?

La médiane n'est pas décomposable : aucun état de taille fixe ne permet de la recoller, et le motif ne s'applique donc pas directement.

Trois issues. Si les données d'un magasin tiennent en mémoire sur une machine, répartir par magasin et calculer la médiane exacte dans le reduce, qui voit alors toutes les valeurs de sa clé. C'est la solution la plus simple, et elle échoue sur le magasin dominant.

Sinon, transporter un histogramme des montants par tranches, qui se combine associativement en additionnant les effectifs des tranches, et lire la médiane dans l'histogramme cumulé. Le résultat est approché, à la largeur de tranche près, et cette erreur est connue et bornée.

Sinon, employer un algorithme de quantiles approchés, conçu pour cela.

Un traitement sur 40 machines dure 50 minutes. Les journaux montrent que 39 machines ont fini en 3 minutes. Que conclure, et que tenter ?

Une clé concentre probablement l'essentiel des données, ce qui reste à vérifier : une machine malade, un disque qui faiblit par exemple, produit le même relevé. Relancer la tâche lente sur une autre machine tranche, et les moteurs le font d'eux-mêmes en fin de calcul : si elle reste lente, ce sont ses données qui pèsent.

Si c'est bien une clé, ajouter des machines ne changera rien, puisque la clé lourde reste indivisible. Le temps resterait de 50 minutes sur 400 machines.

Les pistes utiles : identifier la clé fautive en comptant les lignes par clé, ce qui est lui-même un map et reduce ; puis réduire localement si la réduction se recolle, choisir une clé plus fine, décomposer la clé lourde par un suffixe aléatoire suivi d'une seconde réduction, ou traiter cette clé séparément.

Il arrive que la clé fautive soit une valeur par défaut : un inconnu, un null converti en chaîne, un identifiant à zéro. Dans ce cas, le déséquilibre est un symptôme de la qualité des données et non de leur distribution réelle.

Chapitre révisé en octobre 2026 · Une erreur dans ce chapitre ?

Dans la bibliothèque d'exercices