清理GridFS中的孤立文件

3 投票
3 回答
3309 浏览
提问于 2025-04-17 23:23

我有一个集合,里面存储着指向GridFS文件的引用,通常每条记录会有1到2个文件。这个集合的数据量还挺大的,父集合大约有70.5万条记录,而GridFS文件有79万多个。随着时间的推移,出现了一些孤立的GridFS文件——也就是说,父记录被删除了,但引用的文件却没有被删除。现在我想把这些孤立的文件从GridFS集合中清理掉。

像这里提到的这种方法存在一个问题,就是把70万条记录合并成一个大的ID列表,会导致Python在内存中占用大约4MB的空间——把这个列表传给MongoDB的$nin查询,简直慢得让人抓狂。反过来,先获取fs.files中的所有ID,然后查询父集合看它们是否存在,这个过程也非常耗时。

有没有人遇到过这个问题,并找到更快的解决方案呢?

3 个回答

0

编辑: 使用distinct这个功能有一个16MB的限制,所以如果你有很多不同的数据块,这个方法可能就不太管用了。在这种情况下,你可以把distinct操作限制在一部分UUID上。

/* 
 * This function will count orphaned chunks grouping them by file_id.
 * This is faster but uses more memory.
 */
function countOrphanedFilesWithDistinct(){
    var start = new Date().getTime();
    var orphanedFiles = [];
    db.documents.chunks.distinct("files_id").forEach(function(id){
        var count = db.documents.files.count({ "_id" : id });
        if(count===0){
            orphanedFiles.push(id);
        }
    });
    var stop = new Date().getTime();
    var time = stop-start;
    print("Found [ "+orphanedFiles.length+" ] orphaned files in: [ "+time+"ms ]");
}

/*
 * This function will delete any orphaned document cunks.
 * This is faster but uses more memory.
 */
function deleteOrphanedFilesWithDistinctOneBulkOp(){
    print("Building bulk delete operation");
    var bulkChunksOp = db.documents.chunks.initializeUnorderedBulkOp();
    db.documents.chunks.distinct("files_id").forEach(function(id){
        var count = db.documents.files.count({ "_id" : id });
        if(count===0){
            bulkChunksOp.find({ "files_id" : id }).remove();
        }
    });
    print("Executing bulk delete...");
    var result = bulkChunksOp.execute();
    print("Num Removed: [ "+result.nRemoved+" ]");        
}
0

我想在这个讨论中补充一些我的看法。根据文件之间的差异大小,首先找出你需要保留的文件身份可能是个不错的主意,然后再删除那些不需要保留的部分。这种情况通常发生在你管理大量临时文件的时候。

在我的案例中,我们每天都会保存相当多的临时文件到GridFS。目前大约有18万个临时文件,还有一些不是临时的。当过期索引触发时,我们最终会有大约40万个孤儿文件。

在寻找这些文件时,有一个有用的知识点是,ObjectID是基于时间戳的。因此,你可以通过在日期之间缩小搜索范围,来查找_idfiles_id

我开始查找文件时,会用一个循环来遍历日期,像这样:

var nowDate = new Date();
nowDate.setDate(nowDate.getDate()-1);

var startDate = new Date(nowDate);
startDate.setMonth(startDate.getMonth()-1) // -1 month from now

var endDate = new Date(startDate);
endDate.setDate(startDate.getDate()+1); // -1 month +1 day from now

while(endDate.getTime() <= nowDate.getTime()) {
    // interior further in this answer
}

在循环内部,我会创建一些变量来搜索ID的范围:

var idGTE = new ObjectID(startDate.getTime()/1000);
var idLT = new ObjectID(endDate.getTime()/1000);

并将存在于.files集合中的文件ID收集到一个变量中:

var found = db.getCollection("collection.files").find({
    _id: {
        $gte: idGTE,
        $lt: idLT
    }
}).map(function(o) { return o._id; });

目前我在found变量中大约有50个ID。接下来,为了删除.chunks集合中大量的孤儿文件,我会循环搜索100个ID进行删除,只要我没有找到任何东西:

var removed = 0;
while (true) {

    // note that you have to search in a IDs range, to not delete all your files ;)
    var idToRemove = db.getCollection("collection.chunks").find({
        files_id: {
            $gte: idGTE, // important!
            $lt: idLT,   // important!
            $nin: found, // `NOT IN` var found
        },
        n: 0 // unique ids. Choosen this against aggregate for speed
    }).limit(100).map(function(o) { return o.files_id; });

    if (idToRemove.length > 0) {

        var result = db.getCollection("collection.chunks").remove({
            files_id: {
                $gte: idGTE, // could be commented
                $lt: idLT,   // could be commented
                $in: idToRemove // `IN` var idToRemove
            }
        });

        removed += result.nRemoved;

    } else {
        break;
    }
}

然后再增加日期,以便更接近当前时间:

startDate.setDate(startDate.getDate()+1);
endDate.setDate(endDate.getDate()+1);

目前我无法解决的一个问题是,删除操作花费的时间比较长。根据files_id查找和删除块的过程大约需要3到5秒,针对大约200个块(100个唯一ID)。我可能需要创建一些智能索引,以加快查找速度。

改进

我把这个过程打包成一个“小”任务,就是在Mongo服务器上创建删除过程并断开连接。这显然是JavaScript,你可以每天发送到Mongo shell,例如:

var startDate = new Date();
startDate.setDate(startDate.getDate()-3) // from -3 days

var endDate = new Date();
endDate.setDate(endDate.getDate()-1); // until yesterday

var idGTE = new ObjectID(startDate.getTime()/1000);
var idLT = new ObjectID(endDate.getTime()/1000);

var found = db.getCollection("collection.files").find({
    _id: {
        $gte: idGTE,
        $lt: idLT
    }
}).map(function(o) { return o._id; });

db.getCollection("collection.chunks").deleteMany({
    files_id: {
        $gte: idGTE,
        $lt: idLT, 
        $nin: found,
    }
}, {
    writeConcern: {
        w: 0 // "fire and forget", allows you to close console.
    }
});
5

首先,我们来看看什么是GridFS。作为一个开端,我们可以从手册中找到相关的描述:

GridFS是一种用于存储和检索超过16MB大小限制的文件的规范。

了解了这一点,可能这正是你需要的情况。但这里要学习的教训是,GridFS并不是存储文件的“首选”方法。

在你的案例(以及其他人的案例)中,发生的事情是因为“驱动层”的规范,MongoDB本身并没有做什么“魔法”。你的“文件”被“拆分”成了两个集合:一个集合是内容的主要引用,另一个集合是数据的“块”。

你的问题(以及其他人)是,由于“主要”引用被删除,你留下了“块”。那么,面对大量数据,如何处理这些孤儿数据呢?

你当前的做法是“循环比较”,而由于MongoDB不支持连接,所以确实没有其他答案。但有一些方法可以帮助你。

所以,不要直接运行一个巨大的$nin,可以尝试做一些不同的事情来拆分这个过程。考虑从反向顺序开始,例如:

db.fs.chunks.aggregate([
    { "$group": { "_id": "$files_id" } },
    { "$limit": 5000 }
])

你在这里做的是获取不同的“files_id”值(即对fs.files的引用),从所有条目中获取5000个条目作为开始。然后当然你又回到了循环,检查fs.files中是否有匹配的_id。如果没有找到,就删除与“files_id”匹配的“chunks”文档。

但这只是5000个,所以保留在这一组中找到的最后一个id,因为现在你要再次运行相同的聚合语句,但方式不同:

db.fs.chunks.aggregate([
    { "$match": { "files_id": { "$gte": last_id } } },
    { "$group": { "_id": "$files_id" } },
    { "$limit": 5000 }
])

所以这样做是有效的,因为ObjectId值是单调递增的。因此,所有条目总是大于最后一个。然后你可以再次循环这些值,并在未找到的情况下进行相同的删除。

这会“花很长时间”吗?嗯,是的。你可能会使用db.eval()来处理这个,但阅读文档。总的来说,这就是你使用两个集合所付出的代价。

回到开始。GridFS的规范是这样设计的,因为它特别想要绕过16MB的限制。但如果这不是你的限制,那就要问为什么你一开始要使用GridFS

MongoDB在任何给定的BSON文档的元素中没有问题存储“二进制”数据。所以你不需要使用GridFS来存储文件。如果你这样做了,那么所有的更新将是完全“原子”的,因为它们只在一个集合中的一个文档上操作。

由于GridFS故意将文档拆分到多个集合中,所以如果你使用它,就得忍受这种痛苦。因此,如果你需要它,就用它;但如果你不需要,那么就把BinData作为普通字段存储,这样这些问题就会消失。

但至少你有一个比把所有东西加载到内存中更好的方法。

撰写回答