一次恶心的删除minio文件之旅

2年前 (2022) 程序员胖胖胖虎阿
366 0 0

刚入职一家公司,需求下来了

需求

由于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服务,其中用绿色框起来的文字是 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多万个文件,要耗时半年啊

版权声明:程序员胖胖胖虎阿 发表于 2022年10月29日 下午6:32。
转载请注明:一次恶心的删除minio文件之旅 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...