Como faço para acelerar um script baseado em uma pesquisa grep?

5

Existe um arquivo de texto muito grande com dois valores separados por vírgula:

78414387,10033
78769989,12668
78771319,13677
78771340,13759
80367563,16336
81634533,10025
82878571,10196
110059366,10218
110059411,10812
110059451,10067

Eu preciso procurar esses valores em um arquivo de log que está assim:

- delivery-AMC_prod_product 231825855936862016-07-02 00:00:52 c.c.i.d.s.d.DeliveryTopologyFactory$$anon$1$$anon$2 [INFO] ack: uid=57773c737e3d80d7def296c7| id=278832702| version=28| timestamp=1467432051000
- delivery-AMC_prod_product 231825855936862016-07-02 00:00:52 c.c.i.d.s.d.DeliveryTopologyFactory$$anon$1$$anon$2 [INFO] ack: uid=57773c732f18c26fe604fd04| id=284057302| version=9| timestamp=1467432051000
- delivery-AMC_prod_product 231825855936862016-07-02 00:00:52 c.c.i.d.s.d.DeliveryTopologyFactory$$anon$1$$anon$2 [INFO] ack: uid=57773c747e3d80d7def296c8| id=357229| version=1151| timestamp=1467432052000
- delivery-AMC_prod_product 231825855936862016-07-02 00:00:52 c.c.i.d.s.d.DeliveryTopologyFactory$$anon$1$$anon$2 [INFO] ack: uid=57773c742f18c26fe604fd05| id=279832706| version=35| timestamp=1467432052000
- delivery-AMC_prod_product 231825855936862016-07-02 00:00:52 c.c.i.d.s.d.DeliveryTopologyFactory$$anon$1$$anon$2 [INFO] ack: uid=57773c744697ddb976cf5a95| id=354171| version=503| timestamp=1467432052000
- delivery-AMC_prod_product 231825855936862016-07-02 00:00:53 c.c.i.d.s.d.DeliveryTopologyFactory$$anon$1$$anon$2 [INFO] ack: uid=57773c754697ddb976cf5a96| id=355638| version=1287| timestamp=1467432053000

Meu script:

#!/bin/bash
COUNT=0
while IFS=',' read ID VERSION; do
    VERSION='echo $VERSION |col -bx'
    if (grep "id=${ID}| version=$VERSION" worker-6715.log.2016-$1.log.* > /dev/null); then
            let "COUNT++"
    else
            echo "$ID, $VERSION FAIL"
            exit 2

    fi
done < recon.txt

echo "All OK, $COUNT checked"
  • Se eu cortar campos desnecessários do arquivo de log, isso aceleraria a execução?
  • Se eu criar um dispositivo de RAM e copiar o arquivo de log lá, isso aceleraria a execução ou seria meu Red Hat Linux 6 (Hedwig) armazenando o arquivo em cache? Alguma outra sugestão?
por user1700494 04.07.2016 / 13:02

5 respostas

4

Seu afunilamento está lendo o arquivo recon.txt linha por linha no shell. Para obter as linhas com falha, você poderia pré-processar as linhas nos logs para ficarem parecidas com as linhas em recon.txt e, em seguida, usar comm(1) para encontrar a diferença definida, talvez assim:

comm -23 \
    <(sort -u recon.txt) \
    <(sed 's/.*| id=\([0-9]*\)| version=\([0-9]*\)|.*/,/' worker-6715.log.2016-$1.log.* | \
        sort -u)

Isso pressupõe um shell que pode manipular <(...) construções. Além disso, observe que as linhas no resultado não preservam a ordem das linhas em recon.txt . Manter esse pedido seria muito mais difícil (e mais lento).

Se você também precisar das contagens de sucesso, faça o inverso, pré-processe recon.txt para que fique parecido com o que pode ser encontrado nos registros e use fgrep(1) ou grep -F para fazer a pesquisa. A definição de localidades para C também pode acelerar bastante as coisas em alguns sistemas. Assim:

COUNT=$( \
    sed 's/\([0-9]*\),\([0-9]*\)/| id=| version=|/' recon.txt | \
    LC_ALL=C fgrep -f - worker-6715.log.2016-$1.log.* | \
    wc -l )

Isso pressupõe que recon.txt não contenha duplicatas e que cada linha em recon.txt corresponda no máximo uma vez a todos os logs. Levantar a primeira restrição seria difícil. O segundo poderia ser levantado com um comm(1) cuidadosamente escolhido.

    
por 04.07.2016 / 13:26
1

Como apontado, o principal problema aqui é o loop do shell. Eu usaria awk para processar o arquivo de log primeiro, unindo os valores id e version e salvando o resultado em uma matriz, em seguida, lendo recon.txt e em cada linha, verifique se está na matriz, se não - salve o conteúdo da linha em uma variável t e exit imediatamente (executando o bloco END ). No bloco END , se houver uma linha salva em t , então exit 2 com uma mensagem, senão imprima a mensagem OK com no. de linhas de recon.txt :

awk 'NR==FNR{j=$9","$10;gsub(/[^0-9,]/, "", j);a[j]++;next}
!($0 in a){t=$0;{exit}}
END{if (t){print t, "FAIL"; exit 2}
else{print "All OK,", FNR, "checked"}}' logfile recon.txt

Isso pressupõe valores numéricos para os id e version e para os que estão sempre no 9º e 10º campo respectivamente. Se esse não for o caso, você pode usar um regex como o Sato - ou seja, se o awk flavor oferecer suporte a referências anteriores, caso contrário, algo assim seria feito:

