Lié à ma question CouchDB .
Quelqu'un peut-il expliquer MapReduce en des termes qu'un numbnuts pourrait comprendre?
Lié à ma question CouchDB .
Quelqu'un peut-il expliquer MapReduce en des termes qu'un numbnuts pourrait comprendre?
Réponses:
Pour aller jusqu'aux bases de Map and Reduce.
Map est une fonction qui "transforme" les éléments d'une sorte de liste en un autre type d'élément et les remet dans le même type de liste.
supposons que j'ai une liste de nombres: [1,2,3] et que je veux doubler chaque nombre, dans ce cas, la fonction pour "doubler chaque nombre" est la fonction x = x * 2. Et sans mappages, je pourrais écrire une simple boucle, disons
A = [1, 2, 3]
foreach (item in A) A[item] = A[item] * 2
et j'aurais A = [2, 4, 6] mais au lieu d'écrire des boucles, si j'ai une fonction de carte, je pourrais écrire
A = [1, 2, 3].Map(x => x * 2)
x => x * 2 est une fonction à exécuter sur les éléments de [1,2,3]. Ce qui se passe, c'est que le programme prend chaque élément, exécute (x => x * 2) contre lui en faisant x égal à chaque élément, et produit une liste des résultats.
1 : 1 => 1 * 2 : 2
2 : 2 => 2 * 2 : 4
3 : 3 => 3 * 2 : 6
donc après avoir exécuté la fonction de carte avec (x => x * 2) vous auriez [2, 4, 6].
Réduire est une fonction qui "rassemble" les éléments dans les listes et effectue des calculs sur chacun d'eux, les réduisant ainsi à une valeur unique.
La recherche d'une somme ou la recherche de moyennes sont toutes des instances d'une fonction de réduction. Par exemple, si vous avez une liste de nombres, disons [7, 8, 9] et que vous voulez les résumer, vous écririez une boucle comme celle-ci
A = [7, 8, 9]
sum = 0
foreach (item in A) sum = sum + A[item]
Mais, si vous avez accès à une fonction de réduction, vous pouvez l'écrire comme ceci
A = [7, 8, 9]
sum = A.reduce( 0, (x, y) => x + y )
Maintenant, il est un peu déroutant pourquoi il y a 2 arguments (0 et la fonction avec x et y) passés. Pour qu'une fonction de réduction soit utile, elle doit être capable de prendre 2 éléments, de calculer quelque chose et de "réduire" ces 2 éléments à une seule valeur, ainsi le programme pourrait réduire chaque paire jusqu'à ce que nous ayons une seule valeur.
l'exécution suivrait:
result = 0
7 : result = result + 7 = 0 + 7 = 7
8 : result = result + 8 = 7 + 8 = 15
9 : result = result + 9 = 15 + 9 = 24
Mais vous ne voulez pas commencer par des zéros tout le temps, donc le premier argument est là pour vous permettre de spécifier une valeur de départ spécifiquement la valeur de la première result =
ligne.
disons que vous voulez additionner 2 listes, cela pourrait ressembler à ceci:
A = [7, 8, 9]
B = [1, 2, 3]
sum = 0
sum = A.reduce( sum, (x, y) => x + y )
sum = B.reduce( sum, (x, y) => x + y )
ou une version que vous êtes plus susceptible de trouver dans le monde réel:
A = [7, 8, 9]
B = [1, 2, 3]
sum_func = (x, y) => x + y
sum = A.reduce( B.reduce( 0, sum_func ), sum_func )
C'est une bonne chose dans un logiciel de base de données car, avec le support de Map \ Reduce, vous pouvez travailler avec la base de données sans avoir besoin de savoir comment les données sont stockées dans une base de données pour l'utiliser, c'est à quoi sert un moteur de base de données.
Vous avez juste besoin de pouvoir "dire" au moteur ce que vous voulez en lui fournissant une fonction Map ou Réduire, puis le moteur DB pourrait trouver son chemin dans les données, appliquer votre fonction et obtenir les résultats que vous voulez tout sans que vous sachiez comment il boucle sur tous les disques.
Il y a des index et des clés, des jointures et des vues et beaucoup de choses qu'une seule base de données pourrait contenir, donc en vous protégeant contre la façon dont les données sont réellement stockées, votre code est plus facile à écrire et à maintenir.
Il en va de même pour la programmation parallèle, si vous ne spécifiez que ce que vous voulez faire avec les données au lieu d'implémenter réellement le code de bouclage, alors l'infrastructure sous-jacente pourrait "paralléliser" et exécuter votre fonction dans une boucle parallèle simultanée pour vous.
Average()
c'est censé être du givrage par-dessus Sum()
. Mais j'en ai parlé pour illustrer pourquoi la fonction s'appelle "Réduire" ... Une fonction moyenne est quelque chose qui prend une liste de nombres et la réduit à un seul nombre (qui est la moyenne).
MapReduce est une méthode pour traiter de vastes sommes de données en parallèle sans demander au développeur d'écrire un autre code autre que le mappeur et de réduire les fonctions.
La fonction de carte prend des données et produit un résultat, qui est conservé dans une barrière. Cette fonction peut s'exécuter en parallèle avec un grand nombre de la même tâche cartographique . L'ensemble de données peut alors être réduit à une valeur scalaire.
Donc, si vous y pensez comme une instruction SQL
SELECT SUM(salary)
FROM employees
WHERE salary > 1000
GROUP by deptname
Nous pouvons utiliser la carte pour obtenir notre sous-ensemble d'employés avec un salaire> 1000, qui émet vers la barrière dans des compartiments de taille de groupe.
Réduire résume chacun de ces groupes. Vous donnant votre ensemble de résultats.
je viens de tirer ceci de mes notes d'étude universitaires du papier google
L'étape 2 est la carte. L'étape 3 est Réduire.
Par exemple,
La raison pour laquelle MapReduce est divisé entre Map et Reduce est que différentes parties peuvent facilement être réalisées en parallèle. (Surtout si Réduire a certaines propriétés mathématiques.)
Pour une description complexe mais correcte de MapReduce, voir: Modèle de programmation MapReduce de Google - Revisité (PDF) .
MAP et REDUCE sont d'anciennes fonctions Lisp d'une époque où l'homme a tué les derniers dinosaures.
Imaginez que vous ayez une liste de villes avec des informations sur le nom, le nombre de personnes qui y vivent et la taille de la ville:
(defparameter *cities*
'((a :people 100000 :size 200)
(b :people 200000 :size 300)
(c :people 150000 :size 210)))
Maintenant, vous voudrez peut-être trouver la ville avec la densité de population la plus élevée.
Nous créons d'abord une liste de noms de villes et de densité de population à l'aide de MAP:
(map 'list
(lambda (city)
(list (first city)
(/ (getf (rest city) :people)
(getf (rest city) :size))))
*cities*)
=> ((A 500) (B 2000/3) (C 5000/7))
En utilisant REDUCE, nous pouvons maintenant trouver la ville avec la plus grande densité de population.
(reduce (lambda (a b)
(if (> (second a) (second b))
a
b))
'((A 500) (B 2000/3) (C 5000/7)))
=> (C 5000/7)
En combinant les deux parties, nous obtenons le code suivant:
(reduce (lambda (a b)
(if (> (second a) (second b))
a
b))
(map 'list
(lambda (city)
(list (first city)
(/ (getf (rest city) :people)
(getf (rest city) :size))))
*cities*))
Introduisons les fonctions:
(defun density (city)
(list (first city)
(/ (getf (rest city) :people)
(getf (rest city) :size))))
(defun max-density (a b)
(if (> (second a) (second b))
a
b))
Ensuite, nous pouvons écrire notre code MAP REDUCE comme suit:
(reduce 'max-density
(map 'list 'density *cities*))
=> (C 5000/7)
Il appelle MAP
et REDUCE
(l'évaluation est à l'envers), c'est pourquoi on l'appelle réduire la carte .
max-density
compare le deuxième élément des arguments passés, non? Désolé pour la modification idiote.
Prenons l'exemple du papier Google . Le but de MapReduce est de pouvoir utiliser efficacement une charge d'unités de traitement travaillant en parallèle pour certains types d'algorithmes. L'exemple est le suivant: vous voulez extraire tous les mots et leur nombre dans un ensemble de documents.
Implémentation typique:
for each document
for each word in the document
get the counter associated to the word for the document
increment that counter
end for
end for
Implémentation de MapReduce:
Map phase (input: document key, document)
for each word in the document
emit an event with the word as the key and the value "1"
end for
Reduce phase (input: key (a word), an iterator going through the emitted values)
for each value in the iterator
sum up the value in a counter
end for
Autour de cela, vous aurez un programme maître qui partitionnera l'ensemble des documents en «fractionnements» qui seront traités en parallèle pour la phase de cartographie. Les valeurs émises sont écrites par le worker dans un buffer spécifique au worker. Le programme maître délègue ensuite à d'autres travailleurs la tâche d'effectuer la phase de réduction dès qu'il est informé que le tampon est prêt à être manipulé.
Chaque sortie de worker (qu'il s'agisse d'un Map ou d'un worker de Reduce) est en fait un fichier stocké sur le système de fichiers distribué (GFS pour Google) ou dans la base de données distribuée pour CouchDB.
Une introduction vraiment simple , rapide et "pour les nuls" à MapReduce est disponible sur: http://www.marcolotz.com/?p=67
Publier une partie de son contenu:
Tout d'abord, pourquoi MapReduce a-t-il été créé à l'origine?
Fondamentalement, Google avait besoin d'une solution pour rendre les tâches de calcul volumineuses facilement parallélisables, permettant aux données d'être distribuées dans un certain nombre de machines connectées via un réseau. En dehors de cela, il devait gérer la panne de la machine de manière transparente et gérer les problèmes d'équilibrage de charge.
Quels sont les véritables atouts de MapReduce?
On peut dire que la magie de MapReduce est basée sur l'application des fonctions Map and Reduce. Je dois avouer mon pote que je ne suis pas du tout d'accord. La principale caractéristique qui a rendu MapReduce si populaire est sa capacité de parallélisation et de distribution automatiques, combinée à une interface simple. Ces facteurs additionnés à une gestion transparente des échecs pour la plupart des erreurs ont rendu ce cadre si populaire.
Un peu plus de profondeur sur le papier:
MapReduce a été mentionné à l'origine dans un article de Google (Dean & Ghemawat, 2004 - lien ici) comme une solution pour effectuer des calculs dans le Big Data en utilisant une approche parallèle et des grappes d'ordinateurs de base. Contrairement à Hadoop, qui est écrit en Java, le framework de Google est écrit en C ++. Le document décrit comment un cadre parallèle se comporterait en utilisant les fonctions de mappage et de réduction de la programmation fonctionnelle sur de grands ensembles de données.
Dans cette solution, il y aurait deux étapes principales - appelées Mapper et réduire -, avec une étape facultative entre la première et la seconde - appelée Combiner. L'étape de mappage s'exécuterait en premier, effectuerait des calculs dans la paire clé-valeur d'entrée et générerait une nouvelle valeur-clé en sortie. Il faut garder à l'esprit que le format des paires clé-valeur d'entrée ne doit pas nécessairement correspondre à la paire de format de sortie. L'étape Réduire rassemblerait toutes les valeurs de la même clé, effectuant d'autres calculs dessus. En conséquence, cette dernière étape produirait des paires clé-valeur. L'une des applications les plus simples de MapReduce consiste à implémenter le comptage de mots.
Le pseudo-code de cette application est donné ci-dessous:
map(String key, String value):
// key: document name
// value: document contents
for each word w in value:
EmitIntermediate(w, “1”);
reduce(String key, Iterator values):
// key: a word
// values: a list of counts
int result = 0;
for each v in values:
result += ParseInt(v);
Emit(AsString(result));
Comme on peut le remarquer, la carte lit tous les mots d'un enregistrement (dans ce cas, un enregistrement peut être une ligne) et émet le mot comme clé et le nombre 1 comme valeur. Plus tard, la réduction regroupera toutes les valeurs de la même clé. Donnons un exemple: imaginons que le mot «maison» apparaisse trois fois dans l'enregistrement. L'entrée du réducteur serait [maison, [1,1,1]]. Dans le réducteur, il additionnera toutes les valeurs de la maison de clés et donnera en sortie la valeur de clé suivante: [maison, [3]].
Voici une image de ce à quoi cela ressemblerait dans un framework MapReduce:
Comme quelques autres exemples classiques d'applications MapReduce, on peut dire:
• Nombre de fréquence d'accès aux URL
• Graphique de lien Web inversé
• Grep distribué
• Vecteur de termes par hôte
Afin d'éviter trop de trafic réseau, l'article décrit comment le cadre doit essayer de maintenir la localité des données. Cela signifie qu'il doit toujours essayer de s'assurer qu'une machine exécutant des tâches de carte a les données dans sa mémoire / stockage local, en évitant de les extraire du réseau. Dans le but de réduire le réseau grâce à un mappeur, l'étape optionnelle de combinaison, décrite précédemment, est utilisée. Le combinateur effectue des calculs sur la sortie des mappeurs dans une machine donnée avant de l'envoyer aux réducteurs - qui peuvent être dans une autre machine.
Le document décrit également comment les éléments du cadre doivent se comporter en cas de pannes. Ces éléments, dans le papier, sont appelés comme ouvrier et maître. Ils seront divisés en éléments plus spécifiques dans les implémentations open source. Étant donné que Google n'a décrit l'approche que dans le document et n'a pas publié son logiciel propriétaire, de nombreux frameworks open source ont été créés afin de mettre en œuvre le modèle. À titre d'exemple, on peut dire Hadoop ou la fonctionnalité limitée MapReduce de MongoDB.
L'exécution doit prendre en charge les détails des programmeurs non experts, comme le partitionnement des données d'entrée, la planification de l'exécution du programme sur le grand ensemble de machines, la gestion des pannes des machines (de manière transparente, bien sûr) et la gestion de la communication entre les machines. . Un utilisateur expérimenté peut régler ces paramètres, comme la façon dont les données d'entrée seront partitionnées entre les travailleurs.
Concepts clés:
• Tolérance aux pannes:Il doit tolérer correctement les pannes de la machine. Pour ce faire, le maître envoie périodiquement un ping aux travailleurs. Si le maître ne reçoit pas de réponses d'un travailleur donné dans un laps de temps défini, le maître définira le travail comme ayant échoué dans ce travailleur. Dans ce cas, toutes les tâches de carte effectuées par le travailleur défectueux sont jetées et sont données à un autre travailleur disponible. La même chose se produit si le travailleur était toujours en train de traiter une carte ou une tâche de réduction. Notez que si le travailleur a déjà terminé sa partie de réduction, tout le calcul était déjà terminé au moment où il a échoué et n'a pas besoin d'être réinitialisé. En tant que point de défaillance principal, si le maître échoue, tous les travaux échouent. Pour cette raison, on peut définir des points de contrôle périodiques pour le maître, afin de sauvegarder sa structure de données.
• Localité: afin d'éviter le trafic réseau, le framework essaie de s'assurer que toutes les données d'entrée sont localement disponibles pour les machines qui vont effectuer des calculs sur elles. Dans la description d'origine, il utilise Google File System (GFS) avec un facteur de réplication défini sur 3 et des tailles de bloc de 64 Mo. Cela signifie que le même bloc de 64 Mo (qui compose un fichier dans le système de fichiers) aura des copies identiques sur trois machines différentes. Le maître sait où se trouvent les blocs et essaie de planifier des travaux de carte sur cette machine. En cas d'échec, le maître tente d'allouer une machine à proximité d'une réplique des données d'entrée des tâches (c'est-à-dire une machine de travail dans le même rack de la machine de données).
• Granularité des tâches: supposant que chaque phase de la carte est divisée en M morceaux et que chaque phase de réduction est divisée en morceaux R, l'idéal serait que M et R soient beaucoup plus grands que le nombre de machines ouvrières. Cela est dû au fait qu'un travailleur effectuant de nombreuses tâches différentes améliore l'équilibrage dynamique de la charge. En dehors de cela, il augmente la vitesse de récupération en cas d'échec du travailleur (puisque les nombreuses tâches de carte qu'il a effectuées peuvent être réparties sur toutes les autres machines).
• Tâches de sauvegarde: parfois, un ouvrier Map ou Reducer peut se comporter beaucoup plus lentement que les autres dans le cluster. Cela peut contenir le temps de traitement total et le rendre égal au temps de traitement de cette seule machine lente. Le document original décrit une alternative appelée Tâches de sauvegarde, qui sont planifiées par le maître lorsqu'une opération MapReduce est presque terminée. Ce sont des tâches qui sont planifiées par le maître des tâches en cours. Ainsi, l'opération MapReduce se termine lorsque le primaire ou la sauvegarde se termine.
• Compteurs: Parfois, on peut souhaiter compter les occurrences d'événements. Pour cette raison, compte où créé. Les valeurs de compteur de chaque ouvrier sont périodiquement propagées au maître. Le maître agrège ensuite (oui. On dirait que les agrégateurs Pregel sont venus de cet endroit) les valeurs de compteur d'une carte réussie et réduisent la tâche et les renvoient au code utilisateur lorsque l'opération MapReduce est terminée. Il existe également une valeur de compteur actuelle disponible dans l'état du maître, de sorte qu'un humain qui surveille le processus peut suivre son comportement.
Eh bien, je suppose qu'avec tous les concepts ci-dessus, Hadoop sera un morceau de gâteau pour vous. Si vous avez des questions sur l'article original de MapReduce ou sur quoi que ce soit en rapport, veuillez me le faire savoir.
Je ne veux pas paraître banal, mais cela m'a beaucoup aidé, et c'est assez simple:
cat input | map | reduce > output
Si vous êtes familier avec Python, voici l'explication la plus simple possible de MapReduce:
In [2]: data = [1, 2, 3, 4, 5, 6]
In [3]: mapped_result = map(lambda x: x*2, data)
In [4]: mapped_result
Out[4]: [2, 4, 6, 8, 10, 12]
In [10]: final_result = reduce(lambda x, y: x+y, mapped_result)
In [11]: final_result
Out[11]: 42
Voyez comment chaque segment de données brutes a été traité individuellement, dans ce cas, multiplié par 2 (la partie carte de MapReduce). Sur la base de la mapped_result
, nous avons conclu que le résultat serait 42
(la réduction partie de MapReduce).
Une conclusion importante de cet exemple est le fait que chaque bloc de traitement ne dépend pas d'un autre bloc. Par exemple, si les thread_1
cartes [1, 2, 3]
et les thread_2
cartes [4, 5, 6]
, le résultat final des deux threads serait toujours, [2, 4, 6, 8, 10, 12]
mais nous avons réduit de moitié le temps de traitement pour cela. La même chose peut être dite pour l'opération de réduction et c'est l'essence même du fonctionnement de MapReduce dans le calcul parallèle.