Bash-Scripting und große Dateien (Fehler): Eingaben mit dem eingebauten Lesevorgang aus einer Umleitung führen zu unerwarteten Ergebnissen

16

Ich habe ein seltsames Problem mit großen Dateien und bash. Dies ist der Kontext:

  • Ich habe eine große Datei: 75G und mehr als 400.000.000 Zeilen (es ist eine Protokolldatei, meine schlechte, ich lasse es wachsen).
  • Die ersten 10 Zeichen jeder Zeile sind Zeitstempel im Format JJJJ-MM-TT.
  • Ich möchte diese Datei aufteilen: eine Datei pro Tag.

Ich habe es mit dem folgenden Skript versucht, das nicht funktioniert hat. Meine Frage ist, dass dieses Skript nicht funktioniert, keine alternativen Lösungen .

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

Nach dem Debuggen habe ich das Problem in der new_fileVariablen gefunden. Dieses Skript:

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

gibt das Ergebnis wie folgt aus (Ich setze die xes, um die Daten vertraulich zu behandeln, andere Zeichen sind die wirklichen). Beachten Sie die dhund die kürzeren Saiten:

...
  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    
...

Es ist kein Problem im Format meiner Datei . Das Skript cut -c 1-10 file.log | uniq -cgibt nur gültige Zeitstempel aus. Interessanterweise wird ein Teil der obigen Ausgabe mit cut ... | uniq -c:

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

Wir können sehen, dass 4474604mein anfängliches Skript nach der Anzahl der Unikate fehlgeschlagen ist.

Habe ich ein Limit in Bash erreicht, das ich nicht kenne, habe ich einen Fehler in Bash gefunden (es ist unwahrscheinlich), oder habe ich etwas falsch gemacht?

Update :

Das Problem tritt auf, nachdem 2G der Datei gelesen wurden. Es Nähte readund Umleitung nicht wie größere Dateien als 2G. Aber immer noch auf der Suche nach einer genaueren Erklärung.

Update2 :

Es sieht definitiv aus wie ein Bug. Es kann reproduziert werden mit:

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

Aber dies funktioniert gut als Workaround (es scheint, dass ich eine nützliche Verwendung gefunden habe cat):

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

Ein Fehler wurde bei GNU und Debian gemeldet. Betroffen sind die Versionen bash4.1.5 unter Debian Squeeze 6.0.2 und 6.0.4.

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

Update3:

Dank Andreas Schwab, der schnell auf meinen Fehlerbericht reagiert hat, ist dieser Patch die Lösung für dieses Fehlverhalten. Die betroffene Datei ist, lib/sh/zread.cwie Gilles früher bemerkte:

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;

Die rVariable wird verwendet, um den Rückgabewert von zu halten lseek. Da lseekder Offset vom Anfang der Datei zurückgegeben wird, ist der intWert negativ , wenn er über 2 GB liegt, was dazu führt, dass der Test if (r >= 0)dort fehlschlägt, wo er erfolgreich sein sollte.

jfg956
quelle
1
Können Sie das Problem mit kleineren Eingabedatensätzen wiederholen? Sind es immer die gleichen Eingangsleitungen, die zu diesen Problemen führen?
larsks
@ Larks: gute Frage. Das Problem beginnt immer in Zeile 13.520.918 (zweimal für die Tests, die ich durchgeführt habe). Die Größe der Datei vor dieser Zeile beträgt 2.147.487.726. Es scheint, dass es hier eine 32-Bit-Grenze gibt, aber nicht genau, da wir etwas über 2 ^ 31 (2.147.483.648) sind, sondern genau bei einer 4K-Puffergrenze (2 ^ 31 + 4K = 2.147.487.744). Die vorherige und die nächste Zeile bestehen normalerweise aus 100 bis 200 Zeichen.
jfg956
Getestet mit einer zweiten Datei (ungefähr gleich groß): Das Problem beginnt in Zeile 13.522.712, und die Datei ist vor dieser Zeile 2.147.498.679 Byte groß. Es scheint in die Richtung einer Begrenzung der readAnweisung in Bash zu zeigen.
jfg956

Antworten:

13

Sie haben einen Fehler in der Bash gefunden. Es ist ein bekannter Fehler mit einem bekannten Fix.

Programme repräsentieren einen Versatz in einer Datei als Variable in einem ganzzahligen Typ mit einer endlichen Größe. Früher wurde jeder intfür so gut wie alles verwendet, und der intTyp war auf 32 Bit beschränkt, einschließlich des Vorzeichenbits, sodass Werte von -2147483648 bis 2147483647 gespeichert werden konnten. Heutzutage gibt es verschiedene Typnamen für verschiedene Dinge , einschließlich off_tfür eine Offset in einer Datei.

Standardmäßig off_tist dies ein 32-Bit-Typ auf einer 32-Bit-Plattform (maximal 2 GB) und ein 64-Bit-Typ auf einer 64-Bit-Plattform (maximal 8 GB). Es ist jedoch üblich, Programme mit der Option LARGEFILE zu kompilieren, die den Typ off_tauf 64 Bit Breite umschaltet und das Programm geeignete Implementierungen von Funktionen wie z lseek.

