Como posso otimizar este comando Unix?

Como posso otimizar este comando Unix?

O comando a seguir leva cerca de 10 minutos para gerar o resultado

find . -name "muc*_*_20160920_*.unl*" | xargs zcat |
    awk -F "|" '{if($14=="20160920100643" && $22=="567094398953") print $0}'| head

Como posso melhorar seu desempenho?

Responder1

Isso já está bastante otimizado. É difícil saber o que é o gargalo sem saber mais detalhes como:

  • tipo de armazenamento (HD, SSD, rede, RAID)
  • número e tamanho médio dos arquivos correspondentes
  • número de diretórios e outros arquivos não correspondentes
  • número de campos em cada linha
  • comprimento médio de uma linha

Coisas que você pode fazer em qualquer caso:

  • substitua -print | xargspor -exec cmd {} +ou -print0 | xargs -r0se você find/ xargsapoiá-lo. -print | xargsnão é apenas errado, mas também mais caro, pois xargsprecisa decodificar caracteres para descobrir quais estão em branco e fazer algum processamento de cotação caro.
  • corrija o código do idioma para C ( export LC_ALL=C). Como todos os caracteres envolvidos aqui ( |e dígitos decimais para o conteúdo do arquivo e letras latinas, ponto final e sublinhado para os nomes dos arquivos) fazem parte do conjunto de caracteres portátil, se o seu conjunto de caracteres for UTF-8 ou algum outro conjunto de caracteres multibyte, alternando para C com seu conjunto de caracteres de byte único garantirá muito trabalho para finde awk.
  • simplifique a awkparte para: awk -F "|" '$14 == "20160920100643" && $22 == "567094398953"'.
  • como você está canalizando a saída para head, convém desabilitar o buffer de saída para awkque ele produza essas 10 linhas o mais cedo possível. Com gawkou mawk, você pode usar fflush()para isso. Ou você pode adicionar um if (++n == 10) exitarquivo awk.

Resumindo:

(export LC_ALL=C
find . -name "muc*_*_20160920_*.unl*" -exec zcat {} + |
  awk -F "|" '$14 == "20160920100643" && $22 == "567094398953" {
    print; if (++n == 10) exit}')

Se a CPU for o gargalo, em um sistema GNU multi-core, você pode tentar:

(export LC_ALL=C
find . -name "muc*_*_20160920_*.unl*" -print0 |
  xargs -r0P 4 -n 100 sh -c '
    zcat "$@" | 
      awk -F "|" "\$14 == "20160920100643" && \$22 == "567094398953" {
        print; fflush()}"' sh | head)

Para executar 4 zcat | awktrabalhos em paralelo em lotes de 100 arquivos.

Se 20160920100643for um carimbo de data e hora, você pode excluir os arquivos que foram modificados pela última vez antes disso. Com GNU ou BSD find, adicione um arquivo -newermt '2016-09-20 10:06:42'.

Se as linhas tiverem um grande número de campos, você receberá uma penalidade por awkdividi-las e alocar tantos $ncampos. Usar uma abordagem que considere apenas os primeiros 22 campos poderia acelerar as coisas:

grep -E '^([^|]*\|){13}20160920100643(\|[^|]*){7}\|567094398953(\||$)'

em vez do awkcomando. Com GNU grep, adicione a --line-bufferedopção de gerar as linhas o mais cedo possível na abordagem paralela ou -m 10parar após 10 partidas na abordagem não paralela.

Resumindo, se a CPU é o gargalo e você tem pelo menos 4 núcleos de CPU em seu sistema e há pelo menos 400 arquivos muc* e você está em um sistema GNU (onde grepgeralmente é significativamente mais rápido que GNU awk):

