In che modo Hadoop elabora i record suddivisi tra i confini dei blocchi?


119

Secondo il Hadoop - The Definitive Guide

I record logici definiti da FileInputFormats di solito non si adattano perfettamente ai blocchi HDFS. Ad esempio, i record logici di un TextInputFormat sono linee, che superano i confini di HDFS il più delle volte. Ciò non ha alcuna relazione con il funzionamento del tuo programma, ad esempio le linee non vengono perse o interrotte, ma vale la pena conoscerlo, poiché significa che le mappe locali di dati (cioè le mappe che sono in esecuzione sullo stesso host del loro dati di input) eseguirà alcune letture remote. Il leggero sovraccarico che ciò causa normalmente non è significativo.

Supponiamo che una riga di record sia divisa in due blocchi (b1 e b2). Il mappatore che elabora il primo blocco (b1) noterà che l'ultima riga non ha un separatore EOL e preleva il resto della riga dal successivo blocco di dati (b2).

In che modo il mappatore che elabora il secondo blocco (b2) determina che il primo record è incompleto e deve essere elaborato a partire dal secondo record nel blocco (b2)?

Risposte:


160

Domanda interessante, ho passato un po 'di tempo a guardare il codice per i dettagli e qui ci sono i miei pensieri. Le suddivisioni sono gestite dal client da InputFormat.getSplits, quindi uno sguardo a FileInputFormat fornisce le seguenti informazioni:

  • Per ogni file di input, ottieni la lunghezza del file, la dimensione del blocco e calcola la dimensione della divisione come max(minSize, min(maxSize, blockSize))dove maxSizecorrisponde a mapred.max.split.sizeed minSizeè mapred.min.split.size.
  • Dividi il file in diversi messaggi in FileSplitbase alla dimensione della divisione calcolata sopra. Ciò che è importante qui è che ciascuno di essi FileSplitviene inizializzato con un startparametro corrispondente all'offset nel file di input . Non c'è ancora alcuna gestione delle linee a quel punto. La parte rilevante del codice è simile a questa:

    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;
    }
    

Dopodiché, se guardi il LineRecordReaderche è definito da TextInputFormat, è lì che vengono gestite le linee:

  • Quando si inizializza LineRecordReader, cerca di creare un'istanza LineReaderche è un'astrazione per poter leggere le righe FSDataInputStream. Ci sono 2 casi:
  • Se è presente un CompressionCodecdefinito, questo codec è responsabile della gestione dei confini. Probabilmente non pertinente alla tua domanda.
  • Se non c'è un codec, però, è lì che le cose sono interessanti: se il starttuo InputSplitè diverso da 0, fai un backtrack di 1 carattere e poi salti la prima riga che incontri identificata da \ n o \ r \ n (Windows) ! Il backtrack è importante perché nel caso in cui i confini della tua linea siano gli stessi dei confini divisi, questo ti assicura di non saltare la linea valida. Ecco il codice pertinente:

    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;
    

Quindi, poiché le suddivisioni vengono calcolate nel client, i mappatori non devono essere eseguiti in sequenza, ogni mappatore sa già se è necessario eliminare la prima riga o meno.

Quindi, in pratica, se hai 2 righe di ogni 100 MB nello stesso file, e per semplificare diciamo che la dimensione della divisione è 64 MB. Quindi, quando vengono calcolate le suddivisioni degli input, avremo il seguente scenario:

  • Divisione 1 contenente il percorso e gli host di questo blocco. Inizializzato all'inizio 200-200 = 0 Mb, lunghezza 64 Mb.
  • Divisione 2 inizializzata all'inizio 200-200 + 64 = 64 Mb, lunghezza 64 Mb.
  • Divisione 3 inizializzata all'inizio 200-200 + 128 = 128 Mb, lunghezza 64 Mb.
  • Split 4 inizializzato all'inizio 200-200 + 192 = 192Mb, lunghezza 8Mb.
  • Il Mapper A elaborerà la divisione 1, l'inizio è 0, quindi non saltare la prima riga e leggere una riga completa che va oltre il limite di 64 MB, quindi è necessaria la lettura remota.
  • Il Mapper B elaborerà la divisione 2, l'inizio è! = 0 quindi salta la prima riga dopo 64 MB-1 byte, che corrisponde alla fine della riga 1 a 100 MB che è ancora nella divisione 2, abbiamo 28 MB della riga nella divisione 2, quindi remoto leggere i restanti 72Mb.
  • Il Mapper C elaborerà la divisione 3, l'inizio è! = 0 quindi salta la prima riga dopo 128 MB-1 byte, che corrisponde alla fine della riga 2 a 200 MB, che è la fine del file, quindi non fare nulla.
  • Il mappatore D è lo stesso del mappatore C tranne per il fatto che cerca una nuova riga dopo 192Mb-1byte.

Anche @PraveenSripati vale la pena ricordare che i casi limite in cui un confine sarebbe \ r in un \ r \ n ritorno sono gestiti nella LineReader.readLinefunzione, non penso sia rilevante per la tua domanda ma posso aggiungere ulteriori dettagli se necessario.
Charles Menguy

Supponiamo che ci siano due linee con 64 MB esatti nell'input e quindi gli InputSplit si verificano esattamente ai confini della linea. Quindi, il mappatore ignorerà sempre la linea nel secondo blocco perché inizia! = 0.
Praveen Sripati

6
@PraveenSripati In quel caso, il secondo mappatore vedrà start! = 0, quindi torna indietro di 1 carattere, che ti riporta appena prima della \ n della prima riga e poi salta al seguente \ n. Quindi salterà la prima riga ma elaborerà la seconda come previsto.
Charles Menguy

