Очистка потерянных файлов из GridFS

У меня есть коллекция, ссылающаяся на файлы GridFS, обычно 1-2 файла на запись. Коллекции достаточно большие - около 705 тыс. Записей в родительской коллекции и 790 тыс. Файлов GridFS. Со временем появилось несколько потерянных файлов GridFS - родительские записи были удалены, а ссылочные файлы - нет. Сейчас я пытаюсь очистить потерянные файлы из коллекции GridFS.

Проблема с подходом, подобным предложенному здесь, заключается в том, что объединение записей 700 КБ в один большой список идентификаторов приводит к появлению списка Python объемом около 4 Мб в памяти - передача его в запрос $nin в Mongo для коллекции fs.files занимает буквально навсегда, Выполнение обратного действия (получение списка всех идентификаторов в файле fs.files и запрос родительской коллекции, чтобы узнать, существуют ли они) также занимает вечность.

Кто-нибудь сталкивался с этим и разработал более быстрое решение?

2 ответа

Решение

Во-первых, давайте уделим время тому, чтобы рассмотреть, что же такое GridFS. И для начала давайте прочитаем со страницы руководства, на которую есть ссылки:

GridFS - это спецификация для хранения и извлечения файлов, размер файла которых превышает ограничение BSON в 16 МБ.

Так с этим из пути, и это вполне может быть ваш случай использования. Но урок, который нужно усвоить, заключается в том, что 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, так что оставьте последний идентификатор, найденный в этом наборе, потому что теперь вы собираетесь снова выполнить тот же агрегатный оператор, но по-другому:

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

Так что это работает, потому чтоObjectIdзначения являются монотонными или "постоянно увеличивающимися". Таким образом, все новые записи всегдабольше, чем последние. Затем вы можете снова зациклить эти значения и сделать то же самое удаление, если оно не найдено.

Будет ли это "взять навсегда". Ну да. Вы можете нанятьdb.eval() для этого, но прочитайте документацию. Но в целом, это цена, которую вы платите за использование двух коллекций.

Вернуться к началу. Спецификация GridFS разработана таким образом, потому что она специально хочет обойти ограничение в 16 МБ. Но если это не ваше ограничение, тогда спросите, почему вы используете GridFS в первую очередь.

MongoDB без проблем хранит "двоичные" данные в любом элементе данного документа BSON. Таким образом, вам не нужно использовать GridFS только для хранения файлов. И если бы вы сделали это, то все ваши обновления были бы полностью "атомарными", так как они воздействовали только на один документ в одной коллекции за раз.

Поскольку GridFS преднамеренно разделяет документы по коллекциям, то, если вы используете их, вы живете с болью. Так что используйте его, если вам это нужно, но если нет, просто сохраните BinData как нормальное поле, и эти проблемы уходят.

Но, по крайней мере, у вас есть лучший подход, чем загружать все в память.

Хотел бы добавить свою лепту в это обсуждение. В зависимости от размера разницы, вы можете сначала найти идентификаторы файлов, которые вы должны сначала сохранить, а затем удалить фрагменты, которые не следует хранить. Это может произойти, когда вы управляете огромным количеством временных файлов.

В моем случае у нас достаточно много временных файлов, которые ежедневно сохраняются в GridFS. В настоящее время у нас есть что-то вроде 180 тыс. Временных файлов и несколько не временных. Когда индекс истечения достигает, мы в конечном итоге ок. 400 тысяч сирот.

При поиске этих файлов полезно знать, что 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
}

Внутри я создаю переменные для поиска в диапазоне идентификаторов:

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

и собирать в переменные идентификаторы файлов, что существует в коллекции .files:

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

На данный момент у меня есть около 50 идентификаторов в found переменная. Теперь, чтобы убрать большое количество детей-сирот в коллекции .chunks, Я ищу в цикле 100 идентификаторов, чтобы удалить, пока я ничего не нашел:

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 уникальных идентификаторов). Вероятно, мне нужно создать какой-нибудь умный индекс, чтобы быстрее находить результаты.

улучшение

Упаковал его в "маленькую" задачу, которая создает процесс удаления на сервере Монго и отключается. Это, очевидно, JavaScript, который вы можете отправить в оболочку mongo, например. ежедневно:

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.
    }
});
/* 
 * 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+" ]");        
}
Другие вопросы по тегам