rm em um diretório com milhões de arquivos

99

Antecedentes: servidor físico, cerca de dois anos, unidades SATA de 7200 RPM conectadas a uma placa RAID de 3Ware, ext3 FS montadas noatime e dados = pedidos, sem carga maluca, kernel 2.6.18-92.1.22 .el5, tempo de atividade 545 dias. O diretório não contém subdiretórios, apenas milhões de arquivos pequenos (~ 100 bytes), com alguns maiores (alguns KB).

Temos um servidor que ficou um pouco cuco ao longo dos últimos meses, mas só o notamos no outro dia quando ele começou a ser incapaz de gravar em um diretório devido a ele conter muitos arquivos. Especificamente, ele começou a lançar esse erro em / var / log / messages:

ext3_dx_add_entry: Directory index full!

O disco em questão tem muitos inodes restantes:

Filesystem            Inodes   IUsed   IFree IUse% Mounted on
/dev/sda3            60719104 3465660 57253444    6% /

Então, estou supondo que isso significa que atingimos o limite de quantas entradas podem estar no próprio arquivo de diretório. Não tenho ideia de quantos arquivos seriam, mas não pode ser mais, como você pode ver, do que três milhões ou mais. Não que isso seja bom, lembre-se! Mas essa é uma parte da minha pergunta: exatamente qual é esse limite superior? É sintonizável? Antes de eu gritar, quero sintonizar down ; este enorme diretório causou todos os tipos de problemas.

De qualquer forma, rastreamos o problema no código que gerava todos esses arquivos e o corrigimos. Agora estou preso com a exclusão do diretório.

Algumas opções aqui:

  1. %código%

    Eu tentei isso primeiro. Eu desisti e o matei depois de ter corrido por um dia e meio sem qualquer impacto perceptível.

  2. unlink (2) no diretório: Definitivamente vale a pena considerar, mas a questão é se seria mais rápido deletar os arquivos dentro do diretório via fsck do que deletar via unlink (2). Ou seja, de uma forma ou de outra, tenho que marcar esses inodes como não utilizados. Isso pressupõe, é claro, que eu possa dizer ao fsck para não remover as entradas dos arquivos em / lost + found; caso contrário, acabei de mudar o meu problema. Além de todas as outras preocupações, depois de ler sobre isso um pouco mais, acontece que eu provavelmente teria que chamar algumas funções FS internas, já que nenhuma das variantes unlink (2) que eu posso encontrar me permitiria apagar alegremente um diretório com entradas nele. Pooh
  3. %código%

    Esta é realmente a versão abreviada; o real que estou executando, que apenas adiciona alguns relatórios de progresso e uma parada limpa quando ficamos sem arquivos para excluir, é:

    export i=0;
    time ( while [ true ]; do
      ls -Uf | head -n 3 | grep -qF '.png' || break;
      ls -Uf | head -n 10000 | xargs rm -f 2>/dev/null;
      export i=$(($i+10000));
      echo "$i...";
    done )

    Isso parece estar funcionando bem. Enquanto escrevo isso, ele excluiu 260.000 arquivos nos últimos 30 minutos.

Agora, para as perguntas:
  1. Como mencionado acima, o limite de entrada por diretório é ajustável?
  2. Por que foi preciso "7m9.561s reais / usuário 0m0.001s / sys 0m0.001s" para excluir um único arquivo que foi o primeiro da lista retornado por rm -rf (dir) e demorou talvez dez minutos para ser excluído as primeiras 10.000 entradas com o comando em # 3, mas agora ele está indo bem feliz? Aliás, eliminou 260.000 em cerca de trinta minutos, mas agora são necessários mais quinze minutos para excluir mais 60.000. Por que as grandes oscilações de velocidade?
  3. Existe uma maneira melhor de fazer esse tipo de coisa? Não armazene milhões de arquivos em um diretório; Eu sei que isso é bobagem, e isso não teria acontecido no meu relógio. Pesquisando no Google o problema e procurando por SF e SO oferece muitas variações em while [ true ]; do ls -Uf | head -n 10000 | xargs rm -f 2>/dev/null; done ) que não serão significativamente mais rápidas do que a minha abordagem por várias razões óbvias. Mas a idéia do delete-via-fsck tem alguma perna? Ou algo completamente diferente? Estou ansioso para ouvir o pensamento inovador (ou dentro da caixa não tão conhecida).
