Frage rm auf ein Verzeichnis mit Millionen von Dateien


Hintergrund: Physischer Server, etwa zwei Jahre alt, 7200-RPM-SATA-Laufwerke, verbunden mit einer 3Ware-RAID-Karte, ext3-FS-Noatime und Daten = bestellt, nicht unter verrückter Last, Kernel 2.6.18-92.1.22.el5, Betriebszeit 545 Tage . Verzeichnis enthält keine Unterverzeichnisse, nur Millionen von kleinen (~ 100 Byte) Dateien, mit einigen größeren (einige KB).

Wir haben einen Server, der im Laufe der letzten Monate ein wenig Kuckuck gemacht hat, aber wir haben es erst neulich bemerkt, als es anfing, in ein Verzeichnis zu schreiben, weil es zu viele Dateien enthielt. Insbesondere hat es begonnen, diesen Fehler in / var / log / messages zu werfen:

ext3_dx_add_entry: Directory index full!

Die betroffene Festplatte hat noch viele übrig gebliebene Inodes:

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

Ich vermute also, dass wir die Grenze der Einträge in der Verzeichnisdatei selbst erreicht haben. Keine Ahnung, wie viele Dateien das sein würden, aber es kann nicht mehr sein, wie du sehen kannst, als drei Millionen oder so. Nicht das ist gut, wohlgemerkt! Aber das ist der erste Teil meiner Frage: Was genau ist diese Obergrenze? Ist es einstellbar? Bevor ich angeschrien werde - ich möchte es tunen Nieder; Dieses riesige Verzeichnis verursachte alle möglichen Probleme.

Wie auch immer, wir haben das Problem in dem Code ausfindig gemacht, der all diese Dateien generiert hat, und wir haben es korrigiert. Jetzt bin ich mit dem Löschen des Verzeichnisses fest.

Ein paar Optionen hier:

  1. rm -rf (dir)

