Skrypty Bash i duże pliki (błąd): wejście z wbudowanym odczytem z przekierowania daje nieoczekiwany rezultat

16

Mam dziwny problem z dużymi plikami i bash. Oto kontekst:

  • Mam duży plik: 75G i ponad 400 000 000 linii (to plik dziennika, mój zły, pozwoliłem mu urosnąć).
  • Pierwsze 10 znaków każdej linii to znaczniki czasu w formacie RRRR-MM-DD.
  • Chcę podzielić ten plik: jeden plik dziennie.

Próbowałem z następującym skryptem, który nie działał. Moje pytanie dotyczy tego, że ten skrypt nie działa, a nie alternatywnych rozwiązań .

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

Po debugowaniu znalazłem problem w new_filezmiennej. Ten skrypt:

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

podaje wynik poniżej (umieszczam te zasady, xaby zachować poufność danych, inne znaki są prawdziwe). Zwróć uwagę na dhi krótsze ciągi:

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

To nie jest problem w formacie mojego pliku . Skrypt cut -c 1-10 file.log | uniq -cpodaje tylko ważne znaczniki czasu. Co ciekawe, część powyższej produkcji otrzymuje postać cut ... | uniq -c:

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

Widzimy, że po zliczeniu uniq 4474604mój początkowy skrypt nie powiódł się.

Czy osiągnąłem limit w bashu, którego nie znam, czy znalazłem błąd w bashu (wydaje się to mało prawdopodobne), czy też zrobiłem coś złego?

Aktualizacja :

Problem występuje po odczytaniu 2G pliku. Szwy readi przekierowania nie lubią większych plików niż 2G. Ale wciąż szukam dokładniejszych wyjaśnień.

Aktualizacja 2 :

To definitywnie wygląda jak błąd. Można go powielać za pomocą:

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

ale działa to dobrze jako obejście (wydaje się, że znalazłem przydatne zastosowanie cat):

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

Błąd został zgłoszony do GNU i Debiana. Dotknięte wersje to bash4.1.5 w Debian Squeeze 6.0.2 i 6.0.4.

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

Aktualizacja 3:

Dzięki Andreasowi Schwabowi, który szybko zareagował na mój raport o błędzie, jest to łatka, która jest rozwiązaniem tego złego zachowania. Dotknięty plik jest, lib/sh/zread.cjak wcześniej zauważył Gilles:

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;

rZmienna jest używana do przechowywania wartości zwracanej lseek. Gdy lseekzwraca przesunięcie od początku pliku, gdy przekracza 2 GB, intwartość jest ujemna, co powoduje, że test if (r >= 0)kończy się niepowodzeniem w miejscu, w którym powinien się powieść.

jfg956
źródło
1
Czy możesz odtworzyć problem z mniejszymi zestawami danych wejściowych? Czy zawsze te same linie wejściowe powodują te problemy?
larsks
@larks: dobre pytanie. Problem zawsze zaczyna się od linii 13.520.918 (faktycznie dwa razy w przypadku testów, które zrobiłem). Rozmiar pliku przed tym wierszem to 2.147.487.726. Wygląda na to, że jest tu limit 32 bitów, ale nie dokładnie tak, jak mamy nieco ponad 2 ^ 31 (2.147.483.648), ale tuż przy limicie bufora 4K (2 ^ 31 + 4K = 2.147.487.744). Poprzednia i następna linia to normalne 100 do 200 linii znaków.
jfg956
Testowany na drugim pliku (mniej więcej o tym samym rozmiarze): problem zaczyna się od wiersza # 13.522.712, a plik ma 2.147.498.679 bajtów przed tą linią. Wydaje się, że wskazuje readbash w kierunku granicy wyrażenia.
jfg956

Odpowiedzi:

13

Znalazłeś błąd w bashu. To znany błąd ze znaną poprawką.

Programy reprezentują przesunięcie w pliku jako zmienną w pewnym typie liczb całkowitych o skończonym rozmiarze. W dawnych czasach wszyscy używali intprawie wszystkiego, a inttyp był ograniczony do 32 bitów, łącznie z bitem znaku, dzięki czemu mógł przechowywać wartości od -2147483648 do 2147483647. Obecnie istnieją różne nazwy typów dla różnych rzeczy , w tym off_tdla przesunięcie w pliku.

Domyślnie off_tjest to wersja 32-bitowa na platformie 32-bitowej (pozwalająca na maksymalnie 2 GB) i typ 64-bitowa na platformie 64-bitowej (pozwalająca na maksymalnie 8EB). Jednak często kompiluje się programy z opcją LARGEFILE, która przełącza ten typ off_tna szerokość 64 bitów i sprawia, że ​​program wywołuje odpowiednie implementacje funkcji takich jak lseek.