Obrigado por ler o pequeno romance; sinta-se à vontade para fazer perguntas e não deixarei de responder. Também atualizarei a pergunta com o número final de arquivos e por quanto tempo o script de exclusão foi executado assim que eu tiver isso.

Saída final do script!:

2970000...
2980000...
2990000...
3000000...
3010000...

real    253m59.331s
user    0m6.061s
sys     5m4.019s

Então, três milhões de arquivos foram deletados em pouco mais de quatro horas.

    
por BMDan 23.09.2010 / 01:57

23 respostas

30

A opção data=writeback mount merece ser tentada, para impedir o registro no diário do sistema de arquivos. Isso deve ser feito somente durante o tempo de exclusão, mas há um risco, no entanto, se o servidor estiver sendo desligado ou reinicializado durante a operação de exclusão.

De acordo com esta página ,

Some applications show very significant speed improvement when it is used. For example, speed improvements can be seen (...) when applications create and delete large volumes of small files.

A opção é configurada em fstab ou durante a operação de montagem, substituindo data=ordered por data=writeback . O sistema de arquivos que contém os arquivos a serem excluídos deve ser remontado.

    
por 26.09.2010 / 07:49
74

Embora uma das principais causas desse problema seja o desempenho do ext3 com milhões de arquivos, a causa raiz real desse problema é diferente.

Quando um diretório precisa ser listado, readdir () é chamado no diretório que gera uma lista de arquivos. readdir é uma chamada posix, mas a chamada real do sistema Linux que está sendo usada aqui é chamada de 'getdents'. Getdents lista as entradas do diretório preenchendo um buffer com entradas.

O problema é principalmente o fato de que o readdir () usa um tamanho de buffer fixo de 32Kb para buscar arquivos. À medida que um diretório fica maior e maior (o tamanho aumenta à medida que os arquivos são adicionados), o ext3 fica mais lento e lento para buscar entradas e o tamanho adicional do buffer de 32 Kb do readdir é suficiente apenas para incluir uma fração das entradas no diretório. Isso faz com que o readdir faça um loop repetidamente e invoque a cara chamada do sistema repetidamente.

Por exemplo, em um diretório de teste criado com mais de 2,6 milhões de arquivos, a execução de "ls -1 | wc-l" mostra uma grande saída de strace de muitas chamadas de sistema getdent.

$ strace ls -1 | wc -l
brk(0x4949000)                          = 0x4949000
getdents(3, /* 1025 entries */, 32768)  = 32752
getdents(3, /* 1024 entries */, 32768)  = 32752
getdents(3, /* 1025 entries */, 32768)  = 32760
getdents(3, /* 1025 entries */, 32768)  = 32768
brk(0)                                  = 0x4949000
brk(0x496a000)                          = 0x496a000
getdents(3, /* 1024 entries */, 32768)  = 32752
getdents(3, /* 1026 entries */, 32768)  = 32760
...

Além disso, o tempo gasto neste diretório foi significativo.

$ time ls -1 | wc -l
2616044

real    0m20.609s
user    0m16.241s
sys 0m3.639s

O método para tornar isso um processo mais eficiente é chamar getdents manualmente com um buffer muito maior. Isso melhora significativamente o desempenho.

Agora, você não deve chamar getdents manualmente, então não existe nenhuma interface para usá-lo normalmente (verifique a página de manual de getdents para ver!), mas você pode chamá-lo manualmente e fazer sua chamada de chamada do sistema é muito mais eficiente.

Isso reduz drasticamente o tempo necessário para buscar esses arquivos. Eu escrevi um programa que faz isso.

/* I can be compiled with the command "gcc -o dentls dentls.c" */

#define _GNU_SOURCE

#include <dirent.h>     /* Defines DT_* constants */
#include <err.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

struct linux_dirent {
        long           d_ino;
        off_t          d_off;
        unsigned short d_reclen;
        char           d_name[256];
        char           d_type;
};

static int delete = 0;
char *path = NULL;

static void parse_config(
        int argc,
        char **argv)
{
    int option_idx = 0;
    static struct option loptions[] = {
      { "delete", no_argument, &delete, 1 },
      { "help", no_argument, NULL, 'h' },
      { 0, 0, 0, 0 }
    };

    while (1) {
        int c = getopt_long(argc, argv, "h", loptions, &option_idx);
        if (c < 0)
            break;

        switch(c) {
          case 0: {
              break;
          }

          case 'h': {
              printf("Usage: %s [--delete] DIRECTORY\n"
                     "List/Delete files in DIRECTORY.\n"
                     "Example %s --delete /var/spool/postfix/deferred\n",
                     argv[0], argv[0]);
              exit(0);                      
              break;
          }

          default:
          break;
        }
    }

    if (optind >= argc)
      errx(EXIT_FAILURE, "Must supply a valid directory\n");

    path = argv[optind];
}

int main(
    int argc,
    char** argv)
{

    parse_config(argc, argv);

    int totalfiles = 0;
    int dirfd = -1;
    int offset = 0;
    int bufcount = 0;
    void *buffer = NULL;
    char *d_type;
    struct linux_dirent *dent = NULL;
    struct stat dstat;

    /* Standard sanity checking stuff */
    if (access(path, R_OK) < 0) 
        err(EXIT_FAILURE, "Could not access directory");

    if (lstat(path, &dstat) < 0) 
        err(EXIT_FAILURE, "Unable to lstat path");

    if (!S_ISDIR(dstat.st_mode))
        errx(EXIT_FAILURE, "The path %s is not a directory.\n", path);

    /* Allocate a buffer of equal size to the directory to store dents */
    if ((buffer = calloc(dstat.st_size*3, 1)) == NULL)
        err(EXIT_FAILURE, "Buffer allocation failure");

    /* Open the directory */
    if ((dirfd = open(path, O_RDONLY)) < 0) 
        err(EXIT_FAILURE, "Open error");

    /* Switch directories */
    fchdir(dirfd);

    if (delete) {
        printf("Deleting files in ");
        for (int i=5; i > 0; i--) {
            printf("%u. . . ", i);
            fflush(stdout);
            sleep(1);
        }
        printf("\n");
    }

    while (bufcount = syscall(SYS_getdents, dirfd, buffer, dstat.st_size*3)) {
        offset = 0;
        dent = buffer;
        while (offset < bufcount) {
            /* Don't print thisdir and parent dir */
            if (!((strcmp(".",dent->d_name) == 0) || (strcmp("..",dent->d_name) == 0))) {
                d_type = (char *)dent + dent->d_reclen-1;
                /* Only print files */
                if (*d_type == DT_REG) {
                    printf ("%s\n", dent->d_name);
                    if (delete) {
                        if (unlink(dent->d_name) < 0)
                            warn("Cannot delete file \"%s\"", dent->d_name);
                    }
                    totalfiles++;
                }
            }
            offset += dent->d_reclen;
            dent = buffer + offset;
        }
    }
    fprintf(stderr, "Total files: %d\n", totalfiles);
    close(dirfd);
    free(buffer);

    exit(0);
}

Embora isso não combata o problema fundamental subjacente (muitos arquivos, em um sistema de arquivos que executa mal nele). É provável que seja muito, muito mais rápido do que muitas das alternativas postadas.

Como premeditação, deve-se remover o diretório afetado e refazê-lo depois. Os diretórios só aumentam de tamanho e podem permanecer com desempenho insatisfatório mesmo com alguns arquivos internos devido ao tamanho do diretório.

Editar: eu limpei isso um pouco. Adicionado uma opção para permitir que você apague na linha de comando em tempo de execução e removeu um monte de coisas de árvores que, honestamente olhando para trás era questionável na melhor das hipóteses. Também foi mostrado para produzir corrupção de memória.

Agora você pode fazer dentls --delete /my/path

Novos resultados. Baseado em um diretório com 1,82 milhões de arquivos.

## Ideal ls Uncached
$ time ls -u1 data >/dev/null

real    0m44.948s
user    0m1.737s
sys 0m22.000s

## Ideal ls Cached
$ time ls -u1 data >/dev/null

real    0m46.012s
user    0m1.746s
sys 0m21.805s


### dentls uncached
$ time ./dentls data >/dev/null
Total files: 1819292

real    0m1.608s
user    0m0.059s
sys 0m0.791s

## dentls cached
$ time ./dentls data >/dev/null
Total files: 1819292

real    0m0.771s
user    0m0.057s
sys 0m0.711s

Fiquei surpreso que isso ainda funcionasse tão bem!

    
por 06.11.2011 / 20:06
31

Seria possível fazer backup de todos os outros arquivos deste sistema de arquivos para um local de armazenamento temporário, reformatar a partição e restaurar os arquivos?

    
por 23.09.2010 / 02:27
12

Não há limite de arquivos por diretório no ext3, apenas o limite de inode do sistema de arquivos (acho que há um limite no número de subdiretórios).

Você ainda pode ter problemas depois de remover os arquivos.

Quando um diretório possui milhões de arquivos, a própria entrada de diretório se torna muito grande. A entrada de diretório deve ser verificada para cada operação de remoção, e isso leva vários períodos de tempo para cada arquivo, dependendo de onde sua entrada está localizada. Infelizmente, mesmo depois de todos os arquivos terem sido removidos, a entrada do diretório mantém seu tamanho. Portanto, outras operações que exigem a verificação da entrada do diretório ainda levarão muito tempo, mesmo que o diretório esteja vazio. A única maneira de resolver esse problema é renomear o diretório, criar um novo nome antigo e transferir os arquivos restantes para o novo. Em seguida, exclua o renomeado.

    
por 23.09.2010 / 07:45
5

Eu não o comparei, mas esse cara fez :

rsync -a --delete ./emptyDirectoty/ ./hugeDirectory/
    
por 04.06.2013 / 13:52
4

find simplesmente não funcionou para mim, mesmo depois de alterar os parâmetros do ext3 fs como sugerido pelos usuários acima. Consumido demais, muita memória. Este script PHP fez o truque - uso rápido e insignificante da CPU, uso insignificante de memória:

<?php 
$dir = '/directory/in/question';
$dh = opendir($dir)) { 
while (($file = readdir($dh)) !== false) { 
    unlink($dir . '/' . $file); 
} 
closedir($dh); 
?>

Postei um relatório de bug referente a esse problema com o link

    
por 23.12.2010 / 20:54
3

Recentemente, enfrentei um problema semelhante e não consegui fazer com que a sugestão data=writeback do ring0 funcionasse (possivelmente devido ao fato de os arquivos estarem na minha partição principal). Ao pesquisar soluções alternativas, deparei com isso:

tune2fs -O ^has_journal <device>

Isso desativará completamente o registro no diário, independentemente da opção data fornecida a mount . Eu combinei isso com noatime e o volume tinha dir_index set, e pareceu funcionar muito bem. A exclusão realmente terminou sem que eu precise matá-la, meu sistema permaneceu responsivo e agora está de volta e funcionando (com o registro no diário de volta) sem problemas.

    
por 24.04.2012 / 00:29
3

Certifique-se de fazer:

mount -o remount,rw,noatime,nodiratime /mountpoint

que também deve acelerar um pouco as coisas.

    
por 27.09.2010 / 04:03
2

É um comando muito lento. Experimente:

find /dir_to_delete ! -iname "*.png" -type f -delete
    
por 23.09.2010 / 06:04
2

dir_index está definido para o sistema de arquivos? ( tune2fs -l | grep dir_index ) Caso contrário, habilite-o. Geralmente é para o novo RHEL.

    
por 27.09.2010 / 06:18
1

Minha opção preferida é a abordagem newfs, já sugerida. O problema básico é, novamente, como já foi observado, a varredura linear para manipular a exclusão é problemática.

rm -rf deve estar próximo do ideal para um sistema de arquivos local (o NFS seria diferente). Mas em milhões de arquivos, 36 bytes por nome de arquivo e 4 por inode (um palpite, não verificando o valor de ext3), são 40 * milhões, para serem mantidos na RAM apenas no diretório.

Por suposto, você está debatendo a memória cache de metadados do sistema de arquivos no Linux, para que os blocos de uma página do arquivo de diretório sejam removidos enquanto você ainda está usando outra parte, apenas para acessar a página do cache novamente quando o próximo arquivo é excluído. O ajuste do desempenho do Linux não é minha área, mas / proc / sys / {vm, fs} / provavelmente contém algo relevante.

Se você puder pagar o tempo de inatividade, considere ativar o recurso dir_index. Ele alterna o índice de diretório de linear para algo muito mais ideal para exclusão em diretórios grandes (b-trees com hash). tune2fs -O dir_index ... seguido por e2fsck -D funcionaria. No entanto, embora eu tenha certeza de que isso ajudaria antes , existem problemas, não sei como a conversão (e2fsck com -D ) se comporta ao lidar com um diretório v.large existente. Backups + chupar e ver.

    
por 26.09.2010 / 14:05
1

Obviamente, não maçãs para maçãs aqui, mas eu configurei um pequeno teste e fiz o seguinte:

Criados 100.000 arquivos de 512 bytes em um diretório ( dd e /dev/urandom em um loop); esqueci de esperar, mas demorou cerca de 15 minutos para criar esses arquivos.

Executei o seguinte para excluir os arquivos:

ls -1 | wc -l && time find . -type f -delete

100000

real    0m4.208s
user    0m0.270s
sys     0m3.930s 

Esta é uma caixa Pentium 4 2.8GHz (algumas centenas de GB IDE 7200 RPM, penso; EXT3). Kernel 2.6.27.

    
por 26.09.2010 / 17:33
1

Às vezes, o Perl pode fazer maravilhas em casos como esse. Você já tentou se um pequeno script como este poderia superar o bash e os comandos básicos do shell?

#!/usr/bin/perl 
open(ANNOYINGDIR,"/path/to/your/directory");
@files = grep("/*\.png/", readdir(ANNOYINGDIR));
close(ANNOYINGDIR);

for (@files) {
    printf "Deleting %s\n",$_;
    unlink $_;
}

Ou outra abordagem, talvez ainda mais rápida, do Perl:

#!/usr/bin/perl
unlink(glob("/path/to/your/directory/*.png")) or die("Could not delete files, this happened: $!");

EDIT: Eu apenas experimentei meus scripts em Perl. O mais detalhado faz algo certo. No meu caso eu tentei isso com um servidor virtual com 256 MB de RAM e meio milhão de arquivos.

time find /test/directory | xargs rm resultados:

real    2m27.631s
user    0m1.088s
sys     0m13.229s

comparado a

time perl -e 'opendir(FOO,"./"); @files = readdir(FOO); closedir(FOO); for (@files) { unlink $_; }'

real    0m59.042s
user    0m0.888s
sys     0m18.737s
    
por 27.09.2010 / 17:24
1

Pelo que eu me lembro, a exclusão de inodes em sistemas de arquivos ext é O (n ^ 2), então quanto mais arquivos você excluir, mais rápido o resto irá.

Houve uma vez que fui confrontado com um problema similar (embora minhas estimativas tivessem um tempo de exclusão de ~ 7h), no final, a rota sugerida pelo jftuga no primeiro comentário .

    
por 30.09.2010 / 02:33
1

Há alguns anos, encontrei um diretório com 16 milhões de arquivos XML no sistema de arquivos / . Devido à criticidade do servidor, usamos o seguinte comando que levou cerca de 30 horas para concluir:

perl -e 'for(<*>){((stat)[9]<(unlink))}'

Era um disco rígido antigo de <>> 7200 rpm , e apesar do gargalo do IO e dos picos de CPU, o antigo servidor da Web continuou seu serviço.

    
por 13.05.2016 / 15:44
0

Bem, esta não é uma resposta real, mas ...

Seria possível converter o sistema de arquivos para ext4 e ver se as coisas mudam?

    
por 27.09.2010 / 17:02
0
Tudo bem, isso foi abordado de várias maneiras no restante do tópico, mas achei que lançaria meus dois centavos. O desempenho culpado no seu caso provavelmente é readdir. Você está recebendo de volta uma lista de arquivos que não são necessariamente sequenciais no disco, o que está causando o acesso ao disco em todo o lugar quando você o desvincula. Os arquivos são pequenos o suficiente para que a operação de desconexão provavelmente não saia muito do espaço. Se você ler readir e ordenar por inode ascendente, provavelmente obterá melhor desempenho. Então readdir em ram (classificar por inode) - > desvincular - > lucro.

O inode é uma aproximação aproximada aqui, eu acho ... mas baseando-se no seu caso de uso, pode ser bastante preciso ...

    
por 30.09.2010 / 01:31
0

Eu provavelmente teria sacado um compilador C e feito o equivalente moral do seu script. Ou seja, use opendir(3) para obter um identificador de diretório, use readdir(3) para obter o nome dos arquivos e, em seguida, calcule os arquivos à medida que eu os desvinculo e, de vez em quando, imprima "% d arquivos excluídos" (e possivelmente o tempo decorrido) ou data e hora atual.

Eu não espero que seja visivelmente mais rápido que a versão shell script, é que eu estou acostumado a ter que extrair o compilador de vez em quando, ou porque não há uma maneira limpa de fazer o que eu quero do shell ou porque, embora factível em shell, é improdutivamente lento dessa maneira.

    
por 26.09.2010 / 14:24
0

É provável que você tenha problemas de reescrita no diretório. Tente excluir os arquivos mais novos primeiro. Veja as opções de montagem que adiarão o writeback para o disco.

Para uma barra de progresso, tente executar algo como rm -rv /mystuff 2>&1 | pv -brtl > /dev/null

    
por 26.09.2010 / 20:16
0

Veja como eu excluo os milhões de arquivos de rastreamento que às vezes podem ser reunidos em um grande servidor de banco de dados Oracle:

for i in /u*/app/*/diag/*/*/*/trace/*.tr? ; do rm $i; echo -n . ;  done

Acho que isso resulta em uma exclusão bastante lenta que tem baixo impacto no desempenho do servidor, geralmente algo nos moldes de uma hora por milhão de arquivos em uma configuração "típica" de 10.000 IOPS.

Geralmente, levará vários minutos até que os diretórios sejam verificados, a lista de arquivos inicial seja gerada e o primeiro arquivo seja excluído. De lá em diante, a. é ecoado para cada arquivo excluído.

O atraso causado pelo eco no terminal provou ser um atraso suficiente para evitar qualquer carga significativa enquanto a exclusão está progredindo.

    
por 10.10.2014 / 11:31
-1

Você pode usar os recursos de paralelização "xargs":

ls -1|xargs -P nb_concurrent_jobs -n nb_files_by_job rm -rf
    
por 30.09.2010 / 16:30
-2
ls|cut -c -4|sort|uniq|awk '{ print "rm -rf " $1 }' | sh -x
    
por 30.09.2010 / 16:06
-2

na verdade, este é um pouco melhor se o shell que você usa fizer a expansão da linha de comando:

ls|cut -c -4|sort|uniq|awk '{ print "echo " $1 ";rm -rf " $1 "*"}' |sh
    
por 30.09.2010 / 22:02