Vraag rm in een directory met miljoenen bestanden


Achtergrond: fysieke server, ongeveer twee jaar oud, 7200-RPM SATA-schijven aangesloten op een 3Ware RAID-kaart, ext3 FS-gemonteerde noatime en data = besteld, niet onder gekke belasting, kernel 2.6.18-92.1.22.el5, uptime 545 dagen . Directory bevat geen submappen, slechts miljoenen kleine (~ 100 byte) bestanden, met enkele grotere (een paar KB) bestanden.

We hebben een server die in de loop van de laatste maanden een beetje gek is geworden, maar we merkten het pas onlangs toen het niet in staat was om naar een map te schrijven omdat het te veel bestanden bevatte. Specifiek begon het deze fout in / var / log / messages te gooien:

ext3_dx_add_entry: Directory index full!

De schijf in kwestie heeft nog voldoende inodes:

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

Dus ik denk dat dat betekent dat we de limiet hebben bereikt van het aantal vermeldingen in het directorybestand zelf. Geen idee hoeveel bestanden dat zou zijn, maar het kan niet meer zijn, zoals je kunt zien, dan drie miljoen of zo. Niet dat dat goed is, let wel! Maar dat is deel een van mijn vraag: wat is die bovengrens precies? Is het afstembaar? Voordat ik wordt geroepen, wil ik het afstemmen naar beneden; deze enorme directory veroorzaakte allerlei problemen.

Hoe dan ook, we hebben het probleem opgespoord in de code die al deze bestanden genereerde, en we hebben het gecorrigeerd. Nu zit ik vast met het verwijderen van de map.

Een paar opties hier:

  1. rm -rf (dir)