(export LC_ALL=C
find . -name "muc*_*_20160920_*.unl*" -newermt '2016-09-20 10:06:42' -print0 |
  xargs -r0P 4 -n 100 sh -c '
    zcat "$@" | 
      grep --line-buffered -E \
        "^([^|]*\|){13}20160920100643(\|[^|]*){7}\|567094398953(\||$)"
  ' sh | head)

Observe que na abordagem paralela, você pode obter a saída dos grepcomandos misturada (embora com o buffer de linha e as linhas fornecidas tenham menos de alguns kilobytes de tamanho, os limites das linhas devem ser preservados).

Responder2

A resposta de @Stéphane Chazelas fornece muitos detalhes sobre como você pode otimizar o pipeline de comando

find . -name "muc*_*_20160920_*.unl*" | xargs zcat |
    awk -F "|" '{if($14=="20160920100643" && $22=="567094398953") print $0}'| head

Vou fornecer outra maneira de abordar o problema em que você realmente mede onde está gastando mais tempo. Depois de descobrir onde o tempo é gasto, você pode determinar o que fazer a respeito. Se você deseja melhorar seu tempo de execução de 10 minutos, otimizar uma etapa que leva 2 segundos é quase inútil.

Quando olho para o pipeline de comando, três coisas chamam minha atenção:

  1. find .- Como é a estrutura de diretórios? Quantos arquivos por diretório? O diretório é local para o sistema no qual o comando está sendo executado? Um sistema de arquivos remoto será ummuitoMais devagar.
  2. -name "muc*_*_20160920_*.unl*"- Quão próximos estão todos os nomes de arquivos na estrutura de diretórios? Eles estão todos "próximos" do nome e difíceis de combinar com a CPU? Porquetodoarquivo na árvore de diretórios deve ter seu nome lido no disco e comparado ao padrão.
  3. xargs zcat- xargsNão me parece que será um grande problema de desempenho, especialmente comparado aos findproblemas acima e ao zcatpróprio. Mesmo que sejam 10.000 ou mesmo 10.000.000 nomes de arquivos, o tempo usado para passar e analisar apenas os nomes é quase certamente insignificante comparado ao tempo gastoencontraros nomes e depois abrir e descompactar todos os arquivos. Qual o tamanho dos arquivos? Porque você está descompactando todo otodoarquivo que corresponde findao padrão de nome de arquivo do seu.

Como você pode determinar qual é o principal problema de desempenho? Meça o desempenho de cada comando no pipeline. (Verhttps://stackoverflow.com/questions/13294554/how-to-use-gnu-time-with-pipelinepara obter detalhes sobre como cronometrar um pipeline inteiro.) Você pode executar os comandos a seguir e ver quanto tempo cada etapa contribui para o tempo de processamento de todo o pipeline:

/usr/bin/time find .- Isso informa quanto tempo leva para percorrer sua árvore de diretórios. Se for lento, você precisará de um sistema de armazenamento melhor. Limpe o cache do seu sistema de arquivosantes de cronometrar isso para obter uma medição do pior caso, execute o cronometrado findnovamente e veja quanto o cache afeta o desempenho. E se o diretório não for local, tente executar o comando no sistema real em que os arquivos estão.

/usr/bin/time find . -name "muc*_*_20160920_*.unl*"- Isso lhe dirá quanto tempo leva para combinar os padrões dos nomes dos arquivos. Novamente, limpe o cache do sistema de arquivos e execute-o duas vezes.

/usr/bin/time bash -c "find . -name 'muc*_*_20160920_*.unl*' | xargs zcat > /dev/null"- Suspeito que este seja o principal componente do longo tempo de execução do seu pipeline. Se esse for o problema, paralelizar os zcatcomandos da resposta de Stéphane Chazelas pode ser a melhor resposta.

Continue adicionando etapas do pipeline de comando original ao que está sendo testado até descobrir onde você passa a maior parte do tempo. Mais uma vez, suspeito que seja o zcatpasso. Nesse caso, talvez a zcatparalelização postada por @Stéphane Chazelas ajude.

Paralelizar zcatpode não ajudar - pode atéferirdesempenho e processamento lento. Com apenas um zcatem execução por vez, o IO pode estar em um bom padrão de streaming que minimiza as buscas no disco. Com vários zcatprocessos em execução ao mesmo tempo, as operações de E/S podem competir e, na verdade, retardar o processamento à medida que os cabeçotes do disco precisam procurar e qualquer leitura antecipada torna-se menos eficaz.

Se a zcatetapa for seu principal gargalo de desempenho e a execução de vários zcatprocessos ao mesmo tempo não ajudar ou realmente atrasar você, seu pipeline está limitado por E/S e você precisará resolver o problema usando um armazenamento mais rápido.

E novamente - se o diretório não for local na máquina em que você está executando o pipeline de comando, tente executá-lo na máquina em que o sistema de arquivos realmente está.

Responder3

Conforme observado nos comentárioszgrepé a melhor escolha para esse tipo de tarefas comestrela globopção que permite usar **comoall path inside the directory except hidden

shopt -s globstar
zgrep -m 10 '^\([^|]*|\)\{13\}20160920100643|\([^|]*|\)\{7\}567094398953' ./**muc*_*_20160920_*.unl*
shopt -u globstar

Responder4

Conforme apontado, não é possível dar a resposta correta sem alguns detalhes extras.

locate -0 -b -r '^muc.*_.*_20160920_.*.unl.*gz' | 
   xargs -0  zcat |
   awk -F "|" '$14=="20160920100643" && $22=="567094398953"'| head
  • 1: localizar (se disponível) é muito mais rápido que **ou find; A expressão regular usada deve ser ajustada...

  • 2 e 3: o filtro do PO

Como @rudimeier apontou sabiamente, há problemas em relação à disponibilidade e ao estado de atualização do locate. (por exemplo, na maioria das máquinas Linux, o local é atualizado diariamente; desta forma, não será possível encontrar os arquivos criados hoje)

No entanto, se localizar estiver disponível, isso produzirá uma aceleração impressionante.

Seria interessante que o OP pudesse fornecer time ...as diversas soluções

informação relacionada