Wygląda na to, że uruchamiasz bash na platformie 32-bitowej, a twój plik bash nie jest skompilowany z obsługą dużych plików. Teraz, gdy czytasz wiersz ze zwykłego pliku, bash używa wewnętrznego bufora do odczytywania znaków partiami w celu zwiększenia wydajności (więcej szczegółów w źródle builtins/read.def). Po zakończeniu linii, bash wywołuje lseekprzewinięcie przesunięcia pliku z powrotem do pozycji końca linii, na wypadek, gdyby jakiś inny program dbał o pozycję w tym pliku. Wywołanie do lseekdzieje się w zsyncfcfunkcji w lib/sh/zread.c.

Nie przeczytałem źródła zbyt szczegółowo, ale przypuszczam, że coś nie dzieje się płynnie w punkcie przejścia, gdy przesunięcie absolutne jest ujemne. Więc bash kończy się czytaniem z niewłaściwymi przesunięciami, kiedy uzupełnia swój bufor, po przekroczeniu znaku 2GB.

Jeśli mój wniosek jest błędny, a twoja gra bash działa na platformie 64-bitowej lub jest skompilowana z obsługą dużych plików, to zdecydowanie błąd. Zgłoś to do swojej dystrybucji lub na wyższym szczeblu .

I tak powłoka nie jest odpowiednim narzędziem do przetwarzania tak dużych plików. Będzie powoli. Jeśli to możliwe, użyj sed, w przeciwnym razie awk.

Gilles „SO- przestań być zły”
źródło
1
Merci Gilles. Świetna odpowiedź: kompletna, z wystarczającą ilością informacji, aby zrozumieć problem, nawet dla osób bez silnego tła CS (32 bity ...). (larski pomagają również w pytaniu o numer linii i należy to potwierdzić.) Potem pomyślałem o 32-bitowym problemie i pobrałem źródło, ale nie byłem jeszcze na tym poziomie analizy. Merci encore, et bonne Journée.
jfg956
4

Nie wiem o złym, ale z pewnością jest zawiłe. Jeśli twoje linie wejściowe wyglądają tak:

YYYY-MM-DD some text ...

Zatem naprawdę nie ma tego powodu:

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

Robisz wiele podciągów, aby uzyskać coś, co wygląda ... dokładnie tak, jak już wygląda w pliku. Co powiesz na to?

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

To tylko chwyta pierwsze 10 znaków z linii. Możesz także zrezygnowaćbash całkowicie i po prostu użyć awk:

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

Pobiera to datę $1(pierwsza kolumna oddzielona spacjami w każdym wierszu) i wykorzystuje ją do wygenerowania nazwy pliku.

Zauważ, że możliwe jest, że w twoich plikach znajdują się fałszywe linie logów. Oznacza to, że problem może dotyczyć danych wejściowych, a nie skryptu. Możesz rozszerzyćawk skrypt, aby oznaczyć fałszywe linie w następujący sposób:

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

Spowoduje to zapisanie wierszy pasujących YYYY-MM-DDdo plików dziennika i oznaczenie wierszy, które nie zaczynają się od znacznika czasu na standardowym wyjściu.

Larsks
źródło
W moim pliku nie ma fałszywych linii: cut -c 1-10 file.log | uniq -cdaje oczekiwany wynik. Używam, ${line:0:4}-${line:5:2}-${line:8:2}ponieważ umieszczę plik w katalogu ${line:0:4}/${line:5:2}/${line:8:2}i uprościłem problem (zaktualizuję opis problemu). Wiem, że awkmoże mi tutaj pomóc, ale miałem inne problemy z korzystaniem z niego. Chcę zrozumieć problem bash, a nie znaleźć alternatywnych rozwiązań.
jfg956
Jak powiedziałeś ... jeśli „uprościsz” problem w pytaniu, prawdopodobnie nie dostaniesz odpowiedzi, których chcesz. Nadal uważam, że rozwiązanie tego za pomocą bash nie jest tak naprawdę właściwym sposobem przetwarzania tego rodzaju danych, ale nie ma powodu, dla którego nie powinno to działać.
larsks
Uproszczony problem daje nieoczekiwany rezultat, który przedstawiłem w pytaniu, więc nie sądzę, że jest to nadmierne uproszczenie. Co więcej, uproszczony problem daje podobny wynik jak cutstwierdzenie, które działa. Ponieważ chcę porównać jabłka z jabłkami, a nie z pomarańczami, muszę uczynić rzeczy tak podobnymi, jak to możliwe.
jfg956
1
Zostawiłem ci pytanie, które może pomóc dowiedzieć się, co się dzieje nie tak ...
Lars
2

Wygląda na to, że chcesz:

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

closeUtrzymuje otwartą tabelę plików z napełnieniem.

Arcege
źródło
Dzięki za rozwiązanie awk. Już idę z czymś podobnym. Moim pytaniem było zrozumienie ograniczenia bashu, a nie znalezienie alternatywnego rozwiązania.
jfg956