linux下删除大量小文件的方案

之前遇到过这样一个case:

背景: 需要删除一个拥有大量小文件的目录
特征: 文件数达到数千万,每个文件平均不到1k
删除方式: 直接rm -rf
影响: cpu磁盘io util持续达到100%,且机器上大多数有磁盘io的进程都进入D状态,删除时间长达小时级别

这显然是不能接受的。为此,通过一些实验和STFG,开始解决这个问题。

为什么删除大量小文件会有这种影响?

通过简单的 strace rm file 可以看到下面几行:

lstat("file", {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
access("file", W_OK) = 0
unlink("file") = 0

由此来看,每删除一个文件,都要进行三次系统调用: lstat access unlink,其中 unlink 由于需要更新文件系统所以一定会刷磁盘。而 unlink 操作和文件内容没关系,只和文件占用多少inode(block)相关,因此每次删除操作真正写入磁盘的内容很少,并且是不能被合并的。这样会导致大量的io write请求。同时,在相应文件的inode没有进buffers的情况下, lstat 也会产生大量的io read请求。如果文件在写入的时候具有随机特性的话(表现为inode在文件系统中分的很散),会使得io都是随机读写,也会对性能有很大的影响。

因此,在删除小量文件的时候无所谓,但在删除大量文件时会打满磁盘io,并使cpu频繁的进行中断,进而对其他进程造成较大影响。

删除大量小文件有哪些方案?

rm -rf directory
经典做法,童叟无欺

find /path/to/folder -type f -delete
find的各种变种效果类似,可解决rm -rf *时参数过长溢出的问题。也可以做一些过滤,只删除一些符合一定特征的文件。

perl -e 'for(<>){((stat)[9]<(unlink))}'
神一样的代码,但本质上和直接 *rm
做的事情差不多

rsync --delete -r empty/ folder/
网上盛传的推荐方案,从strace上看会先对文件名排序,然后执行 lstat unlink 的系统调用。据说性能会快很多,但真的会这样么?且看下面的测试

rsync真的比直接rm快么?

quoraslashroot等网站上,都推荐rsync的方式来删除大量小文件,甚至给出了所测的benchmark。

最初时,我也认同了这个结论,并在自己的机器上做了一组benchmark,得出的结果与之相符。

测试方案及具体数据如下:

  • 随机生成100w小文件(总大小约4G),使用rm –rf删除,统计iostat和时间
  • 随机生成100w小文件,使用rsync删除,统计iostat和时间
  • dd一个20G大小文件,统计iostat和时间
  • 同时rm + dd,统计iostat和时间,测试rm对dd的影响
  • 同时rsync + dd,统计iostat和时间,测试rsync对dd的影响 测试数据

从测试数据来看,rsync因为将多数写操作合并,wrqm、avgrq-sz显著增高,w、avgqu-sz显著降低,await降低,时间也快很多。rsync在删除大量小文件显著优于rm;
对dd读写的影响上,rm会使dd时间显著增加,rsync使dd时间增加不明显。rsync对其他写操作的影响显著优于rm。

但 是 为 什 么?
且做这样的推论:

  1. 由于生成测试文件是一次性顺序生成的,因此文件inode在文件系统的红黑数中应该也是顺序的,且和文件名同序;
  2. 从strace来看,rsync应当在cpu的用户态对文件名进行了一次排序,而rm是乱序的。所以rsync的操作在落到磁盘上时,读io和写io应该都比较近,因此可以做一些合并。如上图的wrqm数据;
  3. 刚生成好文件就做这样的测试,在生成文件的时候inode应该已经进入内存的buffers里,因此在遍历文件的时应当read操作会大幅减少。而因为rsync做了一次排序,因此buffers的命中率应该会更高。

由此可见,rsync比rm的数据好看,很大程度上是顺序生成测试文件且进入内存buffers的功劳。如果文件是完全随机生成的,时间和空间上都没有显著特征,然后在清空buffers的条件下进行测试,效果会是怎么样呢?

** 测试1:**
文件来源:线上真实生成的cache文件
文件数:751817
文件总大小:3.0G
rm -rf 删除时间:

echo 3 > /proc/sys/vm/drop_caches
/usr/bin/time -v rm -rf b
Command being timed: "rm -rf b"
User time (seconds): 0.17
System time (seconds): 21.60
Percent of CPU this job got: 1%
Elapsed (wall clock) time (h:mm:ss or m:ss): 20:45.34
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 1792
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 145
Voluntary context switches: 61456
Involuntary context switches: 95
Swaps: 0
File system inputs: 6444416
File system outputs: 0
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0

** 测试2:**
文件来源:线上真实生成的cache文件2
文件数:751840
文件总大小:3.0G
rsync 删除时间:

echo 3 > /proc/sys/vm/drop_caches
/usr/bin/time -v rsync --delete -r empty/ c/
Command being timed: "rsync --delete -r empty/ c/"
User time (seconds): 1.64
System time (seconds): 24.78
Percent of CPU this job got: 2%
Elapsed (wall clock) time (h:mm:ss or m:ss): 20:18.46
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 407056
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 2
Minor (reclaiming a frame) page faults: 25739
Voluntary context switches: 60303
Involuntary context switches: 698
Swaps: 0
File system inputs: 2689048
File system outputs: 8
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0

可以看出,上述两组数据几乎每一项都没有很大的差别,rsync并没有比直接rm更快

其实这样的数据反倒是符合我个人的预期。毕竟无论怎么删文件,最终都要落到 ulink 的系统调用上,都要去落磁盘,不应该出现质的差别。

怎么解决io util过高的问题?

欢迎使用 ionice:

ionice - get/set program io scheduling class and priority

Examples:

ionice -c 3 rm -rf dir
设置io调度策略为idle级别,并在这种策略下执行删除操作
注意:调整ionice后会降低文件删除的速度。同时虽然通过iostat看到io util依然是100%,但对其他进程的影响会显著降低。

总结

  1. 不要迷信 rsync,在正常情况下 rsync 并不会比 rm -rf 好多少
  2. 在删除一些顺序生成的文件时,可以考虑优先使用 rsync 的方式,至少不会更差
  3. 使用ionice,可以显著解决一些低优先级io的磁盘占用,避免对其他进程的影响