Bash scripting e arquivos grandes (bug): entrada com a leitura embutida de um redirecionamento dá resultado inesperado

16

Eu tenho um problema estranho com arquivos grandes e bash . Este é o contexto:

  • Eu tenho um arquivo grande: 75G e 400.000.000 + linhas (é um arquivo de log, meu ruim, eu deixo crescer).
  • Os primeiros 10 caracteres de cada linha são carimbos de hora no formato AAAA-MM-DD.
  • Quero dividir esse arquivo: um arquivo por dia.

Eu tentei com o seguinte script que não funcionou. A minha pergunta é sobre este script não funcionar, não sobre soluções alternativas .

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

Após a depuração, descobri o problema na variável new_file . Este script:

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

dá o resultado abaixo (eu coloquei o x es para manter os dados confidenciais, outros chars são os reais). Observe as dh e as strings mais curtas:

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

Não é um problema no formato do meu arquivo . O script cut -c 1-10 file.log | uniq -c fornece apenas carimbos de data e hora válidos. Curiosamente, uma parte da saída acima se torna com cut ... | uniq -c :

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

Podemos ver que, após a contagem uniq 4474604 , meu script inicial falhou.

Eu acertei um limite no bash que eu não sei, encontrei um bug no bash (parece improvável), ou fiz algo errado?

Atualizar :

O problema acontece depois de ler 2G do arquivo. Ele emite read e o redirecionamento não gosta de arquivos maiores que o 2G. Mas ainda procurando por uma explicação mais precisa.

Update2 :

Parece definitivamente um bug. Pode ser reproduzido com:

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

mas isso funciona bem como uma solução alternativa (parece que achei um uso útil de cat ):

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

Um bug foi arquivado no GNU e no Debian. As versões afetadas são bash 4.1.5 no Debian Squeeze 6.0.2 e 6.0.4.

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

Update3:

Graças a Andreas Schwab, que reagiu rapidamente ao meu relatório de bug, este é o patch que é a solução para este mau comportamento. O arquivo afetado é lib/sh/zread.c como Gilles apontou mais cedo:

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;

A variável r é usada para manter o valor de retorno de lseek . Como lseek retorna o deslocamento do início do arquivo, quando é mais de 2 GB, o valor int é negativo, o que faz com que o teste if (r >= 0) falhe onde deveria ter êxito.

    
por jfg956 01.03.2012 / 21:06

3 respostas

13

Você encontrou um bug no bash, do tipo. É um bug conhecido com uma correção conhecida.

Os programas representam um deslocamento em um arquivo como uma variável em algum tipo inteiro com um tamanho finito. Antigamente, todos usavam int para praticamente tudo, e o tipo int estava limitado a 32 bits, incluindo o bit de sinal, para que pudesse armazenar valores de -2147483648 a 2147483647. Atualmente existem diferentes digite nomes para coisas diferentes , incluindo off_t para um deslocamento em um arquivo.

Por padrão, off_t é um tipo de 32 bits em uma plataforma de 32 bits (permitindo até 2 GB) e um tipo de 64 bits em uma plataforma de 64 bits (permitindo até 8EB). No entanto, é comum compilar programas com a opção LARGEFILE, que alterna o tipo off_t para 64 bits e faz o programa chamar implementações adequadas de funções como lseek .

Parece que você está executando bash em uma plataforma de 32 bits e seu binário bash não é compilado com suporte a arquivos grandes. Agora, quando você lê uma linha de um arquivo normal, o bash usa um buffer interno para ler caracteres em lotes para desempenho (para mais detalhes, veja a fonte em builtins/read.def ). Quando a linha estiver completa, o bash chama lseek para rebobinar o deslocamento do arquivo de volta para a posição do final da linha, no caso de algum outro programa se importar com a posição naquele arquivo. A chamada para lseek ocorre na função zsyncfc em lib/sh/zread.c .

Eu não li a fonte com muitos detalhes, mas suponho que algo não está acontecendo suavemente no ponto de transição quando o deslocamento absoluto é negativo. Então o bash acaba lendo os offsets errados quando ele recarrega o buffer, depois de passar da marca de 2GB.

Se minha conclusão está errada e seu bash está, de fato, sendo executado em uma plataforma de 64 bits ou compilado com suporte a largefile, isso é definitivamente um bug. Por favor, denuncie para sua distribuição ou upstream .

Um shell não é a ferramenta certa para processar arquivos tão grandes assim mesmo. Vai ser lento. Use sed, se possível, caso contrário, awk.

    
por 02.03.2012 / 01:45
4

Eu não sei nada de errado, mas é certamente complicado. Se suas linhas de entrada são assim:

YYYY-MM-DD some text ...

Então, não há motivo para isso:

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

Você está fazendo um monte de trabalho de substring para acabar com algo que parece ... exatamente como ele já parece no arquivo. Que tal isso?

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

Isso apenas pega os 10 primeiros caracteres da linha. Você também pode dispensar bash totalmente e usar apenas awk :

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

Isso captura a data em $1 (a primeira coluna delimitada por espaços em branco em cada linha) e a usa para gerar o nome do arquivo.

Note que é possível que existam algumas linhas de log falsas em seus arquivos. Ou seja, o problema pode estar na entrada, não no script. Você pode estender o script awk para sinalizar linhas falsas como esta:

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

As linhas de escrita correspondem a YYYY-MM-DD aos seus arquivos de log e sinalizam linhas que não iniciam com um timestamp no stdout.

    
por 01.03.2012 / 21:43
2

Parece que o que você quer fazer é:

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

O close impede que a tabela de arquivos abertos seja preenchida.

    
por 01.03.2012 / 21:46

Tags