题 rm在包含数百万个文件的目录上


背景:物理服务器,大约两年,7200-RPM SATA驱动器连接到3Ware RAID卡,ext3 FS安装noatime和data =有序,不在疯狂加载,内核2.6.18-92.1.22.el5,正常运行时间545天。目录不包含任何子目录,只有数百万个小(~100字节)文件,一些较大(几KB)文件。

我们有一台服务器在过去的几个月中已经有点杜鹃了,但是前几天我们只注意到它因为包含太多文件而无法写入目录。具体来说,它开始在/ var / log / messages中抛出此错误:

ext3_dx_add_entry: Directory index full!

有问题的磁盘有很多inode:

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

所以我猜这意味着我们达到了目录文件本身可以有多少条目的限制。不知道会有多少文件,但正如你所看到的,它不能超过三百万左右。请注意,这不是那么好!但这是我的问题的一部分:究竟是什么是上限?它是可调的吗?在我被大喊之前 - 我想调整它 ;这个巨大的目录引起了各种各样的问题。

无论如何,我们在生成所有这些文件的代码中追踪了这个问题,我们已经纠正了它。现在我不得不删除目录了。

这里有几个选择:

  1. rm -rf (dir)

我先试了一下。在它运行了一天半之后,我放弃并杀死它,没有任何明显的影响。

  • unlink(2)在目录上:绝对值得考虑,但问题是通过fsck删除目录中的文件是否比通过unlink(2)删除更快。也就是说,无论如何,我必须将这些inode标记为未使用。当然,这假设我可以告诉fsck不要删除/ lost + found中的文件的条目;否则,我刚刚解决了我的问题。除了所有其他问题之外,在阅读了这一点之后,事实证明我可能不得不调用一些内部FS函数,因为我找不到的unlink(2)变体都不允许我只是轻率地删除包含条目的目录。呸。
  • while [ true ]; do ls -Uf | head -n 10000 | xargs rm -f 2>/dev/null; done )
  • 这实际上是缩短版;我正在运行的真正的一个,它只是在我们用完要删除的文件时添加一些进度报告和一个干净的停止,是:

    export i = 0;
    时间(而[真];做
      ls -Uf |头-n 3 | grep -qF'.png'||打破;
      ls -Uf | head -n 10000 | xargs rm -f 2> / dev / null;
      export i = $(($ i + 10000));
      回声“$ i ......”;
    完成)

    这似乎工作得很好。在我写这篇文章时,它在过去30分钟左右删除了260,000个文件。


    97
    2017-09-22 23:57




    rm(GNU coreutils)8.4有这个选项: “-v, - verbose解释正在做什么”。它将显示正在删除的所有文件。 - Cristian Ciupitu
    实际上,这是一个很好的方法来进行进度条:由于每个文件长度为37个字符(36 + a'\ n'),我可以轻松地为其编写解析器,因为printf()是廉价和rm命令已经加载了文件的名称,没有特别的性能损失。对于做整个shebang来说,似乎不是一个非首发,因为无论如何我永远不会“做”这样的事情。但它可以作为一个10,000内的进度条很好地工作;也许是一个“。”每百个文件? - BMDan
    rm -rfv | pv -l >/dev/null。光伏应该在 EPEL 库。 - Cristian Ciupitu
    光伏非常棒。我留下了一些光伏装置。 - BMDan
    我最近有同样的问题。谢谢! - richo


    答案:


    data=writeback 值得尝试的mount选项,以防止文件系统的日志记录。这应该仅在删除时间内完成,但是如果在删除操作期间服务器正在关闭或重新启动,则存在风险。

    根据 这一页

    一些应用程序在使用时显示出非常显着的速度提升。例如,当应用程序创建和删除大量小文件时,可以看到速度改进(...)。

    该选项设置为 fstab 或在安装操作期间,更换 data=ordered 同 data=writeback。必须重新装入包含要删除的文件的文件系统。


    30
    2017-09-26 05:49



    他还可以增加时间 commit  选项:“此默认值(或任何低值)将损害性能,但它有利于数据安全。将其设置为0将具有与保持默认值(5秒)相同的效果。将其设置为非常大的值将提高绩效“。 - Cristian Ciupitu
    回写看起来很棒,除了我正在查看的文档(gentoo.org/doc/en/articles/l-afig-p8.xml#doc_chap4)明确提到它仍然记录元数据,我认为这包括我正在改变的所有数据(我当然不会改变文件中的任何数据)。我对该选项的理解是否不正确? - BMDan
    最后,FYI,在该链接中未提及的事实是data = writeback可能是一个巨大的安全漏洞,因为给定条目指向的数据可能没有应用程序写入的数据,这意味着可能导致崩溃在旧的,可能敏感/私人的数据暴露。这里不是问题,因为我们只是暂时开启它,但我想提醒大家注意这个警告,以防你或其他遇到这个建议的人不知道。 - BMDan
    承诺:那太漂亮了!谢谢你的指针。 - BMDan
    data=writeback 在将元数据写入主文件系统之前仍然记录元数据。据我了解,它只是不强制执行诸如编写扩展区映射和将数据写入这些扩展区之类的命令。也许还有其他排序约束,如果你从中看到了一个性能增益,它也会放松。当然,没有轴颈的安装可能会提高性能。 (它可能让元数据更改只发生在RAM中,而不需要在unlink操作完成之前在磁盘上有任何内容)。 - Peter Cordes


    虽然这个问题的主要原因是具有数百万个文件的ext3性能,但是这个问题的实际根本原因是不同的。

    当需要列出目录时,在产生文件列表的目录上调用readdir()。 readdir是一个posix调用,但这里使用的真正的Linux系统调用称为“getdents”。 Getdents通过填充带有条目的缓冲区来列出目录条目。

    问题主要在于readdir()使用32Kb的固定缓冲区大小来获取文件。随着目录变得越来越大(随着文件的添加,大小越来越大),ext3变得越来越慢以获取条目,而额外的readdir的32Kb缓冲区大小仅足以包含目录中的一小部分条目。这会导致readdir反复循环并反复调用昂贵的系统调用。

    例如,在我创建的测试目录中,内部有超过260万个文件,运行“ls -1 | wc-l”显示许多getdent系统调用的大量strace输出。

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

    此外,在此目录中花费的时间非常重要。

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

    使这个更有效的过程的方法是使用更大的缓冲区手动调用getdents。这显着提高了性能。

    现在,您不应该自己手动调用getdents,因此没有正常使用它的界面(查看手册页以获取getdents!),但是你 能够 手动调用它,使您的系统调用调用方式更有效。

    这大大减少了获取这些文件所需的时间。我写了一个执行此操作的程序。

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

    虽然这并没有解决潜在的基本问题(许多文件,在文件系统中表现不佳)。它可能比发布的许多替代品快得多,快得多。

    作为一个预见,应该删除受影响的目录并在之后重新制作。由于目录的大小,即使内部有少量文件,目录也只会增加大小并且仍然表现不佳。

    编辑: 我已经清理了很多这个。添加了一个选项,允许您在运行时在命令行上删除并删除了一堆树形行为的东西,老实说回顾充其量是有问题的。也被证明会产生内存损坏。

    你现在可以做 dentls --delete /my/path

    新结果。基于一个拥有182万个文件的目录。

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

    有点意外,这仍然有效!


    73
    2017-11-06 19:06



    两个小问题:一, [256] 应该是 [FILENAME_MAX]和两个,我的Linux(2.6.18 == CentOS 5.x)似乎没有在dirent中包含d_type条目(至少根据getdents(2))。 - BMDan
    你能否详细说明btree重新平衡以及为什么删除有助于防止它?我试过Googling,但遗憾的是无济于事。 - ovgolovin
    因为现在我觉得如果我们按顺序删除,我们强制重新平衡,因为我们删除一边的叶子而留下另一边: en.wikipedia.org/wiki/B-tree#Rebalancing_after_deletion - ovgolovin
    我希望我不会因为这件事而打扰你。但我仍然开始有关按顺序删除文件的问题 stackoverflow.com/q/17955459/862380,这似乎没有得到一个答案,将解释这个例子的问题,这对普通程序员来说是可以理解的。如果你有时间并且感觉如此,你能看一下吗?也许你可以写一个更好的解释。 - ovgolovin
    这是一段惊人的代码。它是我能找到的唯一能够列出和删除在目录中构建的11,000,000(一千一百万)个会话文件的工具,可能在几年之内。 Plesk进程本来应该使用其他答案中的查找和其他技巧来控制它们,但是无法完成运行,因此文件不断积累。它是对文件系统用于存储目录的二叉树的致敬,会话能够完全工作 - 您可以创建一个文件并毫无延迟地检索它。只是列表无法使用。 - Jason


    是否可以将此文件系统中的所有其他文件备份到临时存储位置,重新格式化分区,然后还原文件?


    31
    2017-09-23 00:27



    实际上,我真的很喜欢这个答案。实际上,在这种情况下,不,但不是我想到的。好样的! - BMDan
    正是我在想什么。这是问题3的答案。如果你问我,理想的选择:) - Joshua


    ext3中没有每个目录文件限制只是文件系统inode限制(我认为子目录的数量有限制)。

    删除文件后,您可能仍会遇到问题。

    当目录有数百万个文件时,目录条目本身就变得非常大。必须为每个删除操作扫描目录条目,并且每个文件需要花费不同的时间,具体取决于其条目的位置。不幸的是,即使删除了所有文件,目录条目仍保留其大小。因此,即使目录现在为空,需要扫描目录条目的进一步操作仍将花费很长时间。解决该问题的唯一方法是重命名目录,使用旧名称创建一个新目录,并将任何剩余文件传输到新目录。然后删除重命名的那个。


    11
    2017-09-23 05:45



    实际上,在删除所有内容后我注意到了这种行为。幸运的是,我们已经将目录从“火线”中删除了,因此我可以直接使用它。 - BMDan
    也就是说,如果没有每个目录文件的限制,为什么我得到“ext3_dx_add_entry:目录索引已满!”当该分区上仍有可用的inode时?此目录中没有子目录。 - BMDan
    嗯我做了一些研究,似乎目录可以占用的块数量有限。确切的文件数取决于一些事情,例如文件名长度。这个 gossamer-threads.com/lists/linux/kernel/921942 似乎表明,使用4k块,你应该能够在一个目录中拥有超过800万个文件。他们的文件名特别长吗? - Alex J. Roberts
    每个文件名长度正好为36个字符。 - BMDan
    好吧,这是我的想法:) - Alex J. Roberts


    我没有对它进行基准测试,但是 这家伙做到了

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

    5
    2018-06-04 11:52





    即使在改变了上面用户建议的ext3 fs参数之后,发现根本不适合我。消耗的方式太多记忆。这个PHP脚本实现了这个技巧 - 快速,无关紧要的CPU使用率,无关紧要的内存使用情况:

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

    我发布了一个关于这个问题的bug报告: http://savannah.gnu.org/bugs/?31961


    4
    2017-12-23 19:54



    这救了我!! - jestro


    我最近遇到了类似的问题,无法获得ring0 data=writeback 建议工作(可能是由于文件在我的主分区上)。在研究变通方法时,我偶然发现了这个问题:

    tune2fs -O ^has_journal <device>
    

    这将完全关闭日记,无论如何 data 选项给 mount。我结合这个 noatime 和音量 dir_index 设置,似乎工作得很好。删除实际上没有我需要杀死它,我的系统仍然响应,它现在恢复运行(日志重新开启)没有问题。


    3
    2018-04-23 22:29



    我打算建议将它安装为ext2而不是ext3,以避免日志记录元数据操作。这应该做同样的事情。 - Peter Cordes


    确保你这样做:

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

    这也应该加快速度。


    3
    2017-09-27 02:03



    好的电话,但它已经安装了noatime,正如我在问题的标题中提到的那样。而且nodiratime是多余的;看到 lwn.net/Articles/245002 。 - BMDan
    ppl重复这个咒语“noatime,nodiratime,nodevatime,noreadingdocsatime” - poige