Bash scripting e file di grandi dimensioni (bug): l'input con la lettura incorporata da un reindirizzamento produce risultati imprevisti


16

Ho uno strano problema con file di grandi dimensioni e bash. Questo è il contesto:

  • Ho un file di grandi dimensioni: 75 G e oltre 400.000.000 di righe (è un file di registro, mio ​​male, l'ho lasciato crescere).
  • I primi 10 caratteri di ogni riga sono i timestamp nel formato AAAA-MM-GG.
  • Voglio dividere quel file: un file al giorno.

Ho provato con il seguente script che non ha funzionato. La mia domanda è che questo script non funziona, non soluzioni alternative .

while read line; do
  new_file=${line:0:10}_file.log
  echo "$line" >> $new_file
done < file.log

Dopo il debug, ho riscontrato il problema nella new_filevariabile. Questo script:

while read line; do
  new_file=${line:0:10}_file.log
  echo $new_file
done < file.log | uniq -c

dà il risultato qui sotto (ho messo gli xes per mantenere i dati riservati, altri caratteri sono quelli reali). Nota le dhstringhe più brevi e:

...
  27402 2011-xx-x4
  27262 2011-xx-x5
  22514 2011-xx-x6
  17908 2011-xx-x7
...
3227382 2011-xx-x9
4474604 2011-xx-x0
1557680 2011-xx-x1
      1 2011-xx-x2
      3 2011-xx-x1
...
     12 2011-xx-x1
      1 2011-xx-dh
      1 2011-xx-x1
      1 208--
      1 2011-xx-x1
      1 2011-xx-dh
      1 2011-xx-x1    
...

Non è un problema nel formato del mio file . Lo script cut -c 1-10 file.log | uniq -cfornisce solo timestamp validi. È interessante notare che una parte dell'output sopra diventa con cut ... | uniq -c:

3227382 2011-xx-x9
4474604 2011-xx-x0
5722027 2011-xx-x1

Possiamo vedere che dopo il conteggio uniq 4474604, il mio script iniziale non è riuscito.

Ho colpito un limite in bash che non conosco, ho trovato un bug in bash (sembra improbabile) o ho fatto qualcosa di sbagliato?

Aggiornamento :

Il problema si verifica dopo aver letto 2G del file. Cuciture reade reindirizzamenti non amano file più grandi di 2G. Ma sto ancora cercando una spiegazione più precisa.

Aggiornamento2 :

Sembra definitivamente un bug. Può essere riprodotto con:

yes "0123456789abcdefghijklmnopqrs" | head -n 100000000 > file
while read line; do file=${line:0:10}; echo $file; done < file | uniq -c

ma questo funziona bene come soluzione alternativa (sembra che ho trovato utile cat):

cat file | while read line; do file=${line:0:10}; echo $file; done | uniq -c 

È stato archiviato un bug su GNU e Debian. Le versioni interessate sono bash4.1.5 su Debian Squeeze 6.0.2 e 6.0.4.

echo ${BASH_VERSINFO[@]}
4 1 5 1 release x86_64-pc-linux-gnu

Update3:

Grazie ad Andreas Schwab che ha reagito rapidamente alla mia segnalazione di bug, questa è la patch che è la soluzione a questo comportamento scorretto. Il file interessato è lib/sh/zread.ccome Gilles ha sottolineato prima:

diff --git a/lib/sh/zread.c b/lib/sh/zread.c index 0fd1199..3731a41 100644
--- a/lib/sh/zread.c
+++ b/lib/sh/zread.c @@ -161,7 +161,7 @@ zsyncfd (fd)
      int fd; {   off_t off;
-  int r;
+  off_t r;

  off = lused - lind;   r = 0;

La rvariabile viene utilizzata per contenere il valore restituito di lseek. Poiché lseekrestituisce l'offset dall'inizio del file, quando supera i 2 GB, il intvalore è negativo, causando il if (r >= 0)fallimento del test dove avrebbe dovuto avere esito positivo.


1
Riesci a replicare il problema con set più piccoli di dati di input? Sono sempre le stesse linee di input che causano questi problemi?
Larks

@larks: bella domanda. Il problema inizia sempre alla riga 13.520.918 (due volte effettivamente per i test che ho fatto). La dimensione del file prima di questa riga è 2.147.487.726. Sembra che ci sia un limite di 32 bit qui, ma non esattamente come siamo poco più di 2 ^ 31 (2.147.483.648), ma proprio a un limite di buffer 4K (2 ^ 31 + 4K = 2.147.487.744). Le righe precedente e successiva sono normali da 100 a 200 righe di caratteri.
jfg956

Testato su un secondo file (circa della stessa dimensione): il problema inizia alla riga 13.522.712 e il file ha una larghezza di 2.147.498.679 byte prima di quella riga. Sembra di puntare nella direzione di un limite readdell'istruzione in bash.
jfg956,

Risposte:


13

Hai trovato un bug in bash, in qualche modo. È un bug noto con una correzione nota.

I programmi rappresentano un offset in un file come variabile in un tipo intero con una dimensione finita. Ai vecchi tempi, tutti usavano intpraticamente tutto e il inttipo era limitato a 32 bit, incluso il bit di segno, quindi poteva memorizzare valori da -2147483648 a 2147483647. Oggi ci sono nomi di tipi diversi per cose diverse , incluso off_tper un offset in un file.

Per impostazione predefinita, off_tè un tipo a 32 bit su una piattaforma a 32 bit (che consente fino a 2 GB) e un tipo a 64 bit su una piattaforma a 64 bit (che consente fino a 8EB). Tuttavia, è comune compilare programmi con l'opzione LARGEFILE, che imposta il tipo off_tsu 64 bit di larghezza e fa in modo che il programma chiami implementazioni adeguate di funzioni come lseek.

Sembra che tu stia eseguendo bash su una piattaforma a 32 bit e il tuo binario bash non sia compilato con supporto di file di grandi dimensioni. Ora, quando leggi una riga da un file normale, bash usa un buffer interno per leggere i caratteri in batch per le prestazioni (per maggiori dettagli, vedi l'origine in builtins/read.def). Quando la linea è completa, bash chiama lseekper riavvolgere l'offset del file nella posizione della fine della linea, nel caso in cui qualche altro programma si occupasse della posizione in quel file. La chiamata a lseekavviene nella zsyncfcfunzione in lib/sh/zread.c.

Non ho letto la fonte in modo molto dettagliato, ma suppongo che qualcosa non sta accadendo senza problemi nel punto di transizione quando l'offset assoluto è negativo. Quindi bash finisce per leggere gli offset sbagliati quando riempie il suo buffer, dopo aver superato il segno da 2 GB.

Se la mia conclusione è sbagliata e il tuo bash è effettivamente in esecuzione su una piattaforma a 64 bit o compilato con supporto per file di grandi dimensioni, è sicuramente un bug. Segnalalo alla tua distribuzione o a monte .

Una shell non è lo strumento giusto per elaborare file così grandi. Sarà lento. Usa sed se possibile, altrimenti awk.


1
Merci Gilles. Ottima risposta: completa, con informazioni sufficienti per comprendere il problema anche per le persone senza un forte background CS (32 bit ...). (Larsks aiuta anche a fare domande sul numero di riga, e dovrebbe essere riconosciuto.) Dopodiché, ho anche pensato a un problema a 32 bit e scaricato il sorgente, ma non ero ancora a questo livello di analisi. Merci encore, et bonne journée.
jfg956,

4

Non so di sbagliato, ma è certamente contorto. Se le linee di input sono simili alle seguenti:

YYYY-MM-DD some text ...

Quindi non c'è davvero alcun motivo per questo:

new_file=${line:0:4}-${line:5:2}-${line:8:2}_file.log

Stai facendo un sacco di lavoro di sottostringa per finire con qualcosa che sembra ... esattamente come appare già nel file. Cosa ne pensi di questo?

while read line; do
  new_file="${line:0:10}_file.log"
  echo "$line" >> $new_file
done

Questo prende solo i primi 10 caratteri dalla linea. Potresti anche rinunciare bashcompletamente e semplicemente usare awk:

awk '{print > ($1 "_file.log")}' < file.log

Questo prende la data in $1(la prima colonna delimitata da spazi bianchi in ogni riga) e la usa per generare il nome del file.

Si noti che è possibile che ci siano alcune righe di registro fasulle nei file. Cioè, il problema potrebbe essere con l'input, non con lo script. È possibile estendere lo awkscript per contrassegnare le linee fasulle in questo modo:

awk '
$1 ~ /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/ {
    print > ($1 "_file.log")
    next
}

{
    print "INVALID:", $0
}
'

Ciò scrive che le linee corrispondono YYYY-MM-DDai file di registro e contrassegna le linee che non iniziano con un timestamp su stdout.


Nessuna riga falsa nel mio file: cut -c 1-10 file.log | uniq -cmi dà il risultato atteso. Sto usando ${line:0:4}-${line:5:2}-${line:8:2}perché inserirò il file in una directory ${line:0:4}/${line:5:2}/${line:8:2}e ho semplificato il problema (aggiornerò la dichiarazione del problema). So che mi awkpuò aiutare qui, ma ho riscontrato altri problemi durante l'utilizzo. Quello che voglio è capire il problema con bash, non trovare soluzioni alternative.
jfg956

Come hai detto ... se "semplifichi" il problema nella domanda, probabilmente non otterrai le risposte che desideri. Penso ancora che risolverlo con bash non sia davvero il modo giusto per elaborare questo tipo di dati, ma non c'è motivo per cui non dovrebbe funzionare.
Larks

Il problema semplificato dà il risultato inaspettato che ho presentato nella domanda, quindi non credo che sia una semplificazione eccessiva. Inoltre, il problema semplificato fornisce un risultato simile a quello cutdell'affermazione che funziona. Dato che voglio confrontare le mele con le mele, non con le arance, ho bisogno di rendere le cose il più simili possibile.
jfg956

1
Ti ho lasciato una domanda che potrebbe aiutare a capire dove stanno andando le cose male ...
Larsks

2

Sembra che quello che vuoi fare è:

awk '
{  filename = substr($0, 0, 10) "_file.log";  # input format same as output format
   if (filename != lastfile) {
       close(lastfile);
       print 'finished writing to', lastfile;
   }
   print >> filename;
   lastfile=filename;
}' file.log

La closemantiene la tabella dei file aperti da riempire.


Grazie per la soluzione Awk. Vengo già con qualcosa di simile. La mia domanda era capire la limitazione di bash, non trovare una soluzione alternativa.
jfg956,
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.