Pertanyaan rm pada direktori dengan jutaan file


Latar belakang: server fisik, sekitar dua tahun, drive SATA 7200-RPM terhubung ke kartu 3Ware RAID, ext3 FS mount noatime dan data = dipesan, tidak di bawah beban gila, kernel 2.6.18-92.1.22.el5, waktu aktif 545 hari . Direktori tidak berisi subdirektori, hanya jutaan file kecil (~ 100 byte), dengan beberapa yang lebih besar (beberapa KB).

Kami memiliki server yang sudah agak lama selama beberapa bulan terakhir, tetapi kami baru menyadarinya beberapa hari ketika mulai tidak dapat menulis ke direktori karena berisi terlalu banyak file. Secara khusus, ia mulai melemparkan kesalahan ini di / var / log / messages:

ext3_dx_add_entry: Directory index full!

Disk yang dimaksud memiliki banyak inode yang tersisa:

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

Jadi saya menduga itu berarti kita mencapai batas berapa banyak entri dapat berada di file direktori itu sendiri. Tidak tahu berapa banyak file yang akan, tetapi tidak bisa lebih, seperti yang Anda lihat, dari tiga juta atau lebih. Bukan itu bagus, ingat kamu! Tapi itu bagian dari pertanyaanku: tepatnya apa batas atas itu? Apakah itu merdu? Sebelum saya dimarahi — saya ingin menyetelnya turun; direktori besar ini menyebabkan segala macam masalah.

Bagaimanapun, kami melacak masalah dalam kode yang menghasilkan semua file tersebut, dan kami telah memperbaikinya. Sekarang saya terjebak dengan menghapus direktori.

Beberapa opsi di sini:

  1. rm -rf (dir)

Saya mencoba ini dulu. Saya menyerah dan membunuhnya setelah lari selama satu setengah hari tanpa ada dampak yang nyata.

  • unlink (2) pada direktori: Pasti layak dipertimbangkan, tetapi pertanyaannya adalah apakah akan lebih cepat menghapus file di dalam direktori melalui fsck daripada menghapus melalui unlink (2). Artinya, dengan satu atau lain cara, saya harus menandai inode tersebut sebagai tidak terpakai. Ini mengasumsikan, tentu saja, bahwa saya dapat memberitahu fsck untuk tidak memasukkan entri ke file di / hilang + yang ditemukan; jika tidak, saya baru saja pindah masalah saya. Selain semua masalah lainnya, setelah membaca tentang ini sedikit lebih banyak, ternyata saya mungkin harus memanggil beberapa fungsi FS internal, karena tidak ada varian unlink (2) yang dapat saya temukan akan memungkinkan saya untuk menghapusnya sebuah direktori dengan entri di dalamnya. Pooh.
  • while [ true ]; do ls -Uf | head -n 10000 | xargs rm -f 2>/dev/null; done )
  • Ini sebenarnya versi singkatnya; yang asli saya jalankan, yang hanya menambahkan beberapa pelaporan kemajuan dan berhenti bersih ketika kita kehabisan file untuk dihapus, adalah:

    ekspor i = 0;
    waktu (sementara [benar]; lakukan
      ls -Uf | kepala -n 3 | grep -qF '.png' || istirahat;
      ls -Uf | head -n 10000 | xargs rm -f 2> / dev / null;
      ekspor i = $ (($ i + 10000));
      echo "$ i ...";
    selesai)

    Ini sepertinya bekerja dengan baik. Ketika saya menulis ini, telah menghapus 260.000 file dalam tiga puluh menit terakhir atau lebih.


    97
    2017-09-22 23:57




    rm (GNU coreutils) 8.4 memiliki opsi ini: "-v, --verbose menjelaskan apa yang sedang dilakukan". Ini akan menampilkan semua file yang sedang dihapus. - Cristian Ciupitu
    Sebenarnya, itu akan menjadi cara yang rapi untuk melakukan progress bar: karena setiap file akan memiliki panjang tiga puluh tujuh karakter (36 + a '\ n'), saya bisa dengan mudah menulis parser untuk itu, dan karena printf () adalah murah dan perintah rm sudah memiliki nama file yang dimuat, tidak ada hukuman kinerja utama. Sepertinya tidak ada starter untuk melakukan shebang secara keseluruhan, karena saya tidak pernah bisa "rm" melakukan hal seperti itu. Tapi itu bisa berfungsi dengan baik sebagai bar kemajuan intra-10.000; mungkin "." untuk setiap seratus file? - BMDan
    rm -rfv | pv -l >/dev/null. pv harus tersedia di EPEL gudang. - Cristian Ciupitu
    pv sangat luar biasa. Saya meninggalkan jejak instalasi pv di belakang saya. - BMDan
    Saya memiliki masalah yang sama persis baru-baru ini. Terima kasih! - richo


    Jawaban:


    Itu data=writeback opsi mount layak untuk dicoba, untuk mencegah journal sistem file. Ini harus dilakukan hanya selama waktu penghapusan, namun ada risiko jika server sedang dimatikan atau reboot selama operasi penghapusan.

    Menurut halaman ini,

    Beberapa aplikasi menunjukkan peningkatan kecepatan yang sangat signifikan ketika digunakan. Sebagai contoh, peningkatan kecepatan dapat dilihat (...) ketika aplikasi membuat dan menghapus file kecil dalam jumlah besar.

    Opsi disetel dalam fstab atau selama operasi pemasangan, diganti data=ordered dengan data=writeback. Sistem file yang berisi file-file yang akan dihapus harus di-mount.


    30
    2017-09-26 05:49



    Dia juga bisa menambah waktu dari commit  pilihan: "Nilai default ini (atau nilai rendah lainnya) akan merusak kinerja, tapi bagus untuk keamanan data. Pengaturan ke 0 akan memiliki efek yang sama seperti meninggalkannya di default (5 detik). Pengaturannya ke nilai yang sangat besar akan meningkatkan kinerja". - Cristian Ciupitu
    Writeback terlihat luar biasa, kecuali dokumentasi yang saya amati (gentoo.org/doc/en/articles/l-afig-p8.xml#doc_chap4) secara eksplisit menyebutkan bahwa itu masih jurnal metadata, yang saya anggap mencakup semua data yang saya ubah (saya pasti tidak mengubah data dalam file itu sendiri). Apakah pemahaman saya tentang opsi itu salah? - BMDan
    Terakhir, FYI, tidak disebutkan dalam tautan itu adalah fakta bahwa data = writeback bisa menjadi lubang keamanan yang besar, karena data yang ditunjukkan oleh entri tertentu mungkin tidak memiliki data yang ditulis di sana oleh aplikasi, yang berarti bahwa crash dapat mengakibatkan dalam data lama, mungkin-sensitif / swasta yang terkena. Bukan masalah di sini, karena kami hanya mengaktifkannya untuk sementara, tapi saya ingin mengingatkan semua orang untuk peringatan itu jika Anda atau orang lain yang berlari di saran itu tidak sadar. - BMDan
    komit: itu cukup licin! Terima kasih atas penunjuknya. - BMDan
    data=writeback masih membuat jurnal metadata sebelum menuliskannya ke dalam sistem file utama. Seperti yang saya pahami, itu tidak memaksakan pemesanan antara hal-hal seperti menulis peta luas dan menulis data ke luasan tersebut. Mungkin ada kendala pemesanan lainnya yang juga rileks, jika Anda melihat keuntungan perf dari ini. Tentu saja, pemasangan tanpa jurnal sama sekali bisa menjadi kinerja yang lebih tinggi. (Mungkin membiarkan perubahan metadata hanya terjadi di RAM, tanpa perlu memiliki apa pun di disk sebelum op lepaskan tautan selesai). - Peter Cordes


    Sementara penyebab utama masalah ini adalah kinerja ext3 dengan jutaan file, penyebab sebenarnya dari masalah ini berbeda.

    Ketika sebuah direktori perlu terdaftar readdir () dipanggil pada direktori yang menghasilkan daftar file. readdir adalah panggilan posix, tetapi sistem panggilan Linux yang digunakan di sini disebut 'getdents'. Getdents daftar entri direktori dengan mengisi buffer dengan entri.

    Masalahnya adalah terutama karena fakta bahwa readdir () menggunakan ukuran buffer tetap 32Kb untuk mengambil file. Sebagai direktori menjadi lebih besar dan lebih besar (ukuran meningkat ketika file ditambahkan) ext3 menjadi lebih lambat dan lebih lambat untuk mengambil entri dan ukuran buffer 32Kb readdir tambahan hanya cukup untuk memasukkan sebagian kecil dari entri dalam direktori. Ini menyebabkan readdir ke loop berulang-ulang dan memanggil system call yang mahal berulang-ulang.

    Sebagai contoh, pada direktori pengujian yang saya buat dengan lebih dari 2,6 juta file di dalam, menjalankan "ls -1 | wc-l" menunjukkan keluaran strace besar dari banyak panggilan sistem 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
    ...
    

    Selain itu waktu yang dihabiskan di direktori ini adalah signifikan.

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

    Metode untuk membuat proses ini lebih efisien adalah memanggil getdents secara manual dengan buffer yang jauh lebih besar. Ini meningkatkan kinerja secara signifikan.

    Sekarang, Anda tidak seharusnya memanggil getdents sendiri secara manual sehingga tidak ada antarmuka yang ada untuk menggunakannya secara normal (lihat halaman manual untuk dilihat oleh para pembaca!), Namun Anda bisa sebut secara manual dan buat cara panggilan sistem Anda lebih efisien.

    Ini secara drastis mengurangi waktu yang diperlukan untuk mengambil file-file ini. Saya menulis sebuah program yang melakukan ini.

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

    Sementara ini tidak memerangi masalah mendasar yang mendasarinya (banyak file, dalam sistem file yang berkinerja buruk dalam hal itu). Ini mungkin jauh, jauh lebih cepat daripada banyak alternatif yang diposting.

    Sebagai pemikiran sebelumnya, orang harus menghapus direktori yang terkena dampak dan membuat kembali setelahnya. Direktori hanya bertambah besar dan dapat tetap berkinerja buruk bahkan dengan beberapa file di dalamnya karena ukuran direktori.

    Edit: Saya sudah membersihkan ini cukup sedikit. Ditambahkan opsi untuk memungkinkan Anda untuk menghapus pada baris perintah saat runtime dan menghapus banyak hal treewalk yang, jujur ​​melihat ke belakang itu dipertanyakan di terbaik. Juga ditunjukkan untuk menghasilkan korupsi memori.

    Anda sekarang bisa melakukannya dentls --delete /my/path

    Hasil baru. Didasarkan dari sebuah direktori dengan 1,82 juta file.

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

    Agak terkejut, ini masih berfungsi dengan baik!


    73
    2017-11-06 19:06



    Dua kekhawatiran kecil: satu, [256] mungkin seharusnya [FILENAME_MAX], dan dua, Linux saya (2.6.18 == CentOS 5.x) tampaknya tidak menyertakan entri d_type dalam dirent (setidaknya menurut getdents (2)). - BMDan
    Bisakah Anda menjelaskan sedikit tentang penyeimbangan btree dan mengapa penghapusan dalam rangka membantu mencegahnya? Saya mencoba Googling untuk itu, sayangnya tidak berhasil. - ovgolovin
    Karena sekarang tampaknya bagi saya jika kami menghapus pesanan, kami memaksa menyeimbangkan kembali, karena kami menghapus daun di satu sisi dan meninggalkan di sisi lain: en.wikipedia.org/wiki/B-tree#Rebalancing_after_deletion - ovgolovin
    Saya harap saya tidak mengganggu Anda dengan masalah ini. Tapi tetap saya mulai pertanyaan tentang menghapus file yang di-order stackoverflow.com/q/17955459/862380, yang tampaknya tidak menerima jawaban yang akan menjelaskan masalah dengan contoh, yang akan dimengerti untuk programmer biasa. Jika Anda punya waktu dan merasa seperti itu, bisakah Anda memeriksanya? Mungkin Anda bisa menulis penjelasan yang lebih baik. - ovgolovin
    Ini adalah bagian kode yang luar biasa. Itu adalah satu-satunya alat yang dapat saya temukan untuk mencantumkan dan menghapus beberapa file sesi 11.000.000 (sebelas juta) yang telah dibangun di direktori, mungkin selama beberapa tahun. Proses Plesk yang seharusnya membuat mereka terkendali menggunakan find dan trik lain dalam jawaban lain di sini, tidak dapat menyelesaikan proses, sehingga file terus bertambah. Ini adalah penghormatan kepada pohon biner yang digunakan sistem berkas untuk menyimpan direktori, bahwa sesi dapat bekerja sama sekali - Anda dapat membuat file dan mengambilnya tanpa penundaan. Hanya daftar yang tidak dapat digunakan. - Jason


    Apakah mungkin untuk mencadangkan semua file lain dari sistem file ini ke lokasi penyimpanan sementara, memformat ulang partisi, dan kemudian mengembalikan file?


    31
    2017-09-23 00:27



    Saya sangat suka jawaban ini, sebenarnya. Sebagai masalah praktis, dalam hal ini, tidak, tapi itu bukan yang saya pikirkan. Bravo! - BMDan
    Persis apa yang aku pikirkan juga. Ini adalah jawaban untuk pertanyaan 3. Ideal jika Anda bertanya kepada saya :) - Joshua


    Tidak ada batas file per direktori di ext3 hanya batas inode filesystem (saya pikir ada batasan pada jumlah subdirektori meskipun).

    Anda mungkin masih mengalami masalah setelah menghapus file.

    Ketika sebuah direktori memiliki jutaan file, entri direktori itu sendiri menjadi sangat besar. Entri direktori harus dipindai untuk setiap operasi penghapusan, dan itu membutuhkan berbagai waktu untuk setiap file, tergantung di mana entrinya berada. Sayangnya bahkan setelah semua file telah dihapus, entri direktori mempertahankan ukurannya. Jadi operasi lebih lanjut yang membutuhkan pemindaian entri direktori akan tetap membutuhkan waktu lama bahkan jika direktori tersebut sekarang kosong. Satu-satunya cara untuk memecahkan masalah itu adalah dengan mengganti nama direktori, membuat yang baru dengan nama lama, dan mentransfer semua file yang tersisa ke yang baru. Lalu hapus nama yang diganti namanya.


    11
    2017-09-23 05:45



    Memang, saya perhatikan hanya perilaku ini setelah menghapus semuanya. Untungnya, kami telah membuat direktori dari "garis api", karena itu, jadi saya hanya bisa rmdir itu. - BMDan
    Yang mengatakan, jika tidak ada batas file per direktori, mengapa saya mendapatkan "ext3_dx_add_entry: indeks Direktori penuh!" ketika masih ada inode yang tersedia di partisi itu? Tidak ada subdirektori di dalam direktori ini. - BMDan
    hmm saya melakukan sedikit riset dan sepertinya ada batasan jumlah blok yang bisa diambil direktori. Jumlah file yang tepat tergantung pada beberapa hal, misal panjang nama file. Ini gossamer-threads.com/lists/linux/kernel/921942 nampaknya mengindikasikan bahwa dengan blok 4k Anda harus dapat memiliki lebih dari 8 juta file dalam direktori. Apakah mereka nama file yang sangat panjang? - Alex J. Roberts
    Setiap nama file panjangnya persis 36 karakter. - BMDan
    baik itu saya kehabisan ide :) - Alex J. Roberts


    Saya belum mengukurnya, tapi orang ini melakukannya:

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

    5
    2018-06-04 11:52





    ternyata tidak bekerja untuk saya, bahkan setelah mengubah parameter ext3 fs seperti yang disarankan oleh pengguna di atas. Terlalu banyak memori yang dikonsumsi. Skrip PHP ini melakukan trik - cepat, penggunaan CPU tidak signifikan, penggunaan memori tidak signifikan:

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

    Saya memposting laporan bug mengenai masalah ini dengan menemukan: http://savannah.gnu.org/bugs/?31961


    4
    2017-12-23 19:54



    Ini menyelamatkan saya !! - jestro


    Saya baru-baru ini menghadapi masalah serupa dan tidak bisa mendapatkan ring0 data=writeback saran untuk bekerja (mungkin karena fakta bahwa file berada di partisi utama saya). Saat meneliti berbagai solusi, saya menemukan ini:

    tune2fs -O ^has_journal <device>
    

    Ini akan mematikan jurnal sepenuhnya, terlepas dari data opsi berikan kepada mount. Saya menggabungkan ini dengan noatime dan volume suara dir_index set, dan tampaknya bekerja cukup baik. Hapus itu benar-benar selesai tanpa saya perlu membunuhnya, sistem saya tetap responsif, dan sekarang kembali berjalan (dengan jurnal kembali) tanpa masalah.


    3
    2018-04-23 22:29



    Saya akan menyarankan pemasangan itu sebagai ext2 bukan ext3, untuk menghindari journal metadata ops. Ini harus melakukan hal yang sama. - Peter Cordes