@CharlesMenguy è possibile che la prima riga del file venga saltata in qualche modo? In concreto, ho la prima riga con key = 1 e valore a, quindi ci sono altre due righe con la stessa chiave da qualche parte nel file, key = 1, val = be key = 1, val = c. Il fatto è che il mio riduttore ottiene {1, [b, c]} e {1, [a]}, invece di {1, [a, b, c]}. Questo non accade se aggiungo una nuova riga all'inizio del mio file. Quale potrebbe essere la ragione, signore?
Kobe-Wan Kenobi

@CharlesMenguy Cosa succede se il file su HDFS è un file binario (al contrario di un file di testo, in cui \r\n, \nrappresenta il troncamento del record)?
CᴴᴀZ

17

L' algoritmo di riduzione della mappa non funziona sui blocchi fisici del file. Funziona sulle suddivisioni degli input logici. La suddivisione dell'input dipende da dove è stato scritto il record. Un record può comprendere due Mapper.

Il modo in cui è stato impostato HDFS , suddivide file molto grandi in blocchi di grandi dimensioni (ad esempio, che misurano 128 MB) e memorizza tre copie di questi blocchi su diversi nodi del cluster.

HDFS non è a conoscenza del contenuto di questi file. Un record potrebbe essere stato avviato nel Blocco-a ma la fine di quel record potrebbe essere presente nel Blocco-b .

Per risolvere questo problema, Hadoop utilizza una rappresentazione logica dei dati archiviati in blocchi di file, noti come suddivisioni di input. Quando un client di lavoro MapReduce calcola le suddivisioni di input , scopre dove inizia il primo record intero in un blocco e dove finisce l'ultimo record nel blocco .

Il punto chiave:

Nei casi in cui l'ultimo record in un blocco è incompleto, la suddivisione dell'input include le informazioni sulla posizione per il blocco successivo e l'offset di byte dei dati necessari per completare il record.

Dai un'occhiata al diagramma sottostante.

inserisci qui la descrizione dell'immagine

Dai un'occhiata a questo articolo e alla relativa domanda SE: Informazioni sulla suddivisione dei file Hadoop / HDFS

Maggiori dettagli possono essere letti dalla documentazione

Il framework Map-Reduce si basa sull'InputFormat del lavoro per:

  1. Convalida la specifica di input del lavoro.
  2. Suddividi i file di input in InputSplit logici, ciascuno dei quali viene quindi assegnato a un singolo Mapper.
  3. Ogni InputSplit viene quindi assegnato a un singolo Mapper per l'elaborazione. Split potrebbe essere una tupla . InputSplit[] getSplits(JobConf job,int numSplits) è l'API per occuparsi di queste cose.

FileInputFormat , che estende il metodo InputFormatimplementato getSplits(). Dai un'occhiata agli interni di questo metodo su grepcode


7

Lo vedo come segue: InputFormat è responsabile della suddivisione dei dati in suddivisioni logiche tenendo conto della natura dei dati.
Niente gli impedisce di farlo, sebbene possa aggiungere una significativa latenza al lavoro: tutta la logica e la lettura intorno ai limiti di dimensione di divisione desiderati avverranno nel jobtracker.
Il formato di input più semplice in grado di riconoscere i record è TextInputFormat. Funziona come segue (per quanto ho capito dal codice) - il formato di input crea divisioni per dimensione, indipendentemente dalle righe, ma LineRecordReader sempre:
a) Salta la prima riga nella divisione (o parte di essa), se non lo è la prima divisione
b) Leggere una riga dopo il limite della divisione alla fine (se i dati sono disponibili, quindi non è l'ultima divisione).


Skip first line in the split (or part of it), if it is not the first split- Se il primo record in un blocco diverso dal primo è completo, non sono sicuro di come funzionerà questa logica.
Praveen Sripati

Per quanto vedo il codice, ogni divisione legge cosa ha + riga successiva. Quindi se l'interruzione di riga non è sul confine del blocco, va bene. Come viene gestito esattamente il caso in cui l'interruzione di riga è esattamente sul limite del blocco - deve essere compreso - leggerò il codice un po 'di più
David Gruzman

3

Da quello che ho capito, quando FileSplitviene inizializzato per il primo blocco, viene chiamato il costruttore predefinito. Pertanto i valori per inizio e lunghezza sono inizialmente zero. Alla fine dell'elaborazione del primo blocco, se l'ultima riga è incompleta, il valore della lunghezza sarà maggiore della lunghezza della divisione e leggerà anche la prima riga del blocco successivo. Per questo motivo il valore di inizio per il primo blocco sarà maggiore di zero e in questa condizione LineRecordReadersalterà la prima riga del secondo blocco. (Vedi fonte )

Nel caso in cui l'ultima riga del primo blocco sia completa, il valore di lunghezza sarà uguale alla lunghezza del primo blocco e il valore di inizio per il secondo blocco sarà zero. In tal caso LineRecordReader, non salterà la prima riga e leggerà il secondo blocco dall'inizio.

Ha senso?


2
In questo scenario, i mappatori devono comunicare tra loro ed elaborare i blocchi in sequenza quando l'ultima riga in un particolare blocco non è completa. Non sono sicuro che funzioni così.
Praveen Sripati

1

Dal codice sorgente hadoop di LineRecordReader.java il costruttore: trovo alcuni commenti:

// 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;

da questo credo che hadoop leggerà una riga in più per ogni divisione (alla fine della divisione corrente, leggerà la riga successiva nella divisione successiva), e se non la prima divisione, la prima riga verrà eliminata. in modo che nessun record di riga venga perso e incompleto


0

I mappatori non devono comunicare. I blocchi di file sono in HDFS e il mapper corrente (RecordReader) può leggere il blocco che ha la parte rimanente della riga. Questo accade dietro le quinte.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.