Linux下快速删除大量小文件的方法探索

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/stayneckwind2/article/details/82819821

一、前言

Linux服务器磁盘中存在大量小文件,需要进行删除(文件小于1K,数量50w~100w),发现rm删除速度奇慢无比,甚至出现“argument list too long”的错误。网上资料一搜索基本都是建议rsync方法,所以本文对几种常见的方法进行试验对比。

二、相关知识

2.1 文件系统的元数据(metadata)

1)superblock:记录文件系统的整体信息,包含inode/block的大小、总量、使用量、剩余量,以及文件系统的格式,文件系统挂载时间,最近一次数据写入时间,最近一次校验磁盘的时间等。

2)inode:表示文件系统的对象,具备唯一标识符(一个文件占用一个inode),inode记录了文件的元信息,具体来说包含的信息为:文件所有者、访问权限(读、写、执行)、类型(是文件还是目录)、内容修改时间、inode修改时间、上次访问时间、对应的文件系统存储块(Block)的地址;

3)block:实际记录文件的内容,若文件太大,则会占用多个block,通常的block大小有1K,2K,4K三种,这里内核记录block信息的数据结构是Bitmap。

2.2 文件系统工作原理(ext2、ext3)

ext2 为索引式文件系统,新增一个文件的流程如下:

1)确定目录是否有写权限/打开权限(w/x),没有则返回失败;

2)根据inode-bitmap分配新的inode,将新文件权限/属性记入;

3)根据block-bitmap分配新的block,将文件数据写入block中,更新inode的block指向数据;

4)将新的inode、block数据同步到inode-bitmap、block-bitmap中,并更新superblock内容;

ext3 为日志式文件系统,对ext2的缺点进行改进(系统故障重启,修复元数据信息耗时长),多出一块日志记录区块来保证可靠性:

1)当系统准备写入一个文件时,先在日志记录区块记录文件要写入的信息;

2)写入文件权限/属性,写入文件数据,更新元数据内容(同ext2);

3)完成数据与元数据更新后,在日志记录区块完成文件的记录;

在出现故障需要恢复时,可根据日志追踪之前提交到主文件系统的更改,大大减少了磁盘的扫描时间,实现丢失数据的快速重建,比传统的索引式文件系统更安全[3]。Linux下的集中日志式文件系统有XFS(目前是CentOS7的默认文件系统),ReiserFS,Ext3,Ext4。


三、实验

3.1 实验环境

CPU:4核,Intel(R) Core(TM) i3-3120ME CPU @ 2.40GHz 

内存:8G

硬盘:1T(ST1000NM0055)

操作系统:Linux-3.10.25

文件系统:ext3

软件版本:find、rm(busybox1.24)、rsync3.1、bash3.2、perl5.8

3.2 实验步骤

1)创建文件:创建文件夹test,生成$num个8字节的文件,为防止文件名有规律,文件名给定随机的前缀,并且每次生成完成后都清空内存cache(echo 3 >/proc/sys/vm/drop_caches)

function genfiles()
{
        mkdir test
        for ((ix=0; ix<$num; ix++)) do
                local filename=$RANDOM
                echo -n "01234567" >test/$filename-$ix
                echo "Genfile($ix/$num): $filename"
        done
}

2)删除文件:根据文章[1]给出的方法,实验各种删除方法,并使用time给出统计时间

        time rm -rf test

        time find test -type f -delete;

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

        time ./rsync -a --delete $PWD/tmp/ $PWD/test/

3)生成100w个文件,测试结果如下:

测试方法 耗时
rm -rf test 11m12.703s
find test -type f -delete; 13m18.632s
cd test && perl -e 'for(<*>){((stat)[9]<(unlink))}' 19m17.510s
./rsync -a --delete $PWD/tmp/ $PWD/test/ 17m54.199s

结果并没有跟文章[1]描述的情况一致,rsync快速提升?!

四、编程改进

根据文件系统元数据的思路,考虑我生成100w小文件的时候,名字为随机命名的,但是inode号为正向顺序增长的(猜测blocks块按照一定规律写入,机械硬盘的磁头使用自身高效的方式进行写入),如果删除文件时按照inode号顺序进行排序效率会怎么样?思路如下:

1)遍历目录,获取文件列表,按照inode号大小排序;

2)按照文件列表删除文件;

