Expansão do parâmetro Bash: Melhores práticas para velocidade?

2

Eu estava imaginando se alguém conhece alguma prática recomendada ou se há alguma documentação sobre esse tópico:

O cenário é procurar / grep nos arquivos de log. Para enfatizar, usarei ls . Então, digamos que eu corra ls para listar uma série de arquivos dentro do diretório

/var/log/remote/serverX.domain.local/ps/ps2.log.2014-mm-dd.gz

Onde mm e dd são números de mês e dia, há também uma bateria inteira de servidores além do serverX (para o exemplo que eu uso 4,5,9,10 (estes são servidores reais)

Eu corri ls com tempo usando primeiro uma lista de parâmetros em chaves e mudei depois para um asterisco para ver as diferenças. Claro que não esperava que o asterisco tivesse melhor desempenho.

   emartinez@serverlog:~$ time ls /var/log/remote/server{4,5,9,10}.domain.local/ps/ps2.log.2014-10-0{1,2}.gz
    /var/log/remote/server10.domain.local/ps/ps2.log.2014-10-01.gz  
    ...
    /var/log/remote/server5.domain.local/ps/ps2.log.2014-10-02.gz

real    0m0.004s
user    0m0.010s
sys     0m0.000s

Depois, substituo a última chave por um asterisco:

time ls /var/log/remote/server{4,5,9,10}.domain.local/ps/ps2.log.2014-10-0*.gz

E recebo as seguintes estatísticas:

    real      0m0.028s
    user      0m0.020s
    sys   0m0.020s

Isso é muito diferente, embora haja apenas duas opções, pois as datas disponíveis são apenas 01 e 02 de outubro.

Eu fiz o teste novamente, mas desta vez substituí os meses por uma lista {1..12} sendo os resultados consistentes:

ps2.log.2014-{1..12}-0{1,2}.gz : real 0m0.010s
ps2.log.2014-{1..12}-0*.gz     : real 0m0.168s

É muita diferença para apenas um asterisco! Faz sentido que isso seja mais lento, mas existem alguns pontos de referência sobre o quanto é mais lento e há alguma melhor prática descrita em algum lugar?

    
por runlevel0 03.10.2014 / 10:05

2 respostas

2

Pode parecer que prefix-* deve ser fácil de transformar, por exemplo, prefix-1 prefix-2 , pois estamos acostumados a ver as listagens de diretório classificadas. Mas acontece que muito poucos sistemas de arquivos podem realmente produzir listagens de nomes de arquivos classificadas e, além disso, não existe uma API padrão para solicitar listagens de nomes de arquivos classificadas.

Se um programa - como ls ou bash - precisar de uma lista de nomes de arquivos, ele precisará ler toda a lista de diretórios, que será produzida em uma ordem aleatória (geralmente a ordem está relacionada ao tempo de criação, às vezes é baseada em um hash do nome do arquivo, mas em muito bom caso, não é uma simples ordem alfabética). Portanto, para resolver prefix-* , você precisa ler o diretório inteiro e verificar cada nome de arquivo em relação ao padrão. Como a parte mais cara desse procedimento é a leitura do diretório, faz pouca diferença a complexidade do padrão ou quantos nomes de arquivo correspondem ao padrão.

Em resumo, a expansão do nome do caminho ("resolving globs") será lenta em um diretório grande. Essa é uma razão para evitar diretórios grandes, em vez de um motivo para evitar globs.

Mas há outro ponto de dados importante: prefix-{1,2} é não expansão do nome do caminho. É " expansão de contraventamentos " e é uma extensão do padrão shell Posix (embora quase todos os shells implementam isso). Há um número de diferenças entre expansão de chave e expansão de nome de caminho, mas uma diferença importante e relevante é que a expansão de brace não depende da existência de arquivos . A expansão do suporte é uma operação de string simples.

Consequentemente, prefix-{1,2} será sempre expandido para prefix-1 prefix-2 , independentemente de esses arquivos existirem ou não. Isso significa que ele pode ser expandido sem ler o diretório e sem stat em qualquer arquivo. Claramente, isso será rápido. Mas há uma desvantagem: não há como saber se o resultado corresponde a arquivos reais.

Considere o seguinte exemplo simples:

$ mkdir test && cd test
$ touch file1 file2 file4
$ ls file*
file1 file2 file4
$ ls file[1234]
file1 file2 file4
$ ls file{1,2,3,4}
ls: cannot access file3: No such file or directory
file1 file2 file4

Ponto final: A expansão do nome do caminho é feita pelo shell, não por ls . Com a expansão do nome do caminho, poderíamos usar também echo :

$ echo file*
file1 file2 file4
$ echo file[1234]
file1 file2 file4

E echo produzirá a lista um pouco mais rápida, porque todo echo precisa fazer é imprimir seus argumentos, enquanto ls (que recebe os mesmos argumentos) tem que stat cada argumento para verificar se é um arquivo. Esse stat - que não é uma chamada barata - é totalmente redundante no caso de uma expansão de nome de caminho, porque o shell já usou a lista de diretórios para filtrar a lista de arquivos e, portanto, cada nome de arquivo foi passado para ls é conhecido por existir. (A menos que o glob não corresponda a nenhum arquivo).

Além disso, echo é um bash interno, portanto, pode ser chamado sem criar um processo filho.

No caso da expansão de chaves, no entanto, echo não produz o mesmo resultado:

$ echo file{1,2,3,4}
file1 file2 file3 file4

Portanto, poderíamos usar ls , redirecionando sua saída de erro para o intervalo de bits:

$ ls file{1,2,3,4}
file1 file2 file4

e, neste caso, as chamadas stat não são redundantes porque o shell nunca validou os nomes dos arquivos.

A menos que seus diretórios sejam realmente grandes, nada disso fará muita diferença e o glob será muito mais fácil de escrever. Se os seus diretórios são realmente enormes, você deve considerar dividi-los em subdiretórios menores.

Por exemplo, em vez de caminhos como:

/var/log/remote/serverX.domain.local/ps/ps2.log.2014-mm-dd.gz

você pode usar:

/var/log/remote/serverX/domain.local/ps/ps2.log.2014-mm-dd-gz

E se você estiver mantendo os registros para sempre, talvez queira extrair o ano para evitar o aumento infinito do tamanho do diretório:

/var/log/remote/2014/serverX/domain.local/ps/ps2.log.2014-mm-dd-gz

( 2014 é deliberadamente repetido).

A divisão dos diretórios geralmente será uma grande vitória, pois fornece um mecanismo para otimizar a globalização. Como mencionado acima, o shell não pode otimizar

/var/log/remote/server[2357].domain.local/ps/ps2.log.2014-10-*-gz

mas pode otimizar

/var/log/remote/server[2357]/domain.local/ps/ps2.log.2014-10-*-gz

No segundo caso, server[2357] precisa ser correspondido apenas aos nomes dos diretórios e, uma vez feito isso, ps2.log.2014-10-*-gz só precisa ser comparado aos nomes dos arquivos nos diretórios correspondentes.

    
por 04.10.2014 / 02:37
1

Expansão da Shell é sempre executada em uma ordem específica; expansão da chave é executada primeiro, expansão do nome do arquivo é executada por último.

Assim, um comando como

echo {1..3}*

primeiro é expandido para

echo 1* 2* 3*

, a expansão do nome do arquivo é executada para 1* , 2* e 3* . Cada expansão envolve passar por todos os nomes de arquivos no diretório e compará-los com o padrão.

À medida que o número de palavras e / ou o número de arquivos no diretório crescem, isso se torna gradualmente mais lento. Mesmo em um diretório vazio,

shopt -s nullglob  # print nothing for non-matching words
echo {1..1000000}* # prints nothing
shopt -u nullglob  # back to the default

leva quase cinco segundos na minha máquina. Isso não é de todo surpreendente se você considerar que a expansão do nome do arquivo é realizada um milhão de vezes ...

Uma alternativa muito mais rápida é evitar combinar ambos os tipos de expansão de shell sempre que possível .

O comando

echo [1-1000000]* # also prints nothing

procura pelos mesmos nomes de arquivos, mas usa um único padrão. Isso leva 33 milissegundos na minha máquina.

O uso de colchetes em vez de chaves traz benefícios adicionais:

$ touch 13
$ echo {1..20}*
13 13
$ echo [1..20]*
13

A primeira abordagem encontrou o arquivo duas vezes, pois ele corresponde aos padrões 1* e 13* . Isso não acontece com a expansão do nome do arquivo "puro".

    
por 04.10.2014 / 06:14