Como remover muitos arquivos de forma eficiente

Hoje eu tive uma necessidade um tanto quanto inusitada: criar um número razoavelmente grande de arquivos para fazer alguns testes num software que eu estava desenvolvendo. Criei então 300.000 arquivos num diretório qualquer (/tmp/teste para fins de exemplo).

Depois de usar os arquivos, precisei removê-los. Como o número de arquivos era grande, o comando rm não pôde ser utilizado, porque a lista de argumentos (nomes dos 300.000 arquivos ultrapassava o tamanho máximo da linha de comando). Como já passei por uma situação semelhante no passado, eu já tinha uma carta (comando) na manga: find. No entanto, me ficou a seguinte questão: qual a forma mais eficiente de remover uma grande quantidade de arquivos de um diretório simples, isto é, sem subdiretórios?

Observe que as medições realizadas e apresentadas neste artigo dependem de uma séria de fatores, como a carga do sistema, a velocidade dos discos, a velocidade do processador, caches de comandos do sistema operacional etc. Mas é importante observar que, para manter o ambiente o mais uniforme possível, todos os comandos foram executados no mesmo computador, tentando manter aproximadamente o mesmo uso do mesmo (carga) durante os testes, e sem nenhuma priorização especial dos comandos (renice).

Opção óbvia:

rm -f *

Não funcionou, porque a lista de argumentos ultrapassa o tamanho máximo permitido para uma de comando. O que acontece é que, quando executamos comando rm *, o shell, que é o interpretador de comandos do sistema (Linux) interpreta o caractere ‘*’ como um coringa que significa tudo, então ele obtém a lista de todos os arquivos e adiciona na linha de comando de execução do comando rm. Como a lista de todos os arquivos ultrapassa o tamanho máximo permitido para a linha de comando de execução do programa, o resultado é uma mensagem de erro. Exemplo:

cd /tmp/teste
rm *
bash: /bin/rm: Argument list too long

Caso a lista de argumentos não fosse tão grande, esta seria a opção óbvia e, provavelmente, a mais eficiente.

Hora de tentar outra alternativa e esta talvez seja a opção mais usada nestes casos, pelo menos até agora para quem nunca tinha lido este artigo: o comando find. A finalidade básica deste comando é buscar arquivos e/ou diretórios, e podemos executar comandos externos sobre cada item encontrado e, o principal para o nosso caso, é que seu uso não gera problemas com a linha de comando, porque não usamos nenhum coringa interpretado pelo interpretador de comandos do sistema.

Para remover todos os arquivos, considerando que já estejamos no diretório onde os arquivos deverão ser removidos, o comando é o seguinte:

find . -type f -exec rm {} \;

Funcionou, mas demorou um pouco (na verdade, muito): 01:39:00 (sim, mais de uma hora e meia).

O motivo para tanta demora é simples: o comando find procura todos os arquivos (apenas arquivos) e, para cada arquivo encontrado, ele executa o comando rm e é isso que torna o comando ineficiente: a chamada de um comando externo, como o rm, demanda tempo para o carregamento e execução do mesmo pelo sistema operacional. Se o comando de remoção estivesse incorporado no comando find, o resultado seria muito melhor, no entanto, o comando find foi criado para procurar arquivos, e não removê-los.

Terceira opção (esta acredito que poucos tenham pensado…):

# Crie um diretório vazio
mkdir /tmp/empty

# Use o rsync para copiar todos os arquivos do diretório vazio (ou seja nenhum)
# para o diretório com muitos arquivos e solicite que os arquivos que existirem
# no destino e não existirem na origem sejam removidos:

rsync -a --delete /tmp/empy /tmp/teste

Este comando foi um “pouco” mais rápido que o comando anterior: 00:00:04. Isso mesmo, 4 segundos! O que acontece aqui é que o rsync, além de ser muito otimizado, ele possui recursos para remoção de arquivos sem precisar executar nenhum comando externo que, em teoria, deve ser muito mais eficiente.

A ideia aqui é a seguinte: como o rsync possui recursos interessantes como, por exemplo, remover arquivos que existam no diretório de destino e que não existam no diretório de origem, que é o que eu utilizei: como não há nenhum arquivo no diretório de origem, então todos os arquivos existentes no diretório de destino serão removidos.

Além disso, como não havia nenhum arquivo no diretório de origem, nenhuma comparação precisou ser realizada, bastando remover todo o conteúdo do diretório de destino.

Quarta opção: apagar tudo usando um programa minúsculo em Python. Neste caso, criei o seguinte programa num editor de textos:

#!/usr/bin/python3

import time
import os
import sys

if __name__ == "__main__":
    if len(sys.argv) > 1:
        if os.path.isdir(sys.argv[1]):
            count = 0
            start = time.time()
            for f in os.listdir(sys.argv[1]):
                if os.path.isfile(sys.argv[1] + "/" + f):
                    os.unlink(sys.argv[1] + "/" + f)
                    count += 1
                #
            #
            end = time.time()
            print("Removidos {} arquivos em {:0.2f}s".format(count, end - start))
        else:
            print("Parametro deve ser um diretorio")
        #
    #
#

Algumas considerações sobre o programa acima:

  • Ele só apaga arquivos contidos no primeiro nível do diretório, isto é, subdiretórios e seus conteúdos são ignorados;

  • Ele marca o tempo gasto na remoção dos arquivos. É normal que o programa demore mais tempo do que o apresentado pelo programa, principalmente porque quando este programa é executado, há um tempo de carregamento do interpretador Python e dos módulos usados pelo programa;

  • Possui algumas firulas, como marcar o tempo gasto na remoção e contar quantos itens foram removidos.

A execução deste programa resultou no seguinte (chamei o programa acima de delall.py):

time ./delall.py /tmp/teste
Removidos 300000 arquivos em 3.87s

real        0m3.915s
user        0m1.187s
sys 0m2.652s

Observe que este tempo pode variar em função dos vários fatores citados no início deste artigo, e a variação pode ser relativamente grande. Por exemplo, num dos testes, mais precisamente na primeira vez que executei o programa Python, o resultado foi o seguinte:

time ./delall.py teste/
Removidos 300000 arquivos em 9.63s

real        0m10.091s
user        0m1.180s
sys 0m4.016s

Ou seja, 10 segundos. Isso ocorreu porque, em condições normais de uso, isto é, sem sobrecarga do sistema, o Linux mantém alguns códigos em cache, o que significa que, se você executar o mesmo comando várias vezes seguidas, é bem possível que eles gastem menos tempo após a primeira execução. O que acontece é que, como o Linux mantém dados lidos do disco em cache, quando um comando é executado pela segunda vez, há uma boa chance de que parte dos dados a serem lidos do disco ainda estejam no cache, o que torna o carregamento do programa muito mais rápido. No entanto, observe que as não haverá alteração nas tarefas do programa, mas apenas nos dados a serem carregados do disco.

E agora? Bem, matei minha curiosidade:

  • Encontrei duas formas bastante eficientes de remover uma grande quantidade de arquivos de um diretório;

  • Pensei fora da caixa: usei um comando tipicamente de cópia de arquivos para removê-los (rsync);

  • Fiquei feliz em comprovar, mais uma vez, que programas simples que podemos manter em nosso arsenal podem salvar o dia (ou parte dele).

E para você, esta informação serviu para algo?

Fui