从GridFS中清除孤立的文件

时间:2014-03-22 06:17:43

标签: python mongodb mongodb-query gridfs

我有一个引用GridFS文件的集合,通常每个记录有1-2个文件。集合相当大 - 父集合中约有705k记录,以及790k GridFS文件。随着时间的推移,已经出现了许多孤立的GridFS文件 - 父记录被删除,但引用的文件不是。我现在正试图从GridFS集合中清除孤立的文件。

像建议here这样的方法的问题是将700k记录组合成一个大的id列表会导致Python列表在内存中大约4mb - 将其传递到$ nin查询在fs.files集合的Mongo中,字面意义永远。反过来(获取fs.files中的所有id列表并查询父集合以查看它们是否存在)也需要永远。

是否有人反对这一点并制定了更快的解决方案?

3 个答案:

答案 0 :(得分:4)

首先,让我们花时间考虑GridFS 实际是什么。作为入门者,我们可以阅读引用的手册页:

  

GridFS是用于存储和检索超过16MB的BSON文档size limit的文件的规范。

这样就可以了,这可能就是你的用例。但是,这里要学到的教训是GridFS不是自动" go-to"存储文件的方法。

在你的案例(和其他人)中发生的事情是因为"驱动程序级别" 规范这是(并且MongoDB本身在这里没有魔法),你的"文件"已经"拆分"跨越两个集合。一个集合用于内容的主要参考,另一个集合用于"块"数据。

你的问题(和其他人),是你已经设法留下了" chunk"现在,"主要"参考已被删除。所以有大量的,如何摆脱孤儿。

您当前的阅读说"循环并比较",并且由于MongoDB 不进行连接,那么实际上没有其他答案。但是有些事情可以提供帮助。

因此,不要运行巨大的$nin,而是尝试做一些不同的事情来解决这个问题。考虑使用相反的顺序,例如:

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

所以你正在做的是获得不同" files_id"对于5000个条目的开头,所有条目中的值(作为对fs.files的引用)。然后,您当然要回到循环中,检查fs.files是否有匹配的_id。如果找不到某些内容,则删除匹配" files_id"来自你的" chunks"。

但那只有5000,所以保持在该集合中找到的最后 ID,因为现在您将再次运行相同的聚合语句,但不同:< / p>

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

所以这可行,因为ObjectId值为monotonic或&#34;不断增加&#34;。因此,所有条目始终大于最后一个条目。然后你可以再次循环这些值并执行相同的删除操作。

这会&#34;永远消失&#34;。那么。您可以 使用db.eval(),但阅读文档。但总的来说,这是您使用两个集合支付的价格。

回到开始。 GridFS规范是设计这种方式,因为它特别希望解决16MB限制。但如果是您的限制,那么首先要问为什么您正在使用GridFS

MongoDB 没问题存储&#34;二进制&#34;给定BSON文档的任何元素内的数据。因此不需要仅使用GridFS来存储文件。如果您已经这样做了,那么所有您的更新将完全&#34; atomic&#34;,因为它们只对一个一个文档起作用一次收集。

由于GridFS 故意在集合中拆分文档,如果您使用它,那么您就会感受到痛苦。因此,如果您需要,请使用它,但如果您,则只需将BinData存储为普通字段,这些问题就会消失。

但至少你有一个更好的方法,而不是将所有内容加载到内存中。

答案 1 :(得分:0)

想在本次讨论中加入我的观点。根据差异的大小,您可能会发现首先找到文件的身份是合理的,您必须先保留,而不是删除不应保留的块。当您管理大量临时文件时可能会发生这种情况。

在我的情况下,我们每天都有相当多的临时文件保存到GridFS。我们目前有一些像180k临时文件和一些非临时文件。当到期指数达到时,我们最终得到约。 400k孤儿。

在尝试查找这些文件时要知道的是,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);

并收集到文件的变量ID,它存在于集合.files中:

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的块每个~200块(100个唯一ID)需要3-5个。可能我必须创建一些智能索引以使查找更快。

改进

将它打包成&#34;小&#34;任务,即在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.
    }
});

答案 2 :(得分:0)

编辑:使用distinct的限制为16MB,因此如果您有很多不同的块,则可能无法使用。在这种情况下,您可以将唯一操作限制为一部分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+" ]");        
}