一次恶心的删除minio文件之旅
刚入职一家公司,需求下来了需求由于minio占用空间极速扩大,目前已有3.5T,其中有一个桶buket1下的images目录(就是存放图片的)所占空间为1.5T,只保留最近几天的文件,两天内删除以前全部的文件。分析image目录下都是1KB-1MB的小文件,每天按日期yyyyMMdd产生一个目录,并且文件都放在各自的md5目录下,这就导致一个目录下存在几万甚至十几万个目录文件。举个栗子:http:
刚入职一家公司,需求下来了
需求
由于minio占用空间极速扩大,目前已有3.5T,其中有一个桶buket1下的images目录(就是存放图片的)所占空间为1.5T,只保留最近几天的文件,两天内删除以前全部的文件。
分析
image目录下都是1KB-1MB的小文件,每天按日期yyyyMMdd产生一个目录,并且文件都放在各自的md5目录下,这就导致一个目录下存在几万甚至十几万个目录文件。
举个栗子:http://ip:port/buket1/images/20210601/526F6BCD5661D393CADE4E832523B5F8/wdfr543265ffd%26.jpg 就是minio文件的网络地址。
当然这些是我后来的分析。
时间紧,任务重,先不考虑删除2022年4月24日之前的文件
第一次尝试
最快的方式,一定是在服务器上用linux命令删除
于是我先是测试环境试了试
先在测试服务器上找到minio存放文件的根目录,发现在/lcn目录下有data1,data2,data3,data4四个目录,并且目录下的内容都相同,然后cd /lcn/data1/buket1/ | ls,果然有个images目录。
在生产服务器上也是一样的情况。
我果断在测试环境登录了4个窗口,分别cd 到images目录,rm -rf 20210601/,不一会儿就删除了20210601目录,在minio浏览器客户端看确实是删除了。
可是领导说,不能直接在服务器上删,因为以前有人这样操作过,结果minio服务不能用了。
第一次尝试失败
第二次尝试
写java代码
可是minio的java客户端只支持按完整的对象路径删除
这就需要先遍历对象,再删除对象
一顿操作,写出一个遍历接口和一个批量删除接口
/**
* 遍历minio文件目录
* @param bucketName 桶名称
* @param prefix 限定文件目录前缀
* @return
*/
public Iterable<Result<Item>> iterateObjects(String bucketName, String prefix) {
try {
boolean exists = minIoClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!exists) {
return null;
}
Builder builder = ListObjectsArgs.builder().bucket(bucketName).recursive(true).prefix(prefix);
Iterable<Result<Item>> iterable = minIoClient.listObjects(builder.build());
return iterable;
} catch (Exception e) {
throw new MyMinioException(MyMinioErrorType.GET_FILEPATH_FAIL, bucketName, prefix, e.getMessage());
}
}
/**
* 批量删除minio文件
* @param bucketName 桶名称
* @param filePaths 文件目录
*/
public void removeObjects(String bucketName, List<String> filePaths) {
validateBucketName(bucketName);
List<DeleteObject> list = new ArrayList<>();
for (String filePath : filePaths) {
validateFileLocation(filePath);
list.add(new DeleteObject(filePath));
}
Iterable<Result<DeleteError>> iterable = minIoClient.removeObjects(
RemoveObjectsArgs.builder().bucket(bucketName).objects(list).build()
);
try {
for (Result<DeleteError> result : iterable) {
DeleteError error = result.get();
log.info("minio删除错误->bucketName={},objectName={},message={}", error.bucketName(), error.objectName(), error.message());
}
} catch (Exception e) {
log.error("读取minio删除错误的数据时异常", e);
}
}
部分代码
public void deleteImages() {
log.info("删除minio的buket1/images/文件:开始......");
LocalDate startDate = LocalDate.of(2021, 6, 1);
LocalDate endDate = LocalDate.of(2022, 4, 23);
List<String> list = new ArrayList<>();
for (;;) {
if (startDate.isAfter(endDate)) {
break;
}
String format = startDate.format(DateTimeFormatter.BASIC_ISO_DATE);
list.add(format);
startDate = startDate.plusDays(1L);
}
long count = 0L;
for (String date : list) {
long c = removeImages(date);
count += c;
log.info("images/{}/文件删除完毕,共{},总计{}", date, c, count);
}
log.info("删除minio的buket1/images/文件:完成......");
}
private long removeImages(String time) {
final int BATCH_NUM = 200;
long n = 0L;
String prefix = "images/" + time + "/";
String path = prefix;
try {
log.info("遍历->path->{}", path);
Iterable<Result<Item>> iterable = myFileService.iterateObjects("buket1", path);
if (iterable == null) {
return n;
}
Iterator<Result<Item>> it = iterable.iterator();
List<String> list = new ArrayList<>();
while (it.hasNext()) {
Item item = it.next().get();
if (item.isDeleteMarker() || item.isDir()) {
continue;
}
list.add(item.objectName());
if (list.size() == BATCH_NUM) {
myFileService.removeObjects("buket1", list);
n += BATCH_NUM;
list = new ArrayList<>();
log.info("已删除buket1/{},[{}]个文件", path, n);
}
}
if (!list.isEmpty()) {
myFileService.removeObjects("buket1", list);
n += list.size();
log.info("已删除buket1/{},[{}]个文件", path, n);
}
} catch (Exception e) {
log.error("minio-{}文件删除异常", path, e);
}
return n;
}
结果可想而知,一个对象也没遍历出来,超时了。
第二次尝试失败
第三次尝试
在生产服务器上 cd images/20210601 | ls
等啊等,等了5分钟,终于显示出满满一屏蓝色的目录,还溢出屏幕了
在prefix参数上搞一搞事情吧
private static final String[] PREFIX_STR = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"};
private long removeImages(String time) {
final int BATCH_NUM = 200;
long n = 0L;
String prefix = "images/" + time + "/";
String path = "";
for (int i = 0; i < PREFIX_STR.length; i++) {
try {
path = prefix + PREFIX_STR[i];
log.info("遍历->path->{}", path);
Iterable<Result<Item>> iterable = myFileService.iterateObjects("buket1", path);
if (iterable == null) {
return n;
}
Iterator<Result<Item>> it = iterable.iterator();
List<String> list = new ArrayList<>();
while (it.hasNext()) {
Item item = it.next().get();
if (item.isDeleteMarker() || item.isDir()) {
continue;
}
list.add(item.objectName());
if (list.size() == BATCH_NUM) {
myFileService.removeObjects("buket1", list);
n += BATCH_NUM;
list = new ArrayList<>();
log.info("已删除buket1/{},[{}]个文件", path, n);
}
}
if (!list.isEmpty()) {
myFileService.removeObjects("buket1", list);
n += list.size();
log.info("已删除buket1/{},[{}]个文件", path, n);
}
} catch (Exception e) {
log.error("minio-{}文件删除异常", path, e);
}
}
return n;
}
这次虽然很慢,但好歹一些文件成功删除了,然而还是有很多超时。
第三次尝试算是失败了
第四次尝试
在prefix参数是再加一位
private static final String[] PREFIX_STR = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"};
private long removeImages(String time) {
final int BATCH_NUM = 200;
long n = 0L;
String prefix = "images/" + time + "/";
String path = "";
for (int i = 0; i < PREFIX_STR.length; i++) {
try {
for (int j = 0; j < PREFIX_STR.length; j++) {
path = prefix + PREFIX_STR[i] + PREFIX_STR[j];
log.info("遍历->path->{}", path);
Iterable<Result<Item>> iterable = myFileService.iterateObjects("buket1", path, startAfter);
if (iterable == null) {
return n;
}
Iterator<Result<Item>> it = iterable.iterator();
List<String> list = new ArrayList<>();
while (it.hasNext()) {
Item item = it.next().get();
if (item.isDeleteMarker() || item.isDir()) {
continue;
}
list.add(item.objectName());
if (list.size() == BATCH_NUM) {
myFileService.removeObjects("buket1", list);
n += BATCH_NUM;
list = new ArrayList<>();
log.info("已删除buket1/{},[{}]个文件", path, n);
}
}
if (!list.isEmpty()) {
myFileService.removeObjects("buket1", list);
n += list.size();
log.info("已删除buket1/{},[{}]个文件", path, n);
}
}
} catch (Exception e) {
log.error("minio-{}文件删除异常", path, e);
}
}
return n;
}
这次仍然很慢,好歹大多数没有超时,按这样的速度,没有几个月是删不完的/滑稽
第五次尝试
十个线程同时跑。
结果像第二次一样,都超时了。
第六次尝试
第四次日志显示已经删除20210601的所有文件了,到服务器上看看,哎,怎么还有这么多文件?
经过多番测试,终于找到原因了。
原来,minio遍历对象默认是编码url的,%被编码成了%25,而删除对象不会解码,所以只要对象名称带有%的都没删除掉。
改遍历代码,设置useUrlEncodingType为false
/**
* 遍历minio文件目录
* @param bucketName 桶名称
* @param prefix 限定文件目录前缀
* @return
*/
public Iterable<Result<Item>> iterateObjects(String bucketName, String prefix) {
try {
boolean exists = minIoClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!exists) {
return null;
}
Builder builder = ListObjectsArgs.builder().bucket(bucketName).recursive(true).prefix(prefix).useUrlEncodingType(false);
Iterable<Result<Item>> iterable = minIoClient.listObjects(builder.build());
return iterable;
} catch (Exception e) {
throw new MyMinioException(MyMinioErrorType.GET_FILEPATH_FAIL, bucketName, prefix, e.getMessage());
}
}
/**
* 批量删除minio文件
* @param bucketName 桶名称
* @param filePaths 文件目录
*/
public void removeObjects(String bucketName, List<String> filePaths) {
validateBucketName(bucketName);
List<DeleteObject> list = new ArrayList<>();
for (String filePath : filePaths) {
validateFileLocation(filePath);
list.add(new DeleteObject(filePath));
}
Iterable<Result<DeleteError>> iterable = minIoClient.removeObjects(
RemoveObjectsArgs.builder().bucket(bucketName).objects(list).build()
);
try {
for (Result<DeleteError> result : iterable) {
DeleteError error = result.get();
log.info("minio删除错误->bucketName={},objectName={},message={}", error.bucketName(), error.objectName(), error.message());
}
} catch (Exception e) {
log.error("读取minio删除错误的数据时异常", e);
}
}
这次都可以删除了。但是好慢呀
第七次尝试
从数据库查询并解析html,提取对象路径,直接删除
private static final Pattern SRC_PATTERN = Pattern.compile("(?<=src=\"/bucket1/)images/[\\S\\s]+?(?=\")", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
private static List<String> queryHtmlSpiderSrc(String html) {
List<String> srcList = new ArrayList<>();
if (StringUtils.isBlank(html)) {
return srcList;
}
Matcher matcher = SRC_PATTERN.matcher(html);
matcher.reset();
while (matcher.find()) {
srcList.add(StringUtils.deleteWhitespace(matcher.group()));
}
return srcList.stream().distinct().collect(Collectors.toList());
}
一个小时后,日志显示已经删除了好几天的。
终于完成了。
在服务器上看看,哎,怎么还有很多文件?
在确定从数据库解析的对象真的都删除了后,扒开屎一样的代码看看,呃,估计数据库里记录的对象只占三分之一。
第八次尝试
要是先在服务器上遍历出对象路径存放到文件里,java代码读取文件批量删除就好了。
说干就干,编写shell脚本,现学现用
最终写出了一坨屎
ceshi.sh
#! /bin/bash
# 遍历六月份的minio对象
month='202106'
arr=(0 1 2 3 4 5 6 7 8 9 A B C D E F)
parent_dir='images'
root_dir='/lcn/data1/buket1/images'
dir0=$(ls $root_dir | grep "^${month}")
for i in $dir0
do
# 输出日志到文件
echo $i >> /home/resultpath/ceshi-${month}.log
d1=${root_dir}/${i}
for ele in ${arr[@]}
do
# 输出日志到文件
echo ${i}/${ele} >> /home/resultpath/ceshi-${month}.log
dir1=$(ls $d1 | grep "^$ele")
for j in $dir1
do
d2=${root_dir}/${i}/$j
# ls -A 遍历除.和..以外的所有目录
dir2=$(ls -A $d2)
for p in $dir2
do
# 输出对象路径到文件
echo ${parent_dir}/${i}/${j}/${p} >> /home/resultpath/minio-${month}.txt
done
done
done
done
echo '完成' >> /home/resultpath/ceshi-${month}.log
执行命令在后台运行
nohup sh ceshi.sh >/dev/null 2>&1 &
最后输出到/home/resultpath/ceshi-${month}.txt文件的内容格式为
images/20210601/526F6BCD5661D393CADE4E832523B5F8/wdfr543265ffd%26.jpg
改成这样,两天也删不完啊,怎么也得7天。
后面,我把minio的md5目录改为了取md5的前两位,为以后写正式的定时删除任务做准备吧。
第九次尝试
使用minio客户端命令行,递归删除
在linux服务器上任意目录直接输入mc命令检查是否可用minio客户端命令行
[root@minio-server ~]# mc
NAME:
mc - MinIO Client for cloud storage and filesystems.
USAGE:
mc [FLAGS] COMMAND [COMMAND FLAGS | -h] [ARGUMENTS...]
COMMANDS:
alias set, remove and list aliases in configuration file
ls list buckets and objects
mb make a bucket
rb remove a bucket
cp copy objects
mirror synchronize object(s) to a remote site
cat display object contents
head display first 'n' lines of an object
pipe stream STDIN to an object
share generate URL for temporary access to an object
find search for objects
sql run sql queries on objects
stat show object metadata
mv move objects
tree list buckets and objects in a tree format
du summarize disk usage recursively
retention set retention for object(s)
legalhold manage legal hold for object(s)
diff list differences in object name, size, and date between two buckets
rm remove objects
version manage bucket versioning
ilm manage bucket lifecycle
encrypt manage bucket encryption config
event manage object notifications
watch listen for object notification events
undo undo PUT/DELETE operations
policy manage anonymous access to buckets and objects
tag manage tags for bucket and object(s)
replicate configure server side bucket replication
admin manage MinIO servers
update update mc to latest release
GLOBAL FLAGS:
--autocompletion install auto-completion for your shell
--config-dir value, -C value path to configuration folder (default: "/root/.mc")
--quiet, -q disable progress bar display
--no-color disable color theme
--json enable JSON lines formatted output
--debug enable debug output
--insecure disable SSL certificate verification
--help, -h show help
--version, -v print the version
TIP:
Use 'mc --autocompletion' to enable shell autocompletion
VERSION:
RELEASE.***************
[root@minio-server ~]#
在装有minio服务的服务器上,先找到minio服务,命令如下
mc config host ls
第一个就是我的minio服务,其中用绿色框起来的文字是 minio-server
再查看buket1桶images目录下的子目录列表
# 最后的斜线可以省略
mc ls minio-server/buket1/images/
# 结果
[root@minio-server ~]# mc ls minio-server/buket1/images
[2022-05-09 20:34:42 CST] 0B 20210601/
[2022-05-09 20:34:42 CST] 0B 20210710/
[2022-05-09 20:34:42 CST] 0B 20210711/
[2022-05-09 20:34:42 CST] 0B 20210712/
[2022-05-09 20:34:42 CST] 0B 20210717/
# ............................
最后使用 mc rm 命令删除
# 删除单个文件
mc rm minio-server/buket1/images/20210601/526F6BCD5661D393CADE4E832523B5F8/wdfr543265ffd%26.jpg
# 递归删除目录
mc rm minio-server/buket1/images/20210601 --recursive --force
minio递归删除的原理是先遍历目录下的所有文件,再按每批次1000个,一批一批地删除。
由于我的minio-server/buket1/images/20210601目录下有10万个文字,我等啊等,一个小时过去了也没遍历出来,更别说删除了,真TMD垃圾。ctrl + c
第十次尝试
我发现我还是高估minio了,很多超时。
继续优化代码
按不同日期目录每次轮替删除50条数据。
public void deleteImagesFormFile(List<String> fileNames) {
log.info("开始删除minio-:{}.txt。。。", Objects.toString(fileNames));
if (fileNames == null) {
log.info("fileNames为null");
return;
}
if (fileNames.isEmpty()) {
log.info("fileNames为空");
return;
}
if (fileNames.size() > 4) {
log.info("fileNames的长度超过4个");
return;
}
List<File> fileList = new ArrayList<>();
for (String fileName : fileNames) {
File file = new File("/home/tempfile", "minio-" + fileName + ".txt");
if (!file.exists()) {
log.info("文件不存在->{}", file.getAbsolutePath());
return;
}
if (!file.isFile()) {
log.info("文件不是file类型->{}", file.getAbsolutePath());
return;
}
fileList.add(file);
}
List<LineIterator> iterators = new ArrayList<>();
try {
for (File file : fileList) {
iterators.add(FileUtils.lineIterator(file, "UTF-8"));
}
String fileName = null;
int size = 0;
int n = 0;
List<String> list = new ArrayList<>();
LineIterator iterator = null;
loop:
for (int i = -1;;) {
size = iterators.size();
if (size < 1) {
break;
}
i++;
i = i % size;
iterator = iterators.get(i);
fileName = fileList.get(i).getName();
try {
while (iterator.hasNext()) {
String line = iterator.next();
if (StringUtils.isNotBlank(line)) {
list.add(line);
if (list.size() >= 50) {
log.info("删除minio,50数据->{}", fileName);
myFileService.removeObjects("spider", list);
n += 50;
log.info("删除minio已成功,{}数据->{}", n, fileName);
list = new ArrayList<>();
continue loop;
}
}
}
if (!list.isEmpty()) {
log.info("删除minio,{}数据->{}", list.size(), fileName);
myFileService.removeObjects("spider", list);
n += list.size();
log.info("删除minio已成功,{}数据->{}", n, fileName);
list = new ArrayList<>();
}
log.info("完成删除minio-:{}.txt。。。", fileName);
iterators.remove(i);
LineIterator.closeQuietly(iterator);
fileList.remove(i);
i--;
} catch (Exception e1) {
log.warn("删除minio文件报错->{}, 数据->{}", fileName, list.toString(), e1);
}
}
} catch (Exception e) {
log.error("创建LineIterator异常", e);
} finally {
for (LineIterator lineIterator : iterators) {
LineIterator.closeQuietly(lineIterator);
}
}
log.info("删除minio全部完成-:{}.txt。。。", Objects.toString(fileNames));
}
将shell遍历生成的文件拷贝到/home/tempfile/目录下,传入一组文件名就可以了。
经过测试每隔一段时间,还是会有超时的现象,真不知道minio在干什么。
删除100万个文件,耗时15天,
天哪,删除1000多万个文件,要耗时半年啊
结束
按文章最后的方法删除,不同的是同时开三个线程,每个线程只删除一个日期目录不再轮替目录(事实证明轮替目录删除性能并不好)。还需要运维修改接口、网关、nginx的超时时长,建议改大为5分钟。
有时可能minio一些目录下的文件删除了,但是留了些空目录,在minio客户端页面上删除这些空目录无效。
比如原本minio有对象 buket1/images/20210601/526F6BCD5661D393CADE4E832523B5F8/wdfr543265ffd%26.jpg,
删除后在磁盘上只剩下了空目录 buket1/images/20210601/526F6BCD5661D393CADE4E832523B5F8/。
minio一切皆对象,删除 buket1/images/20210601/526F6BCD5661D393CADE4E832523B5F8 就可以了(末尾千万不能加“/”)
更多推荐
所有评论(0)