Pregunta rm en un directorio con millones de archivos


Antecedentes: servidor físico, de dos años de antigüedad, unidades SATA de 7200 RPM conectadas a una tarjeta 3Ware RAID, ext3 FS noatime montada y datos = ordenados, sin carga loca, núcleo 2.6.18-92.1.22.el5, tiempo de actividad 545 días . El directorio no contiene ningún subdirectorio, solo millones de archivos pequeños (~ 100 bytes), con algunos más grandes (unos pocos KB).

Tenemos un servidor que se ha vuelto un poco extraño en el transcurso de los últimos meses, pero solo lo notamos el otro día cuando comenzó a no poder escribir en un directorio debido a que contiene demasiados archivos. Específicamente, comenzó a lanzar este error en / var / log / messages:

ext3_dx_add_entry: Directory index full!

El disco en cuestión tiene muchos inodos restantes:

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

Así que supongo que eso significa que llegamos al límite de cuántas entradas pueden estar en el archivo de directorio en sí. No tengo idea de cuántos archivos serían, pero no puede ser más, como se puede ver, de más de tres millones. No es que eso sea bueno, fíjate! Pero esa es la primera parte de mi pregunta: ¿exactamente cuál es ese límite superior? ¿Es ajustable? Antes de que me griten, quiero sintonizarlo abajo; Este enorme directorio causó todo tipo de problemas.

De todos modos, rastreamos el problema en el código que generaba todos esos archivos, y lo corregimos. Ahora estoy atascado con la eliminación del directorio.

Algunas opciones aquí:

  1. rm -rf (dir)