NR==FNR{sub(/.* id=/, "");sub(/\| version=/, ",");sub(/\|.*/, ""));a[$0]++;next}
    
por 04.07.2016 / 16:47
0

Eu colocaria esses arquivos no DB (qualquer tipo que você quiser). Talvez nem mesmo importe o arquivo grande, mas coloque linha-a-linha do seu aplicativo que gera essa saída.

Em seguida, o mecanismo de banco de dados não desperdiçará recursos para chamar o grep a cada vez, mas usar a função interna compilada de C.

Não se esqueça de criar índices após a importação (se você importar o arquivo inteiro) ou após a criação da tabela (se você inserir registros linha por linha).

    
por 04.07.2016 / 19:47
0

Primeiro converta o arquivo recon.txt para um formato que seja rápido de usar para o grep -F:

perl -pe 's/,/| version=/' recon.txt > recon.grep-F

Então, sua situação é a descrita em link

Aqui o texto está adaptado para o seu problema:

A solução mais simples para o grep de um arquivo grande para muitos regexps é:

grep -f regexps.txt bigfile

Ou se os regexps forem sequências fixas:

grep -F -f recon.grep-F worker.log

Existem 2 fatores limitantes: CPU e E / S de disco. A CPU é fácil de medir: se o grep levar 90% da CPU (por exemplo, ao executar o topo), a CPU será um fator limitante e a paralelização acelerará isso. Caso contrário, a E / S de disco é o fator limitante e, dependendo do sistema de disco, pode ser mais rápido ou mais lento paralelizar. A única maneira de saber com certeza é medir.

Se a CPU é o fator limitante, a paralelização deve ser feita no regexps:

cat recon.grep-F | parallel --pipe -L1000 --round-robin grep -F -f - worker.log

Se uma linha corresponder a várias expressões regulares, a linha poderá ser duplicada. O comando irá iniciar um grep por CPU e ler bigfile uma vez por CPU, mas como isso é feito em paralelo, todas as leituras, exceto a primeira, serão armazenadas em cache na RAM. Dependendo do tamanho de regexp.txt, pode ser mais rápido usar --block 10m ao invés de -L1000. Se regexp.txt for muito grande para caber na RAM, remova --round-robin e ajuste -L1000. Isso fará com que bigfile seja lido mais vezes.

Alguns sistemas de armazenamento têm melhor desempenho ao ler vários blocos em paralelo. Isso vale para alguns sistemas RAID e para alguns sistemas de arquivos de rede. Para paralelizar a leitura de bigfile:

parallel --pipepart --block 100M -a worker.log -k grep -F -f recon.grep-F

Isso dividirá o bigfile em blocos de 100MB e executará o grep em cada um desses blocos. Para paralelizar a leitura de bigfile e regexp.txt, combine os dois usando --fifo:

parallel --pipepart --block 100M -a worker.log --fifo cat recon.grep-F \
\| parallel --pipe -L1000 --round-robin grep -f - {}

Se uma linha corresponder a várias expressões regulares, a linha poderá ser duplicada.

    
por 05.07.2016 / 09:05
0

Você está realizando a consulta "todos do conjunto A que não estão no conjunto B", além de transformar um conjunto no mesmo formato do outro. A maioria das abordagens para isso requer, em algum nível, loops aninhados.

Em sua versão, você está usando grep para o loop interno (testando todas as linhas nos logs) e bash para o externo (todas as linhas em recon.txt ). O loop bash gera vários subshells e processos a cada vez, o que é lento. Cada grep manipula internamente o laço interno, mas ainda requer um novo processo a cada vez.

A linha a seguir gera três novos processos (dois subshells e col ) a cada vez em volta do loop externo:

VERSION='echo $VERSION |col -bx'

E não está claro para qual finalidade ela serve. Se for necessário, você deve tentar colocar o col fora do loop, para que ele filtre todos os recon.txt em um único processo. Consulte também Por que usar um loop de shell para processar texto considerado uma prática ruim? .

A resposta de Sato Katsura evita o loop aninhado classificando os dois conjuntos de entrada, armazenando-os em arquivos temporários e então usando comm . Com a restrição de que as entradas são classificadas, comm não precisa usar loops aninhados.

No entanto, para arquivos de entrada grandes, os arquivos temporários também serão grandes ( recon.txt é reordenado, ou seja, a saída é do mesmo tamanho que sua entrada; os arquivos de log são reduzidos para o mesmo formato e reordenados). E há o tipo de tempo em si - sort usa o mergesort , que é o pior O (n log n) .

Se você deseja evitar arquivos temporários e classificação, é possível deixar grep manipular os dois loops:

cat worker-6715.log.2016-$1.log.* |
 sed 's/.*| id=\([0-9]*\)| version=\([0-9]*\)|.*/,/' |
 grep -F -x -v -f - recon.txt || ( echo -n "Lines OK: " ; wc -l < recon.txt)

Como antes, uma única invocação de sed (copiada dessa resposta) transforma o formato de log no formato recon.txt . Então grep -v informa apenas as linhas de recon.txt que não foram encontradas nos logs. -f - pega os registros reformatados da entrada padrão (evitando arquivos temporários, mas significando que grep precisa armazená-los todos na memória). -F para corresponder a sequências fixas em vez de padrões, -x para corresponder apenas a linhas inteiras.

Como grep foi invertido para informar que não corresponde, ele retornará "sucesso" se houver linhas não encontradas. Portanto, o || exibirá somente a mensagem e a contagem total se todas as linhas forem encontradas.

    
por 05.07.2016 / 09:10