118 votos

RM en un directorio con millones de archivos

Antecedentes: el servidor físico, unos dos años de edad, 7200 RPM, SATA conectado a una tarjeta RAID 3Ware, ext3 FS montado noatime y data=ordered, y no bajo la loca de carga, kernel 2.6.18-92.1.22.el5, el tiempo de actividad de 545 días. Directorio no contiene los subdirectorios, solo millones de pequeños (~100 byte) de archivos, con algunos de los más grandes (de unos pocos KB).

Tenemos un servidor que ha salido un poco de cuco en el transcurso de los últimos meses, pero sólo nos di cuenta el otro día cuando se empieza no se puede escribir en un directorio debido a que contienen demasiados archivos. En concreto, se comenzó a tirar este error en /var/log/messages:

ext3_dx_add_entry: Directory index full!

El disco en cuestión tiene un montón de i-nodos restantes:

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

Así que supongo que eso significa que hemos alcanzado el límite de cuántas entradas se puede estar en el directorio del archivo en sí. Ni idea de cuántos archivos que sería, pero no puede ser más, como se puede ver, de tres millones o así. No que eso es bueno, la mente! Pero eso es parte de mi pregunta: ¿qué es exactamente la que el límite superior? Es sintonizable? Antes de que se me gritó-quiero afinar hacia abajo; de esta enorme directorio causado todo tipo de problemas.

De todos modos, hemos rastreado la cuestión en el código que estaba generando todos esos archivos, y hemos corregido. Ahora estoy atascado con eliminar el directorio.

Un par de opciones aquí:

  1. rm -rf (dir)

    He intentado esto en primer lugar. Me di por vencido y matado después de que se había ejecutado durante un día y medio sin ningún impacto apreciable.

  2. unlink(2) en el directorio: Definitivamente vale la pena considerar, pero la pregunta es si sería más rápido para eliminar los archivos dentro de un directorio a través de fsck que eliminar a través de desvincular(2). Es decir, de una manera o de otra, tengo que marcar a los inodos como no utilizados. Esto supone, por supuesto, que te puedo decir fsck no dejar caer las entradas a los archivos en /lost+found; de lo contrario, me acabo de mudar a mi problema. Además de todas las otras preocupaciones, después de leer sobre esto un poco más, resulta que probablemente iba a tener que llamar a algunos internos FS funciones, como ninguno de los unlink(2) variantes puedo encontrar me permitiría sólo alegremente eliminar un directorio con entradas en él. Pooh.
  3. while [ true ]; do ls -Uf | head -n 10000 | xargs rm -f 2>/dev/null; done )

    Esto es en realidad la versión abreviada; el real estoy corriendo, que sólo añade algunos avances de informes y limpio con un stop cuando se nos acaba de archivos para eliminar, es:

    exportación i=0;
    el tiempo ( mientras [ verdadero ]; do
     ls-Uf | head-n 3 | grep-qF '.png' || break;
     ls-Uf | head-n 10000 | xargs rm-f 2>/dev/null;
     exportación i=$(($i+10000));
     echo "$i...";
    hecho )

    Esto parece estar funcionando bastante bien. Mientras escribo esto, se ha eliminado de 260.000 archivos en los últimos treinta minutos o así.

Ahora, para las preguntas:

  1. Como se mencionó anteriormente, es el directorio de entrada de límite ajustables?
  2. ¿Por qué lo hizo tomar "real 7m9.561s / user 0m0.001s / sys 0m0.001s" para eliminar un único archivo que fue el primero en la lista devuelta por ls -U, y se tomaba unos diez minutos para eliminar la primera 10.000 entradas con el comando #3, pero ahora está arrastrando a lo largo de bastante feliz? Para que la materia, se eliminan 260.000 en unos treinta minutos, pero es ahora tomado otros quince minutos para eliminar más de 60.000. ¿Por qué las enormes oscilaciones en la velocidad?
  3. Hay una mejor manera de hacer este tipo de cosas? No almacenar millones de archivos en un directorio; sé que es una tontería, y que no habría sucedido en mi reloj. Buscando en google el problema y mirando a través de SF y ofrece una gran cantidad de variaciones en find que no va a ser significativamente más rápido que mi enfoque para varios auto-evidentes razones. Pero no la elimina-a través de-fsck idea tiene patas? O algo completamente distinto? Estoy con ganas de escuchar out-of-the-box (o dentro de-la-no-conocido-box) de pensamiento.

Gracias por leer la pequeña novela; siéntase libre de hacer preguntas y voy a estar seguro para responder. Yo también voy a actualizar a la pregunta con el número final de los archivos y la duración de la secuencia de comandos de eliminación corrió una vez que tengo eso.