Probé esto primero. Me di por vencido y lo maté después de haber corrido durante un día y medio sin ningún impacto perceptible.

  • unlink (2) en el directorio: Definitivamente vale la pena considerarlo, pero la pregunta es si sería más rápido eliminar los archivos dentro del directorio a través de fsck que eliminar a través de unlink (2). Es decir, de una forma u otra, tengo que marcar esos inodos como no utilizados. Esto supone, por supuesto, que puedo decirle a fsck que no suelte entradas a los archivos que se encuentran / perdidos +; De lo contrario, acabo de mover mi problema. Además de todas las demás inquietudes, después de leer un poco más sobre este tema, es probable que tenga que llamar a algunas funciones internas del FS, ya que ninguna de las variantes de desvincular (2) que puedo encontrar me permitiría simplemente borrar alegremente un directorio con entradas en el mismo. Pooh
  • while [ true ]; do ls -Uf | head -n 10000 | xargs rm -f 2>/dev/null; done )
  • Esta es en realidad la versión abreviada; El verdadero que estoy ejecutando, que solo agrega algunos informes de progreso y una parada limpia cuando nos quedamos sin archivos para eliminar, es:

    exportación i = 0;
    tiempo (mientras que [verdadero];
      ls -Uf | cabeza -n 3 | grep -qF '.png' || descanso;
      ls -Uf | cabeza -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, ha eliminado 260,000 archivos en los últimos treinta minutos aproximadamente.


    97
    2017-09-22 23:57


    origen


    rm (GNU coreutils) 8.4 tiene esta opción: "-v, --verbose explicar lo que se está haciendo". Mostrará todos los archivos que están siendo eliminados. - Cristian Ciupitu
    En realidad, sería una buena forma de hacer una barra de progreso: ya que cada archivo tendría treinta y siete caracteres (36 + a '\ n'), podría escribir fácilmente un analizador para eso, y ya que printf () es barato y el comando rm ya tiene el nombre del archivo cargado, no hay ninguna penalización de rendimiento especial. Parece un no iniciador por hacer todo el shebang, ya que de todos modos nunca podría hacer "rm" para hacer algo así. Pero podría funcionar bastante bien como una barra de progreso intra-10,000; tal vez un "." por cada cien archivos? - BMDan
    rm -rfv | pv -l >/dev/null. pv debería estar disponible en el EPEL repositorio. - Cristian Ciupitu
    pv es abrumadoramente impresionante. Dejo un rastro de instalaciones pv a mi paso. - BMDan
    Tuve este mismo problema hace poco. ¡Gracias! - richo


    Respuestas:


    los data=writeback La opción de montaje merece ser probada para evitar el registro en diario del sistema de archivos. Esto debe hacerse solo durante el tiempo de eliminación, sin embargo, existe un riesgo si el servidor se apaga o se reinicia durante la operación de eliminación.

    De acuerdo a esta página,

    Algunas aplicaciones muestran una mejora de velocidad muy significativa cuando se usa. Por ejemplo, se pueden ver mejoras en la velocidad (...) cuando las aplicaciones crean y eliminan grandes volúmenes de archivos pequeños.

    La opción está configurada en fstab o durante la operación de montaje, reemplazando data=ordered con data=writeback. El sistema de archivos que contiene los archivos que se van a eliminar debe volver a montarse.


    30
    2017-09-26 05:49



    También podría aumentar el tiempo desde el commit  opción: "Este valor predeterminado (o cualquier valor bajo) afectará el rendimiento, pero es bueno para la seguridad de los datos. Establecerlo en 0 tendrá el mismo efecto que dejarlo en el valor predeterminado (5 segundos). Si se establece en valores muy grandes mejorar el rendimiento". - Cristian Ciupitu
    La reescritura parece estelar, excepto la documentación que estaba viendo (gentoo.org/doc/en/articles/l-afig-p8.xml#doc_chap4) menciona explícitamente que aún registra metadatos, que supongo que incluyen todos los datos que estoy cambiando (ciertamente no estoy cambiando ningún dato en los archivos). ¿Mi comprensión de la opción es incorrecta? - BMDan
    Por último, para su información, no se menciona en ese enlace el hecho de que data = writeback puede ser un gran agujero de seguridad, ya que los datos apuntados por una entrada dada pueden no tener los datos escritos allí por la aplicación, lo que significa que podría producirse una falla En el antiguo, se exponen datos posiblemente sensibles / privados. No es una preocupación aquí, ya que solo lo estamos activando temporalmente, pero quería alertar a todos sobre esa advertencia en caso de que usted u otras personas que se hayan topado con esa sugerencia no se hayan dado cuenta. - BMDan
    cometer: eso es bastante astuto! Gracias por el puntero. - BMDan
    data=writeback Todavía revisa los metadatos antes de escribirlos en el sistema de archivos principal. Como lo entiendo, simplemente no impone el ordenamiento entre cosas como escribir un mapa de extensión y escribir datos en esas extensiones. Tal vez haya otras restricciones de ordenamiento que también relajen, si se ve una ganancia en esto. Por supuesto, el montaje sin el diario podría ser un rendimiento aún mayor. (Podría permitir que los cambios de metadatos se produzcan en la RAM, sin necesidad de tener nada en el disco antes de que se complete la operación de desvinculación). - Peter Cordes


    Si bien una de las principales causas de este problema es el rendimiento ext3 con millones de archivos, la causa raíz real de este problema es diferente.

    Cuando un directorio necesita ser listado, se llama a readdir () en el directorio que produce una lista de archivos. readdir es una llamada posix, pero la llamada al sistema Linux real que se usa aquí se llama "getdents". Getdents lista las entradas del directorio rellenando un búfer con las entradas.

    El problema se debe principalmente al hecho de que readdir () utiliza un tamaño de búfer fijo de 32Kb para recuperar archivos. A medida que un directorio se hace más y más grande (el tamaño aumenta a medida que se agregan los archivos), ext3 se vuelve más lento y más lento para obtener las entradas y el tamaño del búfer de 32 Kb de readdir adicional es suficiente para incluir una fracción de las entradas en el directorio. Esto hace que readdir realice un ciclo una y otra vez e invoque la costosa llamada al sistema una y otra vez.

    Por ejemplo, en un directorio de prueba que creé con más de 2.6 millones de archivos, la ejecución de "ls -1 | wc-l" muestra una gran salida de strace de muchas llamadas del 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
    ...
    

    Además, el tiempo empleado en este directorio fue significativo.

    $ time ls -1 | wc -l
    2616044
    
    real    0m20.609s
    user    0m16.241s
    sys 0m3.639s
    

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

    Ahora, no debe llamar a getdents usted mismo manualmente, por lo que no existe una interfaz para usarla normalmente (¡consulte la página de manual para que los getdents vean!), Sin embargo puede Llámelo manualmente y haga que su invocación de llamadas al sistema sea más eficiente.

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

    /* 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);
    }
    

    Si bien esto no combate el problema fundamental subyacente (muchos archivos, en un sistema de archivos que tiene un bajo rendimiento). Es probable que sea mucho, mucho más rápido que muchas de las alternativas que se publican.

    Como previsión, uno debe eliminar el directorio afectado y rehacerlo después. Los directorios solo aumentan de tamaño y pueden seguir teniendo un rendimiento deficiente, incluso con algunos archivos internos debido al tamaño del directorio.

    Editar: He limpiado esto un poco. Se agregó una opción para permitirle eliminar en la línea de comandos en el tiempo de ejecución y se eliminó un montón de cosas del paseo de árboles que, honestamente, mirar hacia atrás era, en el mejor de los casos, cuestionable. También se demostró que produce corrupción de memoria.

    Ahora puedes hacer dentls --delete /my/path

    Nuevos resultados. Basado en un directorio con 1.82 millones de archivos.

    ## 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
    

    ¡Estaba un poco sorprendido de que esto todavía funcione tan bien!


    73
    2017-11-06 19:06



    Dos preocupaciones menores: una, [256] probablemente debería ser [FILENAME_MAX]y dos, mi Linux (2.6.18 == CentOS 5.x) no parece incluir una entrada d_type en dirent (al menos según getdents (2)). - BMDan
    ¿Podría por favor elaborar un poco sobre el reequilibrio de btree y por qué la eliminación en orden ayuda a prevenirlo? Traté de buscarlo en Google, desafortunadamente sin éxito. - ovgolovin
    Porque ahora me parece que si estamos eliminando en orden, forzamos el rebalanceo, ya que eliminamos las hojas de un lado y las de otro: es.wikipedia.org/wiki/B-tree#Rebalancing_after_deletion - ovgolovin
    Espero no molestarte con esto. Pero aún así comencé una pregunta sobre la eliminación de archivos en orden stackoverflow.com/q/17955459/862380, que parece no recibir una respuesta que explique el problema con el ejemplo, que será comprensible para los programadores ordinarios. Si tienes tiempo y te sientes así, ¿podrías estudiarlo? Tal vez podrías escribir una mejor explicación. - ovgolovin
    Este es un código asombroso. Fue la única herramienta que pude encontrar y eliminar unos 11,000,000 (once millones) de archivos de sesión que se habían acumulado en un directorio, probablemente durante algunos años. El proceso de Plesk que se suponía que los mantendría bajo control mediante el uso de buscar y otros trucos en otras respuestas aquí, no pudo completar una ejecución, por lo que los archivos simplemente se acumularon. Es un tributo al árbol binario que el sistema de archivos utiliza para almacenar el directorio, en el que las sesiones pudieron funcionar en absoluto: puede crear un archivo y recuperarlo sin demora. Sólo los listados fueron inutilizables. - Jason


    ¿Sería posible hacer una copia de seguridad de todos los demás archivos de este sistema de archivos en una ubicación de almacenamiento temporal, reformatear la partición y luego restaurar los archivos?


    31
    2017-09-23 00:27



    Realmente me gusta esta respuesta, en realidad. Como cuestión práctica, en este caso, no, pero no es algo en lo que hubiera pensado. ¡Bravo! - BMDan
    Exactamente lo que estaba pensando también. Esta es una respuesta para la pregunta 3. Ideal si me preguntas :) - Joshua


    No hay límite por archivo de directorio en ext3 solo el límite de inodo del sistema de archivos (aunque creo que hay un límite en el número de subdirectorios).

    Es posible que todavía tenga problemas después de eliminar los archivos.

    Cuando un directorio tiene millones de archivos, la entrada del directorio en sí se vuelve muy grande. La entrada del directorio debe ser escaneada para cada operación de eliminación, y eso toma varias cantidades de tiempo para cada archivo, dependiendo de dónde se encuentre su entrada. Desafortunadamente, incluso después de que se hayan eliminado todos los archivos, la entrada del directorio conserva su tamaño. Por lo tanto, las operaciones adicionales que requieran escanear la entrada del directorio aún tomarán mucho tiempo, incluso si el directorio ahora está vacío. La única forma de resolver ese problema es cambiar el nombre del directorio, crear uno nuevo con el nombre antiguo y transferir los archivos restantes al nuevo. Luego borra el renombrado.


    11
    2017-09-23 05:45



    De hecho, me di cuenta de este comportamiento justo después de borrar todo. Afortunadamente, ya habíamos sacado el directorio de la "línea de fuego", por así decirlo, así que simplemente podría decirlo. - BMDan
    Dicho esto, si no hay un límite de archivos por directorio, ¿por qué obtuve "ext3_dx_add_entry: Directory index full!" Cuando todavía había inodos disponibles en esa partición? No había subdirectorios dentro de este directorio. - BMDan
    hmm, hice un poco más de investigación y parece que hay un límite de bloques que puede ocupar un directorio. El número exacto de archivos depende de algunas cosas, por ejemplo, la longitud del nombre de archivo. Esta gossamer-threads.com/lists/linux/kernel/921942 Parece indicar que con los bloques 4k debería poder tener más de 8 millones de archivos en un directorio. ¿Eran nombres de archivo particularmente largos? - Alex J. Roberts
    Cada nombre de archivo tenía exactamente 36 caracteres. - BMDan
    bueno esa soy yo sin ideas :) - Alex J. Roberts


    No lo he evaluado, pero este chico hizo:

    rsync -a --delete ./emptyDirectoty/ ./hugeDirectory/
    

    5
    2018-06-04 11:52





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

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

    He publicado un informe de error con respecto a este problema con encontrar: http://savannah.gnu.org/bugs/?31961


    4
    2017-12-23 19:54



    Esto me salvó !! - jestro


    Recientemente enfrenté un problema similar y no pude obtener ring0's data=writeback Sugerencia para trabajar (posiblemente debido al hecho de que los archivos están en mi partición principal). Mientras investigaba soluciones me encontré con esto:

    tune2fs -O ^has_journal <device>
    

    Esto apagará el registro por completo, independientemente de la data opción de dar a mount. Combiné esto con noatime y el volumen tenía dir_index Set, y parecía funcionar bastante bien. La eliminación realmente terminó sin que tuviera que eliminarlo, mi sistema siguió respondiendo y ahora está funcionando nuevamente (con el registro en diario) sin problemas.


    3
    2018-04-23 22:29



    Iba a sugerir montarlo como ext2 en lugar de ext3, para evitar registrar las operaciones de metadatos. Esto debería hacer lo mismo. - Peter Cordes


    Asegúrate de hacer:

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

    lo que debería acelerar las cosas un poco también.


    3
    2017-09-27 02:03



    Buena llamada, pero ya está montado noatime, como mencioné en el encabezado de la pregunta. Y el nodiratime es redundante; ver lwn.net/Articles/245002 . - BMDan
    ppl repite este mantra "noatime, nodiratime, nodevatime, noreadingdocsatime" - poige


    Es un comando muy lento. Tratar:

    find /dir_to_delete ! -iname "*.png" -type f -delete
    

    2
    2017-09-23 04:04



    rm -rf corrió por un día y medio, y finalmente lo maté, sin saber si realmente había logrado algo. Necesitaba una barra de progreso. - BMDan
    Como rm es muy lento, "time find. -Delete" en archivos de 30k: 0m0.357s / 0m0.019s / 0m0.337s real / user / sys. "time (ls -1U | xargs rm -f)" en esos mismos archivos: 0m0.366s / 0m0.025s / 0m0.340s. Que es básicamente un territorio de margen de error. - BMDan
    Podrías haber corrido strace -r -p <pid of rm> para adjuntar al proceso rm ya en ejecución. Entonces puedes ver que tan rápido unlink Las llamadas al sistema se están desplazando más allá. (-r pone el tiempo desde la llamada al sistema anterior al comienzo de cada línea.) - Peter Cordes


    Es dir_index establecer para el sistema de archivos? (tune2fs -l | grep dir_index) Si no, habilítalo. Por lo general, está encendido para nuevo RHEL.


    2
    2017-09-27 04:18



    Sí, está habilitado, ¡pero increíble sugerencia! - BMDan