用Shell并发删除一个目录下的大量碎文件,并分享整个删除过程
建议
建议文件量特别多时分开目录存储(按日期或者按产品ID等),不然后续处理会很棘手
代码多写一点,为后来人多考虑一点。
缘由 因为生产环境中使用到了阿里云的NAS服务, 有大量碎文件存储到了NAS中
后来NAS收费太贵,就将历史数据迁移到了OSS中
迁移完成后需要删除NAS中的碎文件
大约150T
,30亿
个文件
尝试的删除方法
shutil.rmtree(/nas/data/2019 -01 -01 /)
rsync -a --delete /opt/empty/ /nas/data/2019-01-01/
rm -rf /nas/data/2019-01-01/
最终尝试了以上三种方法,发现都非常慢
通过strace命令发现进程主要在做getdents (readdir)操作(获取文件列表)
另外提一句如果是本地数据还好, 阿里云NAS针对readdir
操作做了部分限制, 导致获取文件列表巨慢···
怎么发现的呢?
我启动了20个线程去获取20个目录下的文件列表,发现机器与NAS之间的流量始终只能到30Mb, 后来又启动了40个线程也是到30Mb, 然后跟阿里云沟通结果是他们需要单独调整参数才能优化读取操作, 因为调整需要reload nas服务,对线上有影响,我就放弃了没让他们做···
内核参数调优
因为阿里云NAS最终是以NFS的方式提供服务
所以官方建议调整OS kernel的限制
修改参数后需要重新挂载 NAS或者重启系统
具体说明见阿里云文档
Kernel 2.6(Centos6)左右的内核限制为128
echo "options sunrpc tcp_slot_table_entries=128" >> /etc/modprobe.d/sunrpc.conf echo "options sunrpc tcp_max_slot_table_entries=128" >> /etc/modprobe.d/sunrpc.conf sysctl -w sunrpc.tcp_slot_table_entries=128
Kernel 3(Centos7)的内核限制为65535
echo "options sunrpc tcp_slot_table_entries=65535" >> /etc/modprobe.d/sunrpc.conf echo "options sunrpc tcp_max_slot_table_entries=65535" >> /etc/modprobe.d/sunrpc.conf sysctl -w sunrpc.tcp_slot_table_entries=65535
最佳方案
先获取文件列表后并发删除
1. 获取文件列表
为什么要加后面的两个参数?
默认情况下,ls
命令将对其输出进行排序
。要做到这一点,它必须首先将每个文件的名称篡改到内存中。面对一个非常大的目录,它将坐在那里,读取文件名,占用越来越多的内存,直到最终按字母数字顺序一次列出所有文件。
而ls -1 -f
则不执行任何排序 。它只是读取目录并立即显示文件。
具体说明文档请参考大神文档
2.切分文件 split -l 10000000 -d list split-tmp- # 100w行一个文件 # 文件名以"split-tmp-" 开头 # 文件名以数字结尾
3.并发删除 shell 实现进程并发控制
关于shell的多进程并发见大神文档
# !/bin/sh # 定义日志 Log=./rm.log # 指定并发数量 Nproc=20 # 接受信号2 (ctrl +C)做的操作 trap "exec 1000>$-;exec 1000<&-;exit 0" 2 # $$是进程pid Pfifo="/tmp/$$.fifo" mkfifo $Pfifo # 以1000为文件描述符打开管道,<>表示可读可写 exec 1000<>$Pfifo rm -f $Pfifo # 向管道中写入Nproc行,作为令牌 for((i=1; i<=$Nproc; i++)); do echo done >&1000 filenames=`ls split-tmp-*` for filename in $filenames; do # 从管道中取出1行作为token,如果管道为空,read 将会阻塞 # man bash可以知道-u是从fd中读取一行 read -u1000 { #所要执行的任务 DirPrefix=/nas/data while read line;do rm -I $DirPrefix/$line || echo "`date +%F-%T` rm -rf $DirPrefix/$line failed" | tee >> $Log done < $filename && { echo "`date +%F-%T` $filename done" | tee >> $Log } || { echo "`date +%F-%T` $filename error" | tee >> $Log } sleep 5 #归还token echo >&1000 }& done # 等待所有子进程结束 wait # 关闭管道 exec 1000>&- exec 1000<&-
另外说一个其他的删除,针对以下格式
针对这种单个目录下可能只有一两万个文件的情况
可以用python并发删除
实测: 每分钟删除NAS里2.6w个文件
import osimport shutilimport timeimport threadingimport datetimetarget_path = "/data/" pathnames = os.listdir(target_path) today = datetime.datetime.now() month_ago = today + datetime.timedelta(days=-30 ) def dirdel (tpath) : t1 = time.time() print "start delete:" ,tpath shutil.rmtree(tpath) print "deleted: %s in %s" %(tpath,time.time()-t1) for date_path in pathnames: tmp_path = target_path+date_path if not os.path.isdir(tmp_path): continue if (datetime.datetime.strptime(date_path, "%Y-%m-%d" )<month_ago): print(tmp_path) while len(threading.enumerate())>40 : print "waiting..." time.sleep(30 ) threading.Thread(target=dirdel, args=(tmp_path,)).start()