Final de salida de secuencia de comandos!:

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

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

Por lo tanto, tres millones de archivos borrados en un poco más de cuatro horas.

90voto

Matthew Ife Puntos 12680

Mientras que una de las principales causas de este problema es ext3 rendimiento con millones de archivos, la verdadera causa root de este problema es diferente.

Cuando un directorio debe aparecer readdir() se llama en el directorio que genera una lista de archivos. readdir es un posix llamada, pero la verdadera llamada del sistema linux se utiliza aquí es el llamado 'getdents'. Getdents lista de entradas de directorio mediante la cumplimentación de un búfer de entradas.

El problema se debe principalmente al hecho de que readdir() utiliza un fijo el tamaño de búfer de 32 kb para recuperar los archivos. Como un directorio hace más y más grande (el tamaño aumenta a medida que se agregan archivos) ext3 vuelve más lento y más lento para ir a buscar las entradas y adicionales readdir del 32 kb tamaño del búfer es sólo suficiente para incluir una fracción de las entradas en el directorio. Esto hace que readdir a bucle una y otra vez y se invoca el sistema caro llamar a más y más.

Por ejemplo, en un directorio de prueba que he creado con más de 2,6 millones de archivos en el interior, la ejecución de "ls -1|wc-l" muestra una gran salida de strace de muchos getdent llamadas al sistema.

$ 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
...

Además, el tiempo de permanencia en este directorio fue significativa.

$ time ls -1 | wc -l
2616044

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

El método para hacer de este un proceso más eficiente es llamar getdents manualmente con un mucho mayor de búfer. Esto mejora significativamente el rendimiento.

Ahora, tu no debes llamar getdents usted mismo de forma manual por lo que no interfaz existe para uso normalmente (revise la página man de getdents a ver!), sin embargo, usted puede llamar manualmente y hacer que su sistema llame a la invocación de la manera más eficiente.

Esto reduce drásticamente el tiempo que se tarda en recuperar estos archivos. Escribí un programa que hace esto.

$ time ./dentls bigfolder >out.txt

real    0m2.355s
user    0m0.326s
sys 0m1.995s

Casi diez veces más eficaz! Sospecho que el más grande es el directorio de la manera más eficiente, esto terminaría siendo.

He proporcionado la fuente de este programa a continuación. Si desea eliminar, quite el comentario de la desvinculación de la línea. Esto va a reducir drásticamente el rendimiento de la imagino. También se evita la impresión/de la desvinculación de cualquier cosa que no es un archivo.

Va a escupir los nombres de archivo a stdout. Probablemente debería redirigir a esta salida. Se puede utilizar para eliminar los archivos que están fuera del programa de después, si quería.

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

#define _GNU_SOURCE
#include <search.h>     /* Defines tree functions */
#include <dirent.h>     /* Defines DT_* constants */
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <string.h>

/* Because most filesystems use btree to store dents
 * its very important to perform an in-order removal
 * of the file contents. Performing an 'as-is read' of
 * the contents causes lots of btree rebalancing
 * that has significantly negative effect on unlink performance
 */

/* Tests indicate that performing a ascending order traversal
 * is about 1/3 faster than a descending order traversal */
int compare_fnames(const void *key1, const void *key2) {
  return strcmp((char *)key1, (char *)key2);
}

void walk_tree(const void *node, VISIT val, int lvl) {
  int rc = 0;
  switch(val) {
  case leaf:
    rc = unlink(*(char **)node);
    break;
  /* End order is deliberate here as it offers the best btree
   * rebalancing avoidance.
   */
  case endorder:
    rc = unlink(*(char **)node);
  break;
  default:
    return;
    break;
  }

  if (rc < 0) {
    perror("unlink problem");
    exit(1);
  }

}

void dummy_destroy(void *nil) {
  return;
}

void *tree = NULL;

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