Ich habe das zuerst versucht. Ich gab es auf und tötete es, nachdem es anderthalb Tage ohne erkennbare Wirkung gelaufen war.

  • unlink (2) im Verzeichnis: Definitiv eine Überlegung wert, aber die Frage ist, ob es schneller wäre, die Dateien im Verzeichnis per fsck zu löschen als über unlink (2) zu löschen. Das heißt, auf die eine oder andere Weise muss ich diese Inodes als unbenutzt markieren. Dies setzt natürlich voraus, dass ich fsck anweisen kann, Einträge in die Dateien in / lost + found nicht zu löschen; Ansonsten habe ich gerade mein Problem verschoben. Zusätzlich zu all den anderen Bedenken, nachdem ich etwas mehr darüber gelesen habe, stellt sich heraus, dass ich wahrscheinlich einige interne FS-Funktionen aufrufen muss, da mir keine der Unlink (2) -Varianten, die ich finden kann, es erlauben würde, sie einfach zu löschen ein Verzeichnis mit Einträgen darin. Puuh.
  • while [ true ]; do ls -Uf | head -n 10000 | xargs rm -f 2>/dev/null; done )
  • Dies ist eigentlich die verkürzte Version; Die echte, die ich gerade ausführe, die nur einen Fortschrittsbericht und einen sauberen Stopp hinzufügt, wenn wir keine Dateien zum Löschen mehr haben, ist:

    Export i = 0;
    Zeit (während [wahr];
      ls -Uf | Kopf -n 3 | grep -qF '.png' || brechen;
      ls -Uf | Kopf -n 10000 | xargs rm -f 2> / dev / null;
      export i = $ (($ i + 10000));
      echo "$ i ...";
    erledigt )

    Dies scheint ziemlich gut zu funktionieren. Während ich dies schreibe, hat es 260.000 Dateien in den letzten dreißig Minuten gelöscht.


    97
    2017-09-22 23:57


    Ursprung


    rm (GNU coreutils) 8.4 hat diese Option: "-v, --verbose erkläre, was getan wird". Es werden alle Dateien angezeigt, die gelöscht werden. - Cristian Ciupitu
    Eigentlich wäre das eine gute Möglichkeit, eine Fortschrittsanzeige zu erstellen: Da jede Datei siebenunddreißig Zeichen lang wäre (36 + a '\ n'), könnte ich leicht einen Parser dafür schreiben und da printf () ist billig und der rm-Befehl hat bereits den Namen der Datei geladen, es gibt keine besondere Leistung Strafe. Scheint wie ein Nichtstarter, weil ich den ganzen Shebang gemacht habe, da ich sowieso nie "rm" bekommen hätte. Aber es könnte als Intra-10.000-Fortschrittsbalken recht gut funktionieren; vielleicht ein "." für jede hundert Dateien? - BMDan
    rm -rfv | pv -l >/dev/null. pv sollte in der sein EPEL Repository. - Cristian Ciupitu
    pv ist überwältigend fantastisch. Ich hinterlasse eine Spur von PV-Installationen in meinem Kielwasser. - BMDan
    Ich hatte kürzlich genau dieses Problem. Vielen Dank! - richo


    Antworten:


    Das data=writeback mount-Option verdient es ausprobiert zu werden, um das Journaling des Dateisystems zu verhindern. Dies sollte nur während der Löschzeit erfolgen, es besteht jedoch ein Risiko, wenn der Server während des Löschvorgangs heruntergefahren oder neu gestartet wird.

    Gemäß diese Seite,

    Einige Anwendungen zeigen eine sehr deutliche Geschwindigkeitsverbesserung, wenn sie verwendet wird. Zum Beispiel können Geschwindigkeitsverbesserungen gesehen werden (...) wenn Anwendungen große Mengen kleiner Dateien erstellen und löschen.

    Die Option wird entweder in eingestellt fstab oder während des Mount-Vorgangs ersetzen data=ordered mit data=writeback. Das Dateisystem mit den zu löschenden Dateien muss neu gemountet werden.


    30
    2017-09-26 05:49



    Er könnte auch die Zeit von der commit  Möglichkeit: "Dieser Standardwert (oder ein niedriger Wert) beeinträchtigt zwar die Leistung, ist aber gut für die Datensicherheit. Wenn Sie ihn auf 0 setzen, wird derselbe Effekt wie bei der Standardeinstellung (5 Sekunden) beibehalten Leistung verbessern". - Cristian Ciupitu
    Writeback sieht hervorragend aus, mit Ausnahme der Dokumentation, die ich mir angesehen habe (gentoo.org/doc/en/articles/l-afig-p8.xml#doc_chap4) erwähnt explizit, dass es immer noch Metadaten speichert, von denen ich vermute, dass sie alle Daten enthalten, die ich verändere (ich ändere bestimmt keine Daten in den Dateien selbst). Ist mein Verständnis der Option falsch? - BMDan
    Schließlich, FYI, in diesem Link nicht erwähnt ist die Tatsache, dass Daten = Writeback eine große Sicherheitslücke sein kann, da Daten, auf die von einem gegebenen Eintrag verwiesen wird, nicht die Daten haben, die von der App geschrieben wurde, was bedeutet, dass ein Absturz führen könnte in den alten, möglicherweise sensiblen / privaten Daten ausgesetzt werden. Das ist hier kein Problem, da wir es nur vorübergehend einschalten, aber ich wollte alle auf diesen Vorbehalt aufmerksam machen, für den Fall, dass Sie oder andere, die diesem Vorschlag gegenüberstehen, dies nicht wussten. - BMDan
    commit: das ist ziemlich glatt! Danke für den Zeiger. - BMDan
    data=writeback speichert die Metadaten noch, bevor sie in das Hauptdateisystem geschrieben werden. Wie ich es verstehe, erzwingt es einfach nicht, zwischen Dingen wie dem Schreiben einer Bereichszuordnung und dem Schreiben von Daten in diese Bereiche zu ordnen. Vielleicht gibt es andere Ordnungsbeschränkungen, es entspannt sich auch, wenn Sie eine Verbesserung sehen. Natürlich könnte eine Montage ohne das Journal überhaupt noch eine höhere Leistung sein. (Es könnte die Metadatenänderungen nur im RAM passieren lassen, ohne dass etwas auf der Festplatte vorhanden sein muss, bevor der Unlink-Vorgang abgeschlossen ist). - Peter Cordes


    Während eine Hauptursache für dieses Problem die ext3-Leistung mit Millionen von Dateien ist, ist die eigentliche Ursache für dieses Problem anders.

    Wenn ein Verzeichnis aufgelistet werden muss, wird readdir () für das Verzeichnis aufgerufen, das eine Liste von Dateien ergibt. readdir ist ein Posix-Aufruf, aber der echte Linux-Systemaufruf, der hier verwendet wird, wird 'getdents' genannt. Getdents listet Verzeichniseinträge auf, indem ein Puffer mit Einträgen gefüllt wird.

    Das Problem ist hauptsächlich auf die Tatsache zurückzuführen, dass readdir () eine feste Puffergröße von 32 KB zum Abrufen von Dateien verwendet. Wenn ein Verzeichnis immer größer wird (die Größe nimmt zu, wenn Dateien hinzugefügt werden), wird ext3 immer langsamer, um Einträge zu holen, und die 32Kb-Puffergröße des zusätzlichen readdir reicht nur aus, um einen Bruchteil der Einträge in das Verzeichnis aufzunehmen. Dies führt dazu, dass readdir immer wieder durchläuft und den teuren Systemaufruf immer wieder aufruft.

    In einem Testverzeichnis, das ich zum Beispiel mit über 2,6 Millionen Dateien erstellt habe, zeigt "ls -1 | wc-l" eine große strace-Ausgabe vieler getdent-Systemaufrufe.

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

    Zusätzlich war die Zeit, die in diesem Verzeichnis verbracht wurde, signifikant.

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

    Die Methode, dies zu einem effizienteren Prozess zu machen, besteht darin, getdents manuell mit einem viel größeren Puffer aufzurufen. Dies verbessert die Leistung erheblich.

    Nun, Sie sollten getdents nicht manuell aufrufen, also gibt es keine Schnittstelle, um es normal zu benutzen (sehen Sie sich die Manpage für getdents an!), Wie auch immer Sie können Rufen Sie es manuell auf und machen Sie Ihren Systemaufruf effizienter.

    Dies reduziert die Zeit, die zum Abrufen dieser Dateien benötigt wird, drastisch. Ich habe ein Programm geschrieben, das das tut.

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

    Dies bekämpft zwar nicht das zugrunde liegende fundamentale Problem (viele Dateien, in einem Dateisystem, das schlecht funktioniert). Es ist wahrscheinlich viel, viel schneller als viele der Alternativen.

    Vorausschauend sollte man das betroffene Verzeichnis entfernen und danach neu erstellen. Verzeichnisse werden nur immer größer und können aufgrund der Größe des Verzeichnisses auch mit einigen wenigen Dateien schlecht funktionieren.

    Bearbeiten: Ich habe das ziemlich viel aufgeräumt. Es wurde eine Option hinzugefügt, mit der Sie in der Befehlszeile zur Laufzeit löschen können und eine Menge Treewalk-Sachen entfernt haben, die ehrlich zurückblickend bestenfalls fragwürdig waren. Es wurde auch gezeigt, dass Speicherkorruption erzeugt wird.

    Sie können jetzt tun dentls --delete /my/path

    Neue Ergebnisse. Basiert auf einem Verzeichnis mit 1,82 Millionen Dateien.

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

    War irgendwie überrascht das funktioniert immer noch so gut!


    73
    2017-11-06 19:06



    Zwei kleine Bedenken: [256] sollte wahrscheinlich sein [FILENAME_MAX]Und zwei, mein Linux (2.6.18 == CentOS 5.x) scheint keinen d_type-Eintrag in dirent zu enthalten (zumindest nach getdents (2)). - BMDan
    Könnten Sie bitte ein wenig über das Rebalancing von btree nachdenken und warum die Löschung in der Reihenfolge hilft, es zu verhindern? Ich habe es googeln versucht, leider ohne Erfolg. - ovgolovin
    Weil es mir jetzt scheint, wenn wir in der richtigen Reihenfolge löschen, erzwingen wir eine Neuausrichtung, indem wir Blätter auf der einen Seite entfernen und auf der anderen ablassen: en.wikipedia.org/wiki/B-tree#Rebalancing_after_deletion - ovgolovin
    Ich hoffe, ich störe dich nicht mit diesen Dingen. Trotzdem habe ich eine Frage zum Löschen von Dateien in der richtigen Reihenfolge gestellt stackoverflow.com/q/17955459/862380, die keine Antwort zu erhalten scheint, die das Problem mit dem Beispiel erklären wird, das für normale Programmierer verständlich sein wird. Wenn du Zeit hast und dich so fühlst, könntest du darüber nachdenken? Vielleicht könntest du eine bessere Erklärung schreiben. - ovgolovin
    Das ist ein erstaunliches Stück Code. Es war das einzige Werkzeug, das ich finden konnte und in der Lage war, 11.000.000 (elf Millionen) Session-Dateien aufzulisten und zu löschen, die sich wahrscheinlich über einige Jahre in einem Verzeichnis angesammelt hatten. Der Plesk-Prozess, der sie unter Verwendung von Finden und anderen Tricks in anderen Antworten hier unter Kontrolle halten sollte, konnte einen Lauf nicht abschließen, so dass sich die Dateien gerade weiter entwickelten. Es ist eine Hommage an den binären Baum, den das Dateisystem zum Speichern des Verzeichnisses verwendet, dass die Sitzungen überhaupt funktionieren konnten - Sie konnten eine Datei erstellen und sie ohne Verzögerung abrufen. Nur Auflistungen waren unbrauchbar. - Jason


    Wäre es möglich, alle anderen Dateien aus diesem Dateisystem an einem temporären Speicherort zu sichern, die Partition neu zu formatieren und dann die Dateien wiederherzustellen?


    31
    2017-09-23 00:27



    Ich mag diese Antwort wirklich. Praktisch, in diesem Fall, nein, aber es ist nicht eins, an das ich gedacht hätte. Bravo! - BMDan
    Genau das, was ich auch dachte. Dies ist eine Antwort auf Frage 3. Ideal, wenn Sie mich fragen :) - Joshua


    Es gibt kein Limit pro Dateiverzeichnis in ext3 nur das Dateisystem inode limit (ich denke, es gibt ein Limit für die Anzahl der Unterverzeichnisse).

    Nach dem Entfernen der Dateien können weiterhin Probleme auftreten.

    Wenn ein Verzeichnis Millionen von Dateien enthält, wird der Verzeichniseintrag selbst sehr groß. Der Verzeichniseintrag muss für jeden Entfernungsvorgang durchsucht werden, und das dauert für jede Datei unterschiedlich lange, je nachdem, wo sich der Eintrag befindet. Leider behält der Verzeichniseintrag auch nach dem Entfernen aller Dateien seine Größe bei. Daher werden weitere Operationen, die das Durchsuchen des Verzeichniseintrags erfordern, noch lange dauern, selbst wenn das Verzeichnis jetzt leer ist. Die einzige Möglichkeit, dieses Problem zu lösen, besteht darin, das Verzeichnis umzubenennen, ein neues mit dem alten Namen zu erstellen und alle verbleibenden Dateien auf das neue zu übertragen. Dann lösche den umbenannten.


    11
    2017-09-23 05:45



    Tatsächlich habe ich gerade dieses Verhalten bemerkt, nachdem ich alles gelöscht hatte. Glücklicherweise hatten wir das Verzeichnis bereits aus der "Schusslinie" herausgeholt, also konnte ich es einfach rmdiren. - BMDan
    Das heißt, wenn es kein Dateilimit pro Verzeichnis gibt, warum habe ich "ext3_dx_add_entry: Directory index full!" wenn auf dieser Partition noch Inodes verfügbar waren? Es gab keine Unterverzeichnisse in diesem Verzeichnis. - BMDan
    hmm, ich habe ein bisschen mehr recherchiert und es scheint, als ob die Anzahl der Blöcke, die ein Verzeichnis aufnehmen kann, begrenzt ist. Die genaue Anzahl der Dateien hängt von einigen Dingen ab, zB Dateiname Länge. Diese gossamer-threads.com/lists/linux/kernel/921942 scheint darauf hinzuweisen, dass Sie mit 4k Blöcken mehr als 8 Millionen Dateien in einem Verzeichnis haben können. Waren sie besonders lange Dateinamen? - Alex J. Roberts
    Jeder Dateiname war genau 36 Zeichen lang. - BMDan
    Nun, das bin ich aus Ideen :) - Alex J. Roberts


    Ich habe es nicht bewertet, aber dieser Typ hat es getan:

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

    5
    2018-06-04 11:52





    Finden Sie einfach nicht funktioniert für mich, auch nach Änderung der ext3 fs Parameter wie von den Benutzern oben vorgeschlagen. Verbraucht viel zu viel Speicher. Dieses PHP-Skript hat es geschafft - schnelle, unwichtige CPU-Nutzung, unbedeutende Speicherauslastung:

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

    Ich habe einen Fehlerbericht zu diesem Problem gepostet: http://savannah.gnu.org/bugs/?31961


    4
    2017-12-23 19:54



    Das hat mich gerettet !! - jestro


    Ich hatte kürzlich ein ähnliches Problem und war nicht in der Lage, Ring0s zu bekommen data=writeback Vorschlag zu arbeiten (möglicherweise aufgrund der Tatsache, dass die Dateien auf meiner Hauptpartition sind). Bei der Suche nach Workarounds bin ich auf folgendes gestoßen:

    tune2fs -O ^has_journal <device>
    

    Dadurch wird das Journaling vollständig deaktiviert, unabhängig von der data Option geben an mount. Ich habe das mit kombiniert noatime und das Volumen hatte dir_index eingestellt, und es schien ziemlich gut zu funktionieren. Der Löschvorgang wurde tatsächlich beendet, ohne dass ich ihn beenden musste. Mein System blieb reaktiv, und es ist jetzt wieder einsatzbereit (mit Journaling zurück) ohne Probleme.


    3
    2018-04-23 22:29



    Ich würde vorschlagen, es als ext2 anstelle von ext3 zu mounten, um das Journaling der Metadaten zu vermeiden. Dies sollte das Gleiche tun. - Peter Cordes


    Stellen Sie sicher, dass Sie Folgendes tun:

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

    das sollte die Dinge auch etwas beschleunigen.


    3
    2017-09-27 02:03



    Guter Ruf, aber es ist bereits noatime, wie ich im Header auf die Frage erwähnt habe. Und nodiratime ist überflüssig; sehen lwn.net/Articles/245002 . - BMDan
    Wiederhole dieses Mantra "noatime, nodiratime, nodevatime, noreadingdocsatime" - poige