我有一个引用GridFS文件的集合,通常每个记录有1-2个文件。集合相当大 - 父集合中约有705k记录,以及790k GridFS文件。随着时间的推移,已经有一些孤立的GridFS文件 - 父记录被删除,但引用的文件没有。我现在正在尝试从GridFS集合中清除孤立的文件。
像here这样的方法的问题在于将700k记录组合成一个大的id列表会导致内存中大约4mb的Python列表 - 在fs.files集合中将其传递到Mongo中的$ nin查询。反过来(获取fs.files中的所有id列表并查询父集合以查看它们是否存在)也需要永远。
有没有人反对这个并开发出更快的解决方案?
首先,让我们花时间考虑一下GridFS究竟是什么。作为入门者,让我们从引用的手册页中读取:
GridFS是用于存储和检索超过16MB的BSON文档size limit的文件的规范。
因此,除此之外,这可能是您的用例。但是,这里要学到的教训是,GridFS不会自动成为存储文件的“首选”方法。
在你的情况下(和其他人)发生的事情是因为这是“驱动程序级别”规范(并且MongoDB本身没有任何魔力),你的“文件”已经在两个集合中“分裂”。一个集合用于内容的主要引用,另一个用于数据的“块”。
你的问题(以及其他问题)是,你已经设法留下“块”,因为“主”参考已被删除。所以有大量的,如何摆脱孤儿。
您当前的读数显示“循环和比较”,并且由于MongoDB不进行连接,因此实际上没有其他答案。但是有些事情可以提供帮助。
所以,而不是运行一个巨大的$nin
,尝试做一些不同的事情来打破这一点。考虑使用相反的顺序,例如:
db.fs.chunks.aggregate([
{ "$group": { "_id": "$files_id" } },
{ "$limit": 5000 }
])
所以你正在做的是从所有条目中获取不同的“files_id”值(作为对fs.files
的引用),为5000条条目开始。当然,你回到循环,检查fs.files
匹配_id
。如果找不到某些内容,则从“块”中删除与“files_id”匹配的文档。
但那只是5000,所以保留在该集合中找到的最后一个id,因为现在你将再次运行相同的聚合语句,但不同:
db.fs.chunks.aggregate([
{ "$match": { "files_id": { "$gte": last_id } } },
{ "$group": { "_id": "$files_id" } },
{ "$limit": 5000 }
])
所以这是有效的,因为ObjectId
值是monotonic或“不断增加”。因此,所有新条目总是大于最后一个。然后你可以再次循环这些值并执行相同的删除操作。
这将“永远”。嗯,是。您可以使用db.eval()
,但请阅读文档。但总的来说,这是您使用两个系列所付出的代价。
回到开始。 GridFS规范是这样设计的,因为它特别想要解决16MB的限制。但如果这不是你的限制,那就问你为什么首先使用GridFS。
MongoDB在给定BSON文档的任何元素中存储“二进制”数据没有问题。因此,您不需要仅使用GridFS来存储文件。如果你这样做了,那么你所有的更新都将完全是“原子的”,因为它们一次只对一个集合中的一个文档起作用。
由于GridFS故意将文档分割成集合,如果你使用它,那么你就会忍受痛苦。因此,如果您需要它,请使用它,但如果您不需要,那么只需将BinData
存储为普通字段,这些问题就会消失。
但至少你有一个更好的方法,而不是将所有内容加载到内存中。
我想补充一下这个讨论。根据差异的大小,您可能会发现首先找到文件的身份是合理的,您必须先保留,而不是删除不应保留的块。当您管理大量临时文件时可能会发生这种情况。
就我而言,我们每天都有相当多的临时文件保存到GridFS。我们目前有一些像180k临时文件和一些非临时文件。当到期指数达到时,我们最终得到约。 400k孤儿。
尝试查找这些文件时要知道的有用的事情是ObjectID基于时间戳。因此,您可以缩小日期之间的搜索范围,但将范围包含在_id
或files_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
查找和删除块需要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.
}
});
编辑:使用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+" ]");
}