int main(const int argc, const char** 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;

    /* Test we have a directory path */
    if (argc < 2) {
        fprintf(stderr, "You must supply a valid directory path.\n");
        exit(1);
    }

    const char *path = argv[1];

    /* Standard sanity checking stuff */
    if (access(path, R_OK) < 0) {
        perror("Could not access directory");
        exit(1);
    }

    if (lstat(path, &dstat) < 0) {
        perror("Unable to lstat path");
        exit(1);
    }

    if (!S_ISDIR(dstat.st_mode)) {
        fprintf(stderr, "The path %s is not a directory.\n", path);
        exit(1);
    }

    /* Allocate a buffer of equal size to the directory to store dents */
    if ((buffer = malloc(dstat.st_size+10240)) == NULL) {
        perror("malloc failed");
        exit(1);
    }

    /* Open the directory */
    if ((dirfd = open(path, O_RDONLY)) < 0) {
        perror("Open error");
        exit(1);
    }

    /* Switch directories */
    fchdir(dirfd);

    while (bufcount = syscall(SYS_getdents, dirfd, buffer, dstat.st_size+10240)) {
        offset = 0;
        dent = buffer;
        while (offset < bufcount) {
            /* Dont 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) {
                    /* Sort all our files into a binary tree */
            if (!tsearch(dent->d_name, &tree, compare_fnames)) {
                      fprintf(stderr, "Cannot acquire resources for tree!\n");
                      exit(1);
                    }
                    totalfiles++;
                }
            }
            offset += dent->d_reclen;
            dent = buffer + offset;
        }
    }
    fprintf(stderr, "Total files: %d\n", totalfiles);
    printf("Performing delete..\n");

    twalk(tree, walk_tree);
    printf("Done\n");
    close(dirfd);
    free(buffer);
    tdestroy(tree, dummy_destroy);
}

Mientras esto no se combate el problema fundamental subyacente (un montón de archivos, en un sistema de ficheros que se realiza mal). Sus probabilidades de ser mucho, mucho más rápido que muchas de las alternativas que se están publicados.

Como previsión, se debe quitar el afectado directorio y rehacerla después. Directorios sólo aumentan de tamaño y se puede quedar mal rendimiento incluso con un par de archivos en el interior debido a que el tamaño del directorio.

Edit: he revisado este día de hoy, porque la mayoría de los sistemas de ficheros almacenan sus estructuras de directorios en un árbol formato, la orden de que eliminar los archivos también es importante. Uno debe evitar el reequilibrio de la btree al realizar la desvinculación. Como tal, he añadido una especie antes de las eliminaciones se producen.

El programa ahora (en mi sistema) eliminar 1000000 archivos en 43 segundos. El más cercano programa para esto fue rsync -a --delete que tuvo 60 segundos (que también hace las eliminaciones en fin, demasiado, pero no se realiza una búsqueda de directorio eficaz).

35voto

ring0 Puntos 2732

El data=writeback opción de montaje merece ser tratado, con el fin de prevenir el diario del sistema de archivos. Esto sólo se debe hacer durante el tiempo de eliminación, que hay un riesgo, sin embargo, si el servidor está apagado o reiniciado durante la operación de eliminación.

Según esta página,

Algunas aplicaciones muestran muy significativos en la mejora de la velocidad cuando se utiliza. Por ejemplo, las mejoras de la velocidad puede ser visto (...) cuando las aplicaciones, crear y eliminar grandes volúmenes de archivos pequeños.

La opción se establece en fstab o durante la operación de montaje, sustitución de data=ordered con data=writeback. El sistema de archivos que contiene los archivos a borrar tiene que ser volver a montar.

32voto

jftuga Puntos 3798

¿Sería posible hacer copias de seguridad todos los archivos de este sistema de archivos a una ubicación de almacenamiento temporal, formatear la partición y luego restaurar los archivos?

12voto

Alex J. Roberts Puntos 111

No es por el archivo de directorio límite en ext3 sólo el sistema de archivos de inodo límite (creo que hay un límite en el número de subdirectorios).

Usted todavía puede tener problemas después de la eliminación de los archivos.

Cuando un directorio cuenta con millones de archivos, el directorio de la entrada en sí es muy grande. La entrada de directorio tiene que ser analizado para cada operación de quitar, y que toma diferentes cantidades de tiempo para cada archivo, dependiendo de donde su entrada se encuentra. Por desgracia, incluso después de que todos los archivos han sido eliminados de la entrada de directorio conserva su tamaño. Por lo tanto, las operaciones que requieren la digitalización de la entrada de directorio todavía va a tardar un largo tiempo, incluso si el directorio está vacío ahora. La única manera de resolver este problema es cambiar el nombre del directorio, crear uno nuevo con el nombre antiguo, y la transferencia de los archivos restantes a la nueva. A continuación, elimine el nombre de uno.

4voto

Alexandre Puntos 131

encontrar simplemente no funcionó para mí, incluso después de cambiar los parámetros de la ext3 fs según lo sugerido por los usuarios anteriores. Consume demasiada memoria. Este script PHP hizo el truco - uso de la CPU rápido, insignificante, uso de memoria insignificante:

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

Publiqué un informe de error con respecto a este problema con hallazgo: http://savannah.gnu.org/bugs/?31961

EnMiMaquinaFunciona.com

EnMiMaquinaFunciona es una comunidad de administradores de sistemas en la que puedes resolver tus problemas y dudas.
Puedes consultar las preguntas de otros sysadmin, hacer tus propias preguntas o resolver las de los demás.

Powered by: