Script bash e arquivos grandes (bug): a entrada com a leitura incorporada de um redirecionamento fornece resultados inesperados

16

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

  • Eu tenho um arquivo grande: 75G e mais de 400.000.000 de linhas (é um arquivo de log, que pena, deixei crescer).
  • Os 10 primeiros caracteres de cada linha são carimbos de hora no formato AAAA-MM-DD.
  • Eu quero dividir esse arquivo: um arquivo por dia.

Eu tentei com o seguinte script que não funcionou. Minha pergunta é sobre esse script não funcionar, não 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, encontrei o problema na new_filevariável 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 (coloquei xes para manter os dados confidenciais, outros caracteres são reais). Observe as dhseqüências de caracteres e as 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 -cfornece apenas carimbos de 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 atingi um limite no bash que não conheço, encontrei um bug no bash (parece improvável) ou fiz algo errado?

Atualização :

O problema ocorre após a leitura de 2G do arquivo. Costuras reade redirecionamento não gostam de arquivos maiores que 2G. Mas ainda procurando uma explicação mais precisa.

Update2 :

Definitivamente, parece 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 eu achei um uso útil cat):

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

Um bug foi arquivado no GNU e Debian. As versões afetadas são bash4.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 erros, este é o patch que é a solução para esse mau comportamento. O arquivo impactado é lib/sh/zread.ccomo Gilles apontou antes:

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 rvariável é usada para armazenar o valor de retorno de lseek. Como lseekretorna o deslocamento desde o início do arquivo, quando ele ultrapassa 2 GB, o intvalor é negativo, o que causa if (r >= 0)falha no teste onde deveria ter êxito.

jfg956
fonte
11
Você pode replicar o problema com conjuntos menores de dados de entrada? São sempre as mesmas linhas de entrada que resultam nesses problemas?
Larsks # 1/12
@larks: boa pergunta. O problema sempre começa na linha 13.520.918 (na verdade duas vezes para os testes que fiz). O tamanho do arquivo antes desta linha é 2.147.487.726. Parece que há um limite de 32 bits aqui, mas não exatamente como estamos um pouco acima de 2 ^ 31 (2.147.483.648), mas exatamente no limite do buffer de 4K (2 ^ 31 + 4K = 2.147.487.744). As linhas anterior e seguinte são linhas normais de 100 a 200 caracteres.
Jfg956
Testado em um segundo arquivo (aproximadamente do mesmo tamanho): o problema começa na linha 13.522.712 e o arquivo tem 2.147.498.679 bytes de tamanho antes dessa linha. Costura apontar na direção de um limite da readdeclaração em bash.
Jfg956

Respostas:

13

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

Programas representam um deslocamento em um arquivo como uma variável em algum tipo de número inteiro com um tamanho finito. Antigamente, todo mundo usava intpraticamente tudo, e o inttipo era limitado a 32 bits, incluindo o sinal, para armazenar valores de -2147483648 a 2147483647. Atualmente, existem nomes de tipos diferentes para coisas diferentes , inclusive off_tpara 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 muda o tipo off_tpara 64 bits de largura e faz com que o programa chame implementações adequadas de funções como lseek.

Parece que você está executando o bash em uma plataforma de 32 bits e o seu binário bash não é compilado com suporte a arquivos grandes. Agora, quando você lê uma linha de um arquivo regular, o bash usa um buffer interno para ler caracteres em lotes para desempenho (para obter mais detalhes, consulte a fonte em builtins/read.def). Quando a linha é concluída, o bash chama lseekpara retroceder o deslocamento do arquivo de volta à posição final da linha, caso algum outro programa se preocupe com a posição nesse arquivo. A chamada para lseekacontece na zsyncfcfunção em lib/sh/zread.c.

Não li a fonte com muitos detalhes, mas suponho que algo não esteja ocorrendo sem problemas no ponto de transição quando o deslocamento absoluto é negativo. Portanto, o bash acaba lendo as compensações erradas quando reabastece seu buffer, depois de ultrapassado a marca de 2 GB.

Se minha conclusão estiver errada e seu bash estiver de fato rodando em uma plataforma de 64 bits ou compilado com suporte a arquivos grandes, isso é definitivamente um bug. Por favor, reporte 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.

Gilles 'SO- parar de ser mau'
fonte
11
Merci Gilles. Ótima resposta: completa, com informações suficientes para entender o problema, mesmo para pessoas sem formação em CS (32 bits ...). (Os larsks também ajudam a questionar o número da linha e devem ser reconhecidos.) Depois disso, também pensei em um problema de 32 bits e baixei a fonte, mas ainda não estava nesse nível de análise. Merci encore, e bom jornal.
Jfg956
4

Não sei o que é errado, mas certamente é complicado. Se suas linhas de entrada estiverem assim:

YYYY-MM-DD some text ...

Então não há realmente nenhuma razão para isso:

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

Você está fazendo muito trabalho de substring para acabar com algo que parece ... exatamente da maneira que já aparece no arquivo. Que tal agora?

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 bashcompletamente e apenas usar awk:

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

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

Observe que é possível que haja algumas linhas de log falsas em seus arquivos. Ou seja, o problema pode estar na entrada, não no seu script. Você pode estender o awkscript 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
}
'

Isso grava as linhas correspondentes YYYY-MM-DDaos seus arquivos de log e sinaliza as linhas que não começam com um carimbo de data / hora no stdout.

larsks
fonte
Nenhuma linha falsa no meu arquivo: cut -c 1-10 file.log | uniq -cfornece o resultado esperado. Estou usando ${line:0:4}-${line:5:2}-${line:8:2}porque colocarei o arquivo em um diretório ${line:0:4}/${line:5:2}/${line:8:2}e simplifiquei o problema (atualizarei a declaração do problema). Eu sei que awkpode me ajudar aqui, mas eu tive outros problemas ao usá-lo. O que eu quero é entender o problema bashe não encontrar soluções alternativas.
Jfg956
Como você disse ... se você "simplificar" o problema na pergunta, provavelmente não encontrará as respostas desejadas. Ainda acho que resolver isso com o bash não é realmente o caminho certo para processar esse tipo de dados, mas não há razão para que isso não funcione.
Larsks # 1/12
O problema simplificado fornece o resultado inesperado que apresentei na pergunta; portanto, não acho que seja uma simplificação excessiva. Além disso, o problema simplificado fornece um resultado semelhante ao da cutdeclaração que funciona. Como quero comparar maçãs com maçãs, não com laranjas, preciso tornar as coisas o mais parecidas possível.
Jfg956
11
Deixei-lhe uma pergunta que pode ajudar a descobrir onde as coisas estão indo mal ...
larsks
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

A closemantém a tabela de arquivo aberto de encher.

Arcege
fonte
Obrigado pela solução awk. Eu já venho com algo semelhante. Minha pergunta era entender a limitação do bash, não encontrar uma solução alternativa.
Jfg956