Скрипты Bash и большие файлы (ошибка): ввод с помощью встроенного чтения из перенаправления дает неожиданный результат

16

У меня странная проблема с большими файлами и bash. Это контекст:

  • У меня большой файл: 75G и 400,000,000+ строк (это файл журнала, мой плохой, я позволил ему расти).
  • Первые 10 символов каждой строки представляют собой метки времени в формате ГГГГ-ММ-ДД.
  • Я хочу разделить этот файл: один файл в день.

Я попытался с помощью следующего сценария, который не работал. Мой вопрос о том, что этот скрипт не работает, а не альтернативные решения .

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

После отладки я нашел проблему в new_fileпеременной. Этот скрипт:

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

дает результат ниже (я ставлю xes, чтобы сохранить конфиденциальность данных, другие символы являются реальными). Обратите внимание на dhи более короткие строки:

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

Это не проблема в формате моего файла . Скрипт cut -c 1-10 file.log | uniq -cвыдает только допустимые метки времени. Интересно, что часть вышеприведенного вывода делается с cut ... | uniq -c:

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

Мы видим, что после подсчета uniq 4474604мой первоначальный скрипт не удался.

Я достиг предела в bash, которого я не знаю, обнаружил ли я ошибку в bash (она кажется маловероятной), или я сделал что-то не так?

Обновление :

Проблема возникает после прочтения 2G файла. Это швы readи перенаправление не любят большие файлы, чем 2G. Но все еще в поисках более точного объяснения.

Обновление 2 :

Это определенно выглядит как ошибка. Это может быть воспроизведено с:

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

но это прекрасно работает в качестве обходного пути (кажется, что я нашел полезное применение cat):

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

Ошибка была подана в GNU и Debian. Подвержены уязвимости версии bash4.1.5 в Debian Squeeze 6.0.2 и 6.0.4.

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

Update3:

Благодаря Андреасу Швабу, который быстро отреагировал на мое сообщение об ошибке, этот патч является решением этой проблемы. Файл, на который повлияли, - это, lib/sh/zread.cкак Жиль указал раньше:

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;

rПеременная используется для хранения возвращаемого значения lseek. As lseekвозвращает смещение от начала файла, когда оно превышает 2 ГБ, intзначение является отрицательным, что приводит if (r >= 0)к сбою теста там, где он должен был быть успешным.

jfg956
источник
1
Можете ли вы повторить проблему с меньшими наборами входных данных? Всегда ли одни и те же строки ввода приводят к этим проблемам?
larsks
@larks: хороший вопрос. Проблема всегда начинается со строки № 13.520.918 (дважды для тестов, которые я проводил). Размер файла до этой строки составляет 2,147,487,726. Это говорит о том, что здесь есть ограничение в 32 бита, но не совсем так, как мы чуть более 2 ^ 31 (2.147.483.648), но прямо на пределе буфера 4K (2 ^ 31 + 4K = 2.147.487.744). Предыдущая и следующая строки - это обычные строки длиной от 100 до 200 символов.
jfg956
Протестировано на втором файле (примерно такого же размера): проблема начинается со строки # 13.522.712, а размер файла составляет 2,147,498,679 байт перед этой строкой. Это швы, чтобы указать в направлении предела readутверждения в bash.
jfg956

Ответы:

13

Вы нашли ошибку в Bash, в некотором роде. Это известная ошибка с известным исправлением.

Программы представляют смещение в файле как переменную в некотором целочисленном типе с конечным размером. В старые времена все использовали intпрактически все, и intтип был ограничен 32 битами, включая знаковый бит, поэтому он мог хранить значения от -2147483648 до 2147483647. В настоящее время существуют разные имена типов для разных вещей , в том числе off_tдля смещение в файле.

По умолчанию off_tэто 32-битный тип на 32-битной платформе (допускает до 2 ГБ) и 64-битный тип на 64-битной платформе (допускает до 8EB). Однако обычно программы компилируются с опцией LARGEFILE, которая переключает тип off_tна 64- битную ширину и заставляет программу вызывать подходящие реализации таких функций, как lseek.