Es scheint, dass Sie Bash auf einer 32-Bit-Plattform ausführen und Ihre Bash-Binärdatei nicht mit Unterstützung für große Dateien kompiliert wurde. Wenn Sie nun eine Zeile aus einer regulären Datei lesen, verwendet bash einen internen Puffer, um Zeichen in Batches aus Gründen der Leistung zu lesen (weitere Informationen finden Sie in der Quelle in builtins/read.def). Wenn die Zeile vollständig ist, ruft bash lseekauf, um den Versatz der Datei an die Position des Zeilenendes zurückzuspulen, falls die Position in dieser Datei von einem anderen Programm geändert wurde. Der Aufruf lseekerfolgt in der zsyncfcFunktion in lib/sh/zread.c.

Ich habe die Quelle nicht genau gelesen, aber ich vermute, dass am Übergangspunkt, an dem der absolute Versatz negativ ist, etwas nicht reibungslos abläuft. Daher liest bash am Ende an den falschen Offsets, wenn der Puffer nach dem Überschreiten der 2-GB-Marke wieder aufgefüllt wird.

Wenn meine Schlussfolgerung falsch ist und Ihre Bash tatsächlich auf einer 64-Bit-Plattform läuft oder mit Unterstützung für große Dateien kompiliert wurde, ist das definitiv ein Fehler. Bitte melden Sie es Ihrer Distribution oder dem Upstream .

Eine Shell ist ohnehin nicht das richtige Werkzeug, um so große Dateien zu verarbeiten. Es wird langsam sein. Verwenden Sie sed wenn möglich, sonst awk.

Gilles 'SO - hör auf böse zu sein'
quelle
1
Merci Gilles. Tolle Antwort: Vollständig, mit genügend Informationen, um das Problem auch für Leute ohne starken CS-Hintergrund zu verstehen (32 Bit ...). (Larsks helfen auch beim Hinterfragen der Zeilennummer, und es sollte quittiert werden.) Danach habe ich zwar auch ein 32-Bit-Problem und die Quelle heruntergeladen, war aber noch nicht auf diesem Analyse-Level. Merci encore, et bonne journée.
jfg956
4

Ich weiß nichts falsches, aber es ist sicherlich verworren. Wenn Ihre Eingabezeilen so aussehen:

YYYY-MM-DD some text ...

Dann gibt es wirklich keinen Grund dafür:

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

Sie machen eine Menge Teilstring-Arbeit, um etwas zu erhalten, das genau so aussieht, wie es bereits in der Datei aussieht. Wie wäre es damit?

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

Damit werden nur die ersten 10 Zeichen der Zeile erfasst. Sie könnten auch bashganz darauf verzichten und einfach verwenden awk:

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

Dabei wird das Datum $1(die erste durch Leerzeichen getrennte Spalte in jeder Zeile) erfasst und zur Generierung des Dateinamens verwendet.

Beachten Sie, dass Ihre Dateien möglicherweise falsche Protokollzeilen enthalten. Das Problem liegt möglicherweise bei der Eingabe, nicht bei Ihrem Skript. Sie können das awkSkript so erweitern, dass falsche Zeilen wie folgt gekennzeichnet werden:

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

Dies schreibt Zeilen, die YYYY-MM-DDmit Ihren Protokolldateien übereinstimmen , und kennzeichnet Zeilen, die nicht mit einem Zeitstempel auf stdout beginnen.

larsks
quelle
Keine falschen Zeilen in meiner Datei: cut -c 1-10 file.log | uniq -cGibt mir das erwartete Ergebnis. Ich verwende, ${line:0:4}-${line:5:2}-${line:8:2}weil ich die Datei in einem Verzeichnis ${line:0:4}/${line:5:2}/${line:8:2}ablegen und das Problem vereinfacht habe (ich werde die Problembeschreibung aktualisieren). Ich weiß, awkkann mir hier helfen, aber ich habe andere Probleme damit. Was ich will ist das Problem zu verstehen bash, keine alternativen Lösungen zu finden.
jfg956
Wie Sie sagten ... wenn Sie das Problem in der Frage "vereinfachen", werden Sie wahrscheinlich nicht die gewünschten Antworten erhalten. Ich denke immer noch, dass das Lösen mit Bash nicht wirklich der richtige Weg ist, um diese Art von Daten zu verarbeiten, aber es gibt keinen Grund, warum es nicht funktionieren sollte.
larsks
Das vereinfachte Problem führt zu dem unerwarteten Ergebnis, das ich in der Frage vorgestellt habe. Ich halte es daher nicht für eine zu starke Vereinfachung. Darüber hinaus liefert das vereinfachte Problem ein ähnliches Ergebnis wie die cutfunktionierende Anweisung. Da ich Äpfel mit Äpfeln vergleichen möchte, nicht mit Orangen, muss ich die Dinge so ähnlich wie möglich gestalten.
jfg956
1
Ich habe dir eine Frage hinterlassen, die dir helfen könnte herauszufinden, wo die Dinge schief
laufen
2

Klingt so, als ob Sie Folgendes tun möchten:

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

Das closehält die geöffnete Datei Tabelle von füllen.

Arcege
quelle
Danke für die awk Lösung. Ich komme schon mit etwas ähnlichem. Meine Frage war, die Bash-Beschränkung zu verstehen und keine alternative Lösung zu finden.
jfg956