用Shell并发删除一个目录下的大量碎文件,并分享整个删除过程

建议

建议文件量特别多时分开目录存储(按日期或者按产品ID等),不然后续处理会很棘手

代码多写一点,为后来人多考虑一点。

缘由

因为生产环境中使用到了阿里云的NAS服务, 有大量碎文件存储到了NAS中

后来NAS收费太贵,就将历史数据迁移到了OSS中

迁移完成后需要删除NAS中的碎文件

大约150T,30亿个文件

尝试的删除方法

  • python
shutil.rmtree(/nas/data/2019-01-01/)
  • rsync
rsync -a --delete /opt/empty/ /nas/data/2019-01-01/
  • rm
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 -1 -f DIR

为什么要加后面的两个参数?

​ 默认情况下,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<&-

另外说一个其他的删除,针对以下格式

data/年-月-日/[0-1023]/file

针对这种单个目录下可能只有一两万个文件的情况

可以用python并发删除

实测: 每分钟删除NAS里2.6w个文件

import os
import shutil
import time
import threading
import datetime

target_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()