Похоже, что вы используете bash на 32-битной платформе, а ваш двоичный файл bash не скомпилирован с поддержкой больших файлов. Теперь, когда вы читаете строку из обычного файла, bash использует внутренний буфер для чтения символов в пакетах для повышения производительности (подробнее см. Источник в builtins/read.def). Когда строка завершена, bash вызывает lseekперемотку смещения файла обратно к позиции конца строки, если какая-то другая программа заботится о позиции в этом файле. Вызов lseekпроисходит в zsyncfcфункции в lib/sh/zread.c.

Я не читал источник в деталях, но я предполагаю, что что-то не происходит гладко в точке перехода, когда абсолютное смещение отрицательно. Таким образом, bash завершает чтение с неправильными смещениями, когда заполняет свой буфер, после того, как он прошел отметку 2 ГБ.

Если мой вывод неверен и ваш bash фактически работает на 64-битной платформе или скомпилирован с поддержкой больших файлов, это определенно ошибка. Пожалуйста, сообщите об этом в ваш дистрибутив или апстрим .

В любом случае, оболочка не является подходящим инструментом для обработки таких больших файлов. Это будет медленно. Если возможно, используйте sed, иначе awk.

Жиль "ТАК - перестань быть злым"
источник
1
Мерси Жиль. Отличный ответ: полный, с достаточным количеством информации, чтобы понять проблему, даже для людей без сильного опыта CS (32 бита ...). (Жаворонки также помогают в опросе по номеру строки, и это должно быть подтверждено.) После этого у меня также возникла проблема с 32-разрядной версией, и я скачал исходный код, но еще не дошел до этого уровня анализа. Merci Encore, et Bonne Journée.
jfg956
4

Я не знаю о неправильном, но это, конечно, запутанно. Если ваши строки ввода выглядят так:

YYYY-MM-DD some text ...

Тогда действительно нет причин для этого:

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

Вы выполняете много подстрок, чтобы получить что-то, что выглядит ... именно так, как это уже выглядит в файле. Как насчет этого?

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

Это просто захватывает первые 10 символов из строки. Вы также можете отказаться от bashвсего и просто использовать awk:

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

Это захватывает дату в $1(первый разделенный пробелами столбец в каждой строке) и использует ее для генерации имени файла.

Обратите внимание, что в ваших файлах могут быть фиктивные строки журнала. То есть проблема может быть связана с вводом, а не с вашим сценарием. Вы можете расширить awkскрипт, чтобы пометить фиктивные строки следующим образом:

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

Это записывает строки, соответствующие YYYY-MM-DDвашим файлам журнала, и помечает строки, которые не начинаются с отметки времени на stdout.

larsks
источник
Нет поддельных строк в моем файле: cut -c 1-10 file.log | uniq -cдает мне ожидаемый результат. Я использую, ${line:0:4}-${line:5:2}-${line:8:2}потому что я помещу файл в каталог ${line:0:4}/${line:5:2}/${line:8:2}, и я упростил проблему (я обновлю формулировку проблемы). Я знаю, awkможет помочь мне здесь, но я столкнулся с другими проблемами, используя его. Я хочу понять проблему bash, а не найти альтернативные решения.
jfg956
Как вы сказали ... если вы «упростите» проблему в вопросе, вы, вероятно, не получите желаемых ответов. Я все еще думаю, что решение этой проблемы с помощью bash не совсем правильный способ обработки данных такого рода, но нет никаких причин, по которым это не должно работать.
larsks
Упрощенная задача дает неожиданный результат, который я представил в вопросе, поэтому я не думаю, что это упрощение. Более того, упрощенная задача дает такой же результат, как и cutутверждение, которое работает. Поскольку я хочу сравнивать яблоки с яблоками, а не с апельсинами, мне нужно сделать вещи максимально похожими.
jfg956
1
Я оставил вам вопрос, который может помочь выяснить, где дела идут не так, как
надо
2

Похоже, что вы хотите сделать, это:

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

closeСохраняет таблицы открытых файлов от заполнения.

Arcege
источник
Спасибо за решение awk. Я уже пришел с чем-то похожим. Мой вопрос состоял в том, чтобы понять ограничение bash, а не найти альтернативное решение.
jfg956