3)两种方案进行速度比较:inode排序(-DORDER_BY_INODE)、文件名排序;

遍历目录,将文件插入到红黑树regtree中,目录插到vector中:

static int __dir_scan(const char *pname, REGTREE &regtree, vector<string> &dirlist)
{
        int cnt = 0, ret = 0;

        DIR *dir = NULL;
        struct dirent *entry = NULL;

        dir = opendir(pname);
        assert(dir);

        printf("dir: %s\n", pname);
        while ((entry = readdir(dir)) != NULL) {
                if (0 == strcmp(entry->d_name, ".") ||
                        0 == strcmp(entry->d_name, "..")) {
                        continue;
                }

                string fname = pname;
                fname.append("/");
                fname.append(entry->d_name);

                if (entry->d_type == DT_DIR) {
                        ret = __dir_scan(fname.c_str(), regtree, dirlist);
                        if (ret <= 0) {
                                cnt = 0;
                        }
                        else {
                                cnt += ret + 1;
                        }
                        dirlist.push_back(fname);
                }
                else {
#ifdef ORDER_BY_INODE
                        regtree.insert(pair<size_t, string>(entry->d_ino, fname));
#else
                        regtree.insert(pair<string, string>(fname, fname));
#endif
                        cnt++;
                }

                if (regtree.size() >= BATCH_SIZE) {
                        break;
                }
        }

        closedir(dir);
        return cnt;
}

根据dirlist删除目录、regtree删除文件:

static int __dir_remove(vector<string> &dirlist)
{
        int cnt = dirlist.size();

        for (size_t ix = 0; ix < dirlist.size(); ix++) {
                LOGN("rmdir: %s\n", dirlist[ix].c_str());
                rmdir(dirlist[ix].c_str());
        }

//      dirlist.clear();
        return cnt;
}

static int __reg_remove(REGTREE &regtree)
{
        int cnt = regtree.size();

        for (REGTREE::iterator iter = regtree.begin();
                 iter != regtree.end(); iter++) {
                LOGD("unlink: %llu %s\n", iter->first, iter->second.c_str());
                unlink(iter->second.c_str());
        }
        regtree.clear();

        LOGN("Files: %d\n", cnt);
        return cnt;
}

为了防止内存占用过大,100w文件列表的内存使用超过1Gb,期间对regtree.size()进行限制,当扫描文件列表达到10w后,先退出扫描,先把文件删除一部分后再继续

#include <stdio.h>
#include <dirent.h>
#include <assert.h>

#include <string>
#include <vector>
#include <map>

#define BATCH_SIZE 100000

#ifdef ORDER_BY_INODE
# define REGTREE map<size_t, string>
#else
# define REGTREE map<string, string>
#endif

using namespace std;

int main(int argc, char *argv[])
{
        int ret = FAILURE, cnt = 0;
        REGTREE regtree;
        vector<string> dirlist;

        while (1) {
                if (__dir_scan(argv[1], regtree, dirlist) == 0) {
                        cnt += __dir_remove(dirlist);
                        break;
                }
                else {
                        cnt += __reg_remove(regtree);
                }
        }

        ret = rmdir(argv[1]);
        exit(ret ? EXIT_FAILURE : EXIT_SUCCESS);
}

上面测试程序写的比较糙,vector没有对dirlist进行去重,多次扫描还会多次rmdir同一个目录;中间使用了assert,实际过程可能还会出现文件删除不掉,文件夹陆续还有新文件出现的情况;

测试数据一看还是蛮不错的,相比rsync、rm、find方法快了许多:

测试方法 耗时
./rm_by_inode test 0m53.135s
./rm_by_name test 9m55.988s

五、结论

这本先是根据文章[1]进行了实验,后来发现实验结果完全与文章不一致,又查看了一些文件系统的资料,后来自己写了代码实验了一下,最终发现ext3文件系统下,inode号与磁头处理效率是挂钩的,特此将步骤写下,后续工程中再继续实践~

参考文章:

[1] https://www.slashroot.in/which-is-the-fastest-method-to-delete-files-in-linux

[2] https://www.ibm.com/developerworks/cn/linux/l-linux-filesystem/

[3] https://zhuanlan.zhihu.com/p/22976640

猜你喜欢

转载自blog.csdn.net/stayneckwind2/article/details/82819821
今日推荐