Ik heb dit eerst geprobeerd. Ik gaf het op en doodde het nadat het anderhalve dag had gedronken zonder enige waarneembare impact.

  • ontkoppelen (2) in de map: Zeker de moeite waard om te overwegen, maar de vraag is of het sneller is om de bestanden in de map via fsck te verwijderen dan om via unlink te verwijderen (2). Dat wil zeggen, op de een of andere manier moet ik die inodes markeren als ongebruikt. Dit gaat er natuurlijk van uit dat ik fsck kan vertellen om geen gegevens in de bestanden in / lost + found te laten vallen; anders heb ik mijn probleem net verplaatst. Naast alle andere zorgen, na wat meer hierover te hebben gelezen, blijkt dat ik waarschijnlijk een aantal interne FS-functies zou moeten aanroepen, omdat geen van de ontkoppelde (2) varianten die ik kan vinden, me in staat zou stellen om gewoon te schrappen een directory met vermeldingen erin. Pooh.
  • while [ true ]; do ls -Uf | head -n 10000 | xargs rm -f 2>/dev/null; done )
  • Dit is eigenlijk de verkorte versie; de echte die ik gebruik, wat gewoon wat voortgangsrapportage en een schone stop toevoegt wanneer we te weinig bestanden hebben om te verwijderen, is:

    exporteer i = 0;
    tijd (terwijl [true]; do
      ls -Uf | hoofd -n 3 | grep -qF '.png' || breken;
      ls -Uf | hoofd -n 10000 | xargs rm -f 2> / dev / null;
      exporteer i = $ (($ i + 10000));
      echo "$ i ...";
    gedaan )

    Dit lijkt vrij goed te werken. Terwijl ik dit schrijf, heeft het in de afgelopen dertig minuten 260.000 bestanden verwijderd.


    97
    2017-09-22 23:57


    oorsprong


    rm (GNU coreutils) 8.4 heeft deze optie: "-v, --verbose leg uit wat er wordt gedaan". Het toont alle bestanden die worden verwijderd. - Cristian Ciupitu
    Eigenlijk zou dat een nette manier zijn om een ​​voortgangsbalk te doen: aangezien elk bestand zevenendertig tekens lang zou zijn (36 + a '\ n'), zou ik gemakkelijk een parser hiervoor kunnen schrijven en omdat printf () goedkoop en de opdracht rm heeft al de naam van het bestand geladen, er is geen speciale prestatieboete. Het lijkt een non-starter voor het doen van de hele kreng, omdat ik toch nooit 'rm' zou kunnen krijgen om zoiets te doen. Maar het zou best goed kunnen werken als een vooruitgangsbalk binnen de 10.000; misschien een "." voor elke honderd bestanden? - BMDan
    rm -rfv | pv -l >/dev/null. pv moet beschikbaar zijn in de EPEL repository. - Cristian Ciupitu
    pv is overweldigend geweldig. Ik laat een spoor van pv-installaties achter in mijn kielzog. - BMDan
    Ik had onlangs exact hetzelfde probleem. Dank je! - richo


    antwoorden:


    De data=writeback mount-optie verdient het om te worden uitgeprobeerd, om te voorkomen dat het bestandssysteem wordt geregistreerd. Dit moet alleen tijdens de verwijderingsperiode worden gedaan, maar er is een risico als de server wordt afgesloten of opnieuw wordt opgestart tijdens de verwijderingsbewerking.

    Volgens deze pagina,

    Sommige toepassingen vertonen een zeer aanzienlijke snelheidsverbetering wanneer deze wordt gebruikt. Snelheidsverbeteringen kunnen bijvoorbeeld worden gezien (...) wanneer toepassingen grote volumes kleine bestanden maken en verwijderen.

    De optie is ingesteld in fstab of tijdens het monteren, vervangen data=ordered met data=writeback. Het bestandssysteem dat de bestanden bevat die moeten worden verwijderd, moet opnieuw worden gekoppeld.


    30
    2017-09-26 05:49



    Hij zou ook de tijd van de commit  keuze: "Deze standaardwaarde (of een lage waarde) zal de prestaties schaden, maar het is goed voor de beveiliging van gegevens. Het instellen op 0 heeft hetzelfde effect als het standaard laten (5 seconden). prestatie verbeteren". - Cristian Ciupitu
    Writeback ziet er geweldig uit, behalve de documentatie waar ik naar keek (gentoo.org/doc/en/articles/l-afig-p8.xml#doc_chap4) vermeldt expliciet dat het nog steeds dagboeken metadata zijn, waarvan ik veronderstel dat het alle gegevens bevat die ik aan het veranderen ben (ik verander zeker geen gegevens in de bestanden zelf). Is mijn begrip van de optie onjuist? - BMDan
    Ten slotte is FYI, niet vermeld in die link, het feit dat data = terugschrijven een enorm beveiligingslek kan zijn, omdat gegevens waarnaar wordt verwezen door een gegeven gegeven mogelijk niet de gegevens hebben die daar door de app zijn geschreven, wat betekent dat een crash zou kunnen resulteren in de oude, mogelijk gevoelige / privégegevens die worden blootgesteld. Geen probleem, want we zetten het alleen tijdelijk aan, maar ik wilde iedereen op de hoogte stellen van dat voorbehoud voor het geval dat jij of anderen die tegen die suggestie aanlopen, niet op de hoogte waren. - BMDan
    commit: dat is behoorlijk glad! Bedankt voor de aanwijzer. - BMDan
    data=writeback nog steeds journalen metadata voor het schrijven naar het hoofdbestandssysteem. Zoals ik het begrijp, dwingt het gewoon de volgorde af tussen dingen als het schrijven van een uitgebreidheidskaart en het schrijven van gegevens in die gebieden. Misschien zijn er andere ordeningsbeperkingen die het ook ontspant, als je hier een voordeel uit ziet. Natuurlijk kan montage zonder het dagboek zelfs nog hogere prestaties zijn. (Mogelijk laten de metadata-wijzigingen zich gewoon in het RAM-geheugen voor, zonder dat er iets op de schijf hoeft voordat de ontkoppeling is voltooid). - Peter Cordes


    Terwijl een belangrijke oorzaak van dit probleem ext3-prestaties is met miljoenen bestanden, is de werkelijke oorzaak van dit probleem anders.

    Wanneer een map moet worden vermeld, wordt readdir () aangeroepen in de directory die een lijst met bestanden oplevert. readdir is een posix-aanroep, maar de echte Linux-systeemaanroep die hier wordt gebruikt, wordt 'getdents' genoemd. Getuige lijst met mapvermeldingen door een buffer met vermeldingen in te vullen.

    Het probleem is vooral te wijten aan het feit dat readdir () een vaste buffergrootte van 32 KB gebruikt om bestanden op te halen. Naarmate een map groter en groter wordt (de omvang neemt toe als bestanden worden toegevoegd) wordt ext3 langzamer en trager om gegevens op te halen en is de 32Kb-buffergrootte van readdir alleen voldoende om een ​​fractie van de vermeldingen in de directory op te nemen. Dit zorgt ervoor dat readdir steeds weer opnieuw gaat lussen en de dure systeemoproep steeds opnieuw doet.

    Bijvoorbeeld, in een testdirectory die ik heb gemaakt met meer dan 2,6 miljoen bestanden, toont "ls -1 | wc-l" een grote strace-uitvoer van veel getdent-systeemaanroepen.

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

    Bovendien was de tijd die in deze map werd doorgebracht aanzienlijk.

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

    De methode om dit een efficiënter proces te maken is om getdenten handmatig te bellen met een veel grotere buffer. Dit verbetert de prestaties aanzienlijk.

    Nu is het niet de bedoeling dat je jezelf getuft, dus er is geen interface om deze normaal te gebruiken (kijk op de manpagina voor getdents om te zien!), kan noem het handmatig en zorg ervoor dat uw systeemaanroep efficiënter wordt.

    Dit vermindert de benodigde tijd om deze bestanden op te halen aanzienlijk. Ik schreef een programma dat dit doet.

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

    Terwijl dit het onderliggende fundamentele probleem (veel bestanden, in een bestandssysteem dat er slecht in presteert) niet tegenwerkt. Het is waarschijnlijk veel, veel sneller dan veel van de alternatieven die worden gepost.

    Als een vooruitziende blik, zou men de betreffende map moeten verwijderen en deze daarna opnieuw moeten maken. Directories nemen alleen maar toe in omvang en kunnen slecht presteren, zelfs met een paar bestanden erin vanwege de grootte van de directory.

    Bewerk: Ik heb dit behoorlijk schoongemaakt. Een optie toegevoegd om je toe te staan ​​om tijdens runtime te verwijderen op de commandoregel en een aantal van de treewalk-dingen te verwijderen die, eerlijk gezegd terugblikken, op zijn best dubieus was. Er werd ook aangetoond dat het geheugen corruptie produceerde.

    Je kunt het nu doen dentls --delete /my/path

    Nieuwe resultaten. Gebaseerd op een map met 1,82 miljoen bestanden.

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

    Was een beetje verrast dat dit nog steeds zo goed werkt!


    73
    2017-11-06 19:06



    Twee minder belangrijke zorgen: een, [256] waarschijnlijk wel [FILENAME_MAX]en twee, mijn Linux (2.6.18 == CentOS 5.x) lijkt geen d_type-entry in dirent te bevatten (tenminste volgens getdents (2)). - BMDan
    Kun je alsjeblieft een beetje uitweiden over rebreatancing van btree en waarom verwijderen om ervoor te zorgen dat het kan worden voorkomen? Ik heb geprobeerd ernaar te googlen, helaas zonder resultaat. - ovgolovin
    Omdat het mij nu lijkt of we de volgorde verwijderen, we forceren het opnieuw in evenwicht brengen, terwijl we de bladeren aan de ene kant verwijderen en aan de andere kant laten: en.wikipedia.org/wiki/B-tree#Rebalancing_after_deletion - ovgolovin
    Ik hoop dat ik je niet lastigval met deze zaken. Maar toch begon ik een vraag over het verwijderen van bestanden in de juiste volgorde stackoverflow.com/q/17955459/862380, die geen antwoord lijkt te hebben dat de kwestie met het voorbeeld zal verklaren, wat begrijpelijk zal zijn voor gewone programmeurs. Als u tijd heeft en u zich zo voelt, kunt u ernaar kijken? Misschien zou je een betere verklaring kunnen schrijven. - ovgolovin
    Dit is een geweldig stuk code. Het was de enige tool die ik kon vinden en die ongeveer 11.000.000 (elf miljoen) sessiebestanden kon opsommen en verwijderen die zich in een map hadden opgebouwd, waarschijnlijk over een aantal jaar. Het Plesk-proces dat ze onder controle moest houden met behulp van find en andere trucs in andere antwoorden, kon een run niet voltooien, dus de bestanden bleven maar toenemen. Het is een eerbetoon aan de binaire structuur die het bestandssysteem gebruikt om de map op te slaan, dat de sessies überhaupt konden werken - je kon een bestand maken en het zonder vertraging ophalen. Alleen vermeldingen waren onbruikbaar. - Jason


    Zou het mogelijk zijn om een ​​backup te maken van alle andere bestanden van dit bestandssysteem naar een tijdelijke opslaglocatie, de partitie opnieuw te formatteren en vervolgens de bestanden te herstellen?


    31
    2017-09-23 00:27



    Ik vind dit antwoord echt leuk. Als een praktische zaak, in dit geval, nee, maar het is niet degene die ik had bedacht. Bravo! - BMDan
    Precies wat ik ook dacht. Dit is een antwoord op vraag 3. Ideaal als je het mij vraagt ​​:) - Joshua


    Er is geen per map bestandslimiet in ext3 alleen de inode limiet van het bestandssysteem (ik denk echter dat er een limiet is aan het aantal subdirectories).

    U kunt nog steeds problemen ondervinden na het verwijderen van de bestanden.

    Wanneer een directory miljoenen bestanden bevat, wordt het directory-item zelf erg groot. Het directory-item moet worden gescand voor elke verwijderingsbewerking, en dat neemt verschillende hoeveelheden tijd in beslag voor elk bestand, afhankelijk van waar het item zich bevindt. Jammer genoeg blijft zelfs nadat alle bestanden zijn verwijderd de directory-invoer behouden. Dus verdere bewerkingen die het inlezen van het telefoonboekitem vereisen, zullen nog steeds een lange tijd duren, zelfs als de map nu leeg is. De enige manier om dat probleem op te lossen is om de map te hernoemen, een nieuwe te maken met de oude naam en de resterende bestanden over te zetten naar de nieuwe. Verwijder vervolgens de hernoemde naam.


    11
    2017-09-23 05:45



    Inderdaad, ik merkte alleen dit gedrag op na alles te hebben verwijderd. Gelukkig hadden we het mapje al als het ware uit de "vuistregel" gehaald, dus ik kon het gewoon doen. - BMDan
    Dat gezegd hebbende, als er geen per-map bestandslimiet is, waarom kreeg ik "ext3_dx_add_entry: Directory index vol!" wanneer er nog inodes beschikbaar waren op die partitie? Er waren geen submappen in deze map. - BMDan
    hmm ik deed wat meer onderzoek en het lijkt erop dat er een limiet is van het aantal blokken dat een map kan opnemen. Het exacte aantal bestanden is afhankelijk van een paar dingen, bijvoorbeeld de lengte van de bestandsnaam. Deze gossamer-threads.com/lists/linux/kernel/921942 lijkt erop te wijzen dat u met 4k-blokken meer dan 8 miljoen bestanden in een map kunt hebben. Waren ze bijzonder lange bestandsnamen? - Alex J. Roberts
    Elke bestandsnaam was precies 36 tekens lang. - BMDan
    nou dat is mij uit ideeën :) - Alex J. Roberts


    Ik heb het niet gebenchmarked, maar deze man deed het:

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

    5
    2018-06-04 11:52





    vinden werkte simpelweg niet voor mij, zelfs niet na het veranderen van de parameters van de ext3 fs zoals voorgesteld door de bovenstaande gebruikers. Verbruikte veel te veel geheugen. Dit PHP-script heeft de truc gedaan: snel, onbetekenend CPU-gebruik, onbeduidend geheugengebruik:

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

    Ik heb een bugrapport over dit probleem gepost met find: http://savannah.gnu.org/bugs/?31961


    4
    2017-12-23 19:54



    Dit heeft me gered !! - jestro


    Onlangs kreeg ik een soortgelijk probleem en kon ik geen ring0's bemachtigen data=writeback suggestie om te werken (mogelijk vanwege het feit dat de bestanden zich op mijn hoofdpartitie bevinden). Tijdens het onderzoeken van oplossingen vond ik dit:

    tune2fs -O ^has_journal <device>
    

    Hiermee wordt het journaal volledig uitgeschakeld, ongeacht de data optie geven aan mount. Ik combineerde dit met noatime en het volume had dir_index ingesteld, en het leek vrij goed te werken. Het verwijderen is echt voltooid zonder dat ik het hoefde te doden, mijn systeem bleef reageren en het is nu weer actief (met opnieuw loggen) zonder problemen.


    3
    2018-04-23 22:29



    Ik wilde voorstellen om het als ext2 te installeren in plaats van ext3, om te voorkomen dat de metadata-ops worden gelogd. Dit zou hetzelfde moeten doen. - Peter Cordes


    Zorg ervoor dat u:

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

    wat de dingen een beetje zou moeten versnellen.


    3
    2017-09-27 02:03



    Goed telefoongesprek, maar het is al gemonteerde noatime, zoals ik al zei in de kop van de vraag. En nodiratime is overbodig; zien lwn.net/Articles/245002 . - BMDan
    ppl herhaal deze mantra "noatime, nodiratime, nodevatime, noreadingdocsatime" - poige