Selon le Hadoop - The Definitive Guide
Les enregistrements logiques définis par FileInputFormats ne s’intègrent généralement pas parfaitement dans les blocs HDFS. Par exemple, les enregistrements logiques d’un TextInputFormat sont des lignes qui traversent le plus souvent les limites HDFS. Cela n’a aucune incidence sur le fonctionnement de votre programme (les lignes ne sont pas omises ou brisées, par exemple), mais cela vaut la peine de le savoir, car cela signifie que les cartes locales aux données (c’est-à-dire des cartes qui s'exécutent sur le même hôte que leur données d'entrée) effectuera des lectures à distance. La légère surcharge que cela cause n'est normalement pas significative.
Supposons qu'une ligne d'enregistrement soit divisée en deux blocs (b1 et b2). Le mappeur qui traite le premier bloc (b1) remarquera que la dernière ligne n'a pas de séparateur EOL et extrait le reste de la ligne du bloc de données suivant (b2).
Comment le mappeur qui traite le deuxième bloc (b2) détermine-t-il que le premier enregistrement est incomplet et doit-il traiter à partir du deuxième enregistrement du bloc (b2)?
Question intéressante, j'ai passé quelque temps à regarder le code pour les détails et voici ce que je pense. Les fractionnements sont gérés par le client par InputFormat.getSplits
. Un examen de FileInputFormat donne donc les informations suivantes:
max(minSize, min(maxSize, blockSize))
où maxSize
correspond à mapred.max.split.size
Et minSize
est mapred.min.split.size
.Divisez le fichier en différents FileSplit
s en fonction de la taille de division calculée ci-dessus. Ce qui est important ici est que chaque FileSplit
est initialisé avec un paramètre start
correspondant au décalage dans le fichier d’entrée. Il n'y a toujours pas de traitement des lignes à ce stade. La partie pertinente du code ressemble à ceci:
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
splits.add(new FileSplit(path, length-bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts()));
bytesRemaining -= splitSize;
}
Après cela, si vous regardez le LineRecordReader
qui est défini par le TextInputFormat
, c'est là que les lignes sont gérées:
LineRecordReader
, il tente d’instancier un LineReader
qui est une abstraction pour pouvoir lire des lignes sur FSDataInputStream
. Il y a 2 cas:CompressionCodec
est défini, ce codec est responsable de la gestion des limites. Probablement pas pertinent pour votre question.S'il n'y a pas de codec cependant, c'est là que les choses sont intéressantes: si le start
de votre InputSplit
est différent de 0, alors vous retournez un caractère puis sautez la première ligne rencontre identifiée par\n ou\r\n (Windows)! Le retour en arrière est important car si les limites de votre ligne sont identiques à celles des divisions, vous évitez de sauter la ligne valide. Voici le code pertinent:
if (codec != null) {
in = new LineReader(codec.createInputStream(fileIn), job);
end = Long.MAX_VALUE;
} else {
if (start != 0) {
skipFirstLine = true;
--start;
fileIn.seek(start);
}
in = new LineReader(fileIn, job);
}
if (skipFirstLine) { // skip first line and re-establish "start".
start += in.readLine(new Text(), 0,
(int)Math.min((long)Integer.MAX_VALUE, end - start));
}
this.pos = start;
Ainsi, puisque les divisions sont calculées dans le client, les mappeurs n'ont pas besoin de s'exécuter en séquence, chaque mappeur sait déjà s'il doit ou non supprimer la première ligne.
Donc, fondamentalement, si vous avez 2 lignes de chaque 100 Mo dans le même fichier, et pour simplifier, supposons que la taille de la scission soit de 64 Mo. Ensuite, lorsque les fractionnements en entrée sont calculés, nous aurons le scénario suivant:
L'algorithme Map Reduce ne fonctionne pas sur les blocs physiques du fichier. Cela fonctionne sur les fractionnements d'entrées logiques. Le fractionnement en entrée dépend de l'endroit où l'enregistrement a été écrit. Un enregistrement peut s'étendre sur deux mappeurs.
La manière [~ # ~] hdfs [~ # ~] a été configurée, elle décompose les très gros fichiers en gros blocs (par exemple, la mesure 128 Mo) et stocke trois copies de ces blocs sur différents nœuds du cluster.
HDFS n'a aucune connaissance du contenu de ces fichiers. Un enregistrement peut avoir été démarré dans Block-a mais une fin de cet enregistrement peut être présente dans Block-b.
Pour résoudre ce problème, Hadoop utilise une représentation logique des données stockées dans des blocs de fichiers, appelée fractionnements en entrée. Lorsqu'un client du travail MapReduce calcule les fractionnements d'entrée , il détermine où se trouve le premier enregistrement complet un bloc commence et où se termine le dernier enregistrement du bloc.
Le point clé:
Dans les cas où le dernier enregistrement d'un bloc est incomplet, le fractionnement en entrée inclut les informations d'emplacement pour le bloc suivant et le décalage d'octet des données nécessaires pour terminer l'enregistrement.
Regardez le diagramme ci-dessous.
Jetez un coup d'œil à ceci article et à la question SE correspondante: À propos du fractionnement de fichiers Hadoop/HDFS
Plus de détails peuvent être lus de documentation
La structure Map-Reduce repose sur le format d’entrée du travail pour:
InputSplit[] getSplits(JobConf job,int numSplits
) est l’API pour s’occuper de ces choses.FileInputFormat , qui étend la méthode InputFormat
implémentée getSplits
(). Jetez un coup d'oeil aux éléments internes de cette méthode à grepcode
Je le vois comme suit: InputFormat est responsable de la division des données en divisions logiques en tenant compte de la nature des données.
Rien ne l’empêche de le faire, bien que cela puisse ajouter une latence importante au travail - toute la logique et la lecture autour des limites de taille de division souhaitées se produiront dans le gestionnaire de tâches.
Le format d'entrée le plus simple compatible avec l'enregistrement est TextInputFormat. Cela fonctionne comme suit (d'après ce que j'ai compris du code) - le format d'entrée crée des fractionnements par taille, quelles que soient les lignes, mais LineRecordReader toujours:
a) Omettre la première ligne de la division (ou une partie de celle-ci), si ce n'est pas la première division
b) Lire une ligne après la limite de la division à la fin (si les données sont disponibles, il ne s’agit donc pas de la dernière division).
D'après ce que j'ai compris, lorsque le FileSplit
est initialisé pour le premier bloc, le constructeur par défaut est appelé. Par conséquent, les valeurs de début et de longueur sont zéro initialement. À la fin du traitement du premier bloc, si la dernière ligne est incomplète, la valeur de la longueur sera supérieure à la longueur de la division et la première ligne du bloc suivant sera également lue. Pour cette raison, la valeur de départ pour le premier bloc sera supérieure à zéro et, dans cette condition, le LineRecordReader
ignorera la première ligne du deuxième bloc. (Voir source )
Si la dernière ligne du premier bloc est terminée, la valeur de la longueur sera égale à la longueur du premier bloc et la valeur du début du deuxième bloc sera égale à zéro. Dans ce cas, le LineRecordReader
ne sautera pas la première ligne et lira le deuxième bloc au début.
Logique?
De hadoop code source de LineRecordReader.Java le constructeur: je trouve quelques commentaires:
// If this is not the first split, we always throw away first record
// because we always (except the last split) read one extra line in
// next() method.
if (start != 0) {
start += in.readLine(new Text(), 0, maxBytesToConsume(start));
}
this.pos = start;
je pense que hadoop lira une ligne supplémentaire pour chaque division (à la fin de la division actuelle, lira la ligne suivante dans la division suivante), et si ce n’est pas la première division, la première ligne sera jetée. de sorte qu'aucun enregistrement de ligne ne sera perdu et incomplet
Les mappeurs ne doivent pas communiquer. Les blocs de fichiers sont en HDFS et le mappeur actuel (RecordReader) peut lire le bloc contenant la partie restante de la ligne. Cela se passe dans les coulisses.