在一个拥有数百万个文件的目录上

背景:大约两年前的物理服务器,连接到3Ware RAID卡的7200-RPM SATA驱动器,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% / 

所以我猜这意味着我们达到目录文件本身有多less条目的限制。 不知道会有多less文件,但不能多于三百万左右。 这不好,请介意! 但这是我的问题的一部分:究竟是什么上限? 它是可调的吗? 在我大喊之前 – 我想调整它; 这个巨大的目录造成了各种各样的问题。

无论如何,我们在生成所有这些文件的代码中跟踪了这个问题,并且我们已经纠正了这个问题。 现在我坚持删除目录。

这里有几个选项:

  1. rm -rf (dir)

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

  • 在目录中取消链接(2):绝对值得考虑,但问题是通过fsck删除目录中的文件是否比通过取消链接(2)删除目录更快。 就是这样或那样,我必须把这些inode标记为未使用。 这当然假设我可以告诉fsck不要把条目放到/ lost + found中的文件中; 否则,我刚刚移动了我的问题。 除了所有其他问题之外,在阅读了更多内容之后,我可能不得不调用一些内部的FS函数,因为我可以find的任何unlink(2)变体都不会让我轻易删除一个包含条目的目录。 呸。
  • while [ true ]; do ls -Uf | head -n 10000 | xargs rm -f 2>/dev/null; done )
  • 这实际上是缩短的版本; 我正在运行的是真正的,当我们用完文件删除时,它只是增加了一些进度报告和干净的停止:

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

    这似乎工作得很好。 正如我写这篇文章,过去三十分钟左右已经删除了26万个文件。 现在,对于这些问题:

    1. 如上所述,每个目录的入口限制是否可调?
    2. 为什么要用“真正的7m9.561s /用户0m0.001s / sys 0m0.001s”来删除ls -U返回的列表中的第一个文件,并且花了大概10分钟的时间来删除第一个10000在#3命令的条目,但现在是相当愉快地牵引? 就这一点而言,大约在三十分钟内就删掉了二十六万,现在又花了十五分钟,又删了六万。 为什么巨大的速度在摇摆?
    3. 有没有更好的方法来做这种事情? 不要在目录中存储数百万个文件; 我知道这很愚蠢,而且在我的手表上也不会发生。 谷歌search这个问题,并通过SF和SO寻找提供了很多变化find ,不会比我的方法明显快于几个不言而喻的原因。 但是,删除通过fsck的想法有任何的腿? 还是其他什么东西? 我渴望听到开箱即用(或不知名的盒子)的想法。

    感谢您阅读这本小说; 随时提问,我一定会回应。 我还会用最后的文件数量来更新这个问题,一旦我有了这个,就删除脚本运行多久。

    最终脚本输出!:

     2970000... 2980000... 2990000... 3000000... 3010000... real 253m59.331s user 0m6.061s sys 5m4.019s 

    所以,三百万个文件在四个多小时内被删除了。

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

    根据这个页面 ,

    使用时,某些应用程序显示出非常显着的速度提升。 例如,当应用程序创build和删除大量的小文件时,可以看到速度的提高(……)。

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

    这个问题的主要原因是数百万个文件的ext3性能,这个问题的根本原因是不同的。

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

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

    例如,在我创build的一个testing目录里面有超过260万个文件,运行“ls -1 | wc -l”显示了许多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 ... 

    另外,在这个目录花费的时间是很重要的。

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

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

    现在,你不应该手动调用getdents,所以没有界面可以正常使用(请查看gettes的man页面!),但是你可以手动调用它,并且使你的系统调用的调用方式更有效率。

    这大大减less了获取这些文件的时间。 我写了一个这样的程序。

     $ time ./dentls bigfolder >out.txt real 0m2.355s user 0m0.326s sys 0m1.995s 

    效率将近十倍! 我怀疑,目录越大,效率就越高。

    我已经在下面提供了这个程序的源代码。 如果你想删除,取消注销取消链接的行。 这将大大减缓我想象中的performance。 它还避免打印/取消链接不是文件的任何内容。

    它会把文件名吐出到标准输出。 你可能应该redirect这个输出。 如果需要,可以使用它来删除程序之外的文件。

     /* I can be compiled with the command "gcc -o dentls dentls.c" */ #define _GNU_SOURCE #include <search.h> /* Defines tree functions */ #include <dirent.h> /* Defines DT_* constants */ #include <fcntl.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/stat.h> #include <sys/syscall.h> #include <sys/types.h> #include <string.h> /* Because most filesystems use btree to store dents * its very important to perform an in-order removal * of the file contents. Performing an 'as-is read' of * the contents causes lots of btree rebalancing * that has significantly negative effect on unlink performance */ /* Tests indicate that performing a ascending order traversal * is about 1/3 faster than a descending order traversal */ int compare_fnames(const void *key1, const void *key2) { return strcmp((char *)key1, (char *)key2); } void walk_tree(const void *node, VISIT val, int lvl) { int rc = 0; switch(val) { case leaf: printf("%s\n", *(char **)node); // rc = unlink(*(char **)node); break; /* End order is deliberate here as it offers the best btree * rebalancing avoidance. */ case endorder: printf("%s\n", *(char **)node); // rc = unlink(*(char **)node); break; default: return; break; } if (rc < 0) { perror("unlink problem"); exit(1); } } void dummy_destroy(void *nil) { return; } void *tree = NULL; struct linux_dirent { long d_ino; off_t d_off; unsigned short d_reclen; char d_name[256]; char d_type; }; int main(const int argc, const char** 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; /* Test we have a directory path */ if (argc < 2) { fprintf(stderr, "You must supply a valid directory path.\n"); exit(1); } const char *path = argv[1]; /* Standard sanity checking stuff */ if (access(path, R_OK) < 0) { perror("Could not access directory"); exit(1); } if (lstat(path, &dstat) < 0) { perror("Unable to lstat path"); exit(1); } if (!S_ISDIR(dstat.st_mode)) { fprintf(stderr, "The path %s is not a directory.\n", path); exit(1); } /* Allocate a buffer of equal size to the directory to store dents */ if ((buffer = malloc(dstat.st_size+10240)) == NULL) { perror("malloc failed"); exit(1); } /* Open the directory */ if ((dirfd = open(path, O_RDONLY)) < 0) { perror("Open error"); exit(1); } /* Switch directories */ fchdir(dirfd); while (bufcount = syscall(SYS_getdents, dirfd, buffer, dstat.st_size+10240)) { offset = 0; dent = buffer; while (offset < bufcount) { /* Dont 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) { /* Sort all our files into a binary tree */ if (!tsearch(dent->d_name, &tree, compare_fnames)) { fprintf(stderr, "Cannot acquire resources for tree!\n"); exit(1); } totalfiles++; } } offset += dent->d_reclen; dent = buffer + offset; } } fprintf(stderr, "Total files: %d\n", totalfiles); printf("Performing delete..\n"); twalk(tree, walk_tree); printf("Done\n"); close(dirfd); free(buffer); tdestroy(tree, dummy_destroy); } 

    虽然这不能解决潜在的根本问题(很多文件,在文件系统中performance不佳)。 它可能比许多替代scheme要快得多。

    作为一个深谋远虑,应该删除受影响的目录,然后重新制作。 由于目录的大小,目录只有不断增加的大小,即使只有less量文件,其性能仍然很差。

    编辑:我今天重新访问了这个,因为大多数文件系统以btree格式存储它们的目录结构,你删除文件的顺序也很重要。 当您执行取消链接时,需要避免重新平衡btree。 因此,我在删除之前添加了一个sorting。

    程序现在(在我的系统上)在43秒内删除1000000个文件。 最近的程序是rsync -a --delete ,花费了60秒(它也按顺序删除,但不执行有效的目录查找)。

    编辑2:删除了注释中提到的阻止编译的额外括号。

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

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

    删除文件后,您可能仍然有问题。

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

    发现根本没有为我工作,即使改变ext3 fs的参数,如上面的用户build议。 消耗的方式太多的内存。 这个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

    我没有做基准testing,但是这个人做了 :

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

    确保你做到:

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

    这也会加快速度。

    这个命令很慢 尝试:

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

    我最近面临类似的问题,无法得到ring0的data=writebackbuild议工作(可能是由于这些文件在我的主分区)。 在研究变通办法时,我偶然发现了这一点:

     tune2fs -O ^has_journal <device> 

    这将完全closures日志logging,无论data选项是否为mount 。 我把它与noatime结合起来,音量也设置了dir_index ,而且看起来工作得很好。 删除实际上完成,没有我需要杀死它,我的系统保持响应,它现在是备份和运行(与日记回来)没有问题。

    dir_index是为文件系统设置的吗? ( tune2fs -l | grep dir_index )如果没有,启用它。 这通常是为了新的RHEL。

    我的首选选项是已经build议的newfs方法。 基本问题同样如前所述,处理删除的线性扫描是有问题的。

    对于本地文件系统, rm -rf应该接近最佳值(NFS会有所不同)。 但是,在数百万个文件中,每个文件名有36个字节,每个inode有4个(猜测,不检查ext3的值),这是40 *

    有人猜测,你在Linux中打乱了文件系统的元数据caching内存,所以当你还在使用另一个部分的时候,目录文件的一个页面块将被清除,只有当下一个页面文件被删除。 Linux性能调整不是我的区域,但/ proc / sys / {vm,fs} /可能包含相关的内容。

    如果可以承受停机时间,则可以考虑打开dir_indexfunction。 它将目录索引从线性切换到大型目录(散列b-树)中删除更优化的东西。 tune2fs -O dir_index ...紧接着是e2fsck -D 。 不过,虽然我有信心出现问题之前有所帮助但我不知道在处理现有的v.large目录时转换(带-D e2fsck)是如何执行的。 备份+吸收和看到。

    显然不是苹果在这里,但我设置了一个小testing,并做了以下几点:

    在一个目录中创build100,000个512字节的文件(循环中的dd/dev/urandom ); 忘了时间,但花了大约15分钟来创build这些文件。

    按以下步骤删除所述文件:

    ls -1 | wc -l && time find . -type f -delete

     100000 real 0m4.208s user 0m0.270s sys 0m3.930s 

    这是一个奔腾4 2.8GHz的盒子(几百GB的IDE 7200转我想; EXT3)。 内核2.6.27。

    有时Perl可以在这样的情况下创造奇迹。 如果像这样的小脚本可以胜过bash和基本的shell命令,你已经试过了吗?

     #!/usr/bin/perl open(ANNOYINGDIR,"/path/to/your/directory"); @files = grep("/*\.png/", readdir(ANNOYINGDIR)); close(ANNOYINGDIR); for (@files) { printf "Deleting %s\n",$_; unlink $_; } 

    或者,也许更快,Perl的方法:

     #!/usr/bin/perl unlink(glob("/path/to/your/directory/*.png")) or die("Could not delete files, this happened: $!"); 

    编辑:我只是给我的Perl脚本尝试。 更详细的做一些正确的事情。 在我的情况下,我试图用256 MB的RAM和五十万个文件的虚拟服务器。

    time find /test/directory | xargs rm time find /test/directory | xargs rm结果:

     real 2m27.631s user 0m1.088s sys 0m13.229s 

    相比

     time perl -e 'opendir(FOO,"./"); @files = readdir(FOO); closedir(FOO); for (@files) { unlink $_; }' real 0m59.042s user 0m0.888s sys 0m18.737s 

    从我记得在ext文件系统中删除inode是O(n ^ 2),所以你删除的文件越多,剩下的就越快。

    曾经有一次我遇到过类似的问题(虽然我的估计是看着〜7小时的删除时间),最后去了jftuga的build议路线。

    那么,这不是一个真正的答案,但…

    是否有可能将文件系统转换为ext4,看看事情是否改变?

    好吧,在线程的其他部分已经有了各种各样的方式,但是我想我会投入我的两分钱。 你的情况中的性能元凶可能是readdir。 您取回的文件列表不一定是以任何顺序在磁盘上导致磁盘访问的地方,当你解除链接。 这些文件足够小,解除链接操作可能不会太多地调整空间。 如果你阅读,然后按升序inodesorting,你可能会获得更好的性能。 所以读入公羊(按inodesorting) – >取消链接 – >利润。

    Inode在这里是一个粗略的近似值,但是根据你的用例可能相当准确。

    我可能会掏出一个C编译器,并完成了脚本的道德等价物。 也就是说,使用opendir(3)来获取一个目录句柄,然后使用readdir(3)来获取文件的名称,然后合并文件,因为我取消了链接,偶尔打印“%d个文件被删除”已用时间或当前时间戳)。

    我不希望它比shell脚本版本明显更快,只是我习惯于不得不一次次地翻译编译器,要么是因为没有干净的方式来从shell中做我想做的事情,要么是因为虽然可以在shell中执行,但这种方式非常慢。

    你可能会遇到目录的重写问题。 尝试先删除最新的文件。 查看将回写到磁盘的安装选项。

    对于进度条,尝试运行诸如rm -rv /mystuff 2>&1 | pv -brtl > /dev/null rm -rv /mystuff 2>&1 | pv -brtl > /dev/null

    Here is how I delete the millions of trace files that can sometimes gather on a large Oracle database server:

     for i in /u*/app/*/diag/*/*/*/trace/*.tr? ; do rm $i; echo -n . ; done 

    I find that this results in a fairly slow deletion that has low impact on server performance, usually something along the lines of an hour per million files on a "typical" 10,000 IOPS setup.

    It will often take several minutes before the directories have been scanned, the initial file list generated and the first file is deleted. From there and on, a . is echoed for every file deleted.

    The delay caused by echoing to the terminal has proven enough of a delay to prevent any significant load while deletion is progressing.

    A couple of years back, I found a directory with 16 million XML files in the / filesystem. Due the criticity of the server, we used the following command that took about 30 hours to finish:

     perl -e 'for(<*>){((stat)[9]<(unlink))}' 

    It was an old 7200 rpm hdd, and despite the IO bottleneck and CPU spikes, the old webserver continued its service.

    You could use 'xargs' parallelization features:

     ls -1|xargs -P nb_concurrent_jobs -n nb_files_by_job rm -rf 
     ls|cut -c -4|sort|uniq|awk '{ print "rm -rf " $1 }' | sh -x 

    actually, this one is a little better if the shell you use does command line expansion:

     ls|cut -c -4|sort|uniq|awk '{ print "echo " $1 ";rm -rf " $1 "*"}' |sh