Limpieza de archivos huérfanos de GridFS

Tengo una colección que hace referencia a archivos GridFS, generalmente de 1 a 2 archivos por registro. Las colecciones son razonablemente grandes: aproximadamente 705k registros en la colección principal y 790k archivos GridFS. Con el tiempo, se han convertido en una serie de archivos GridFS huérfanos: se eliminaron los registros primarios, pero no los archivos a los que se hace referencia. Ahora estoy intentando limpiar los archivos huérfanos de la colección GridFS.

El problema con un enfoque como el que se sugiere aquí es que la combinación de los registros de 700 mil en una sola lista grande de identidades da como resultado una lista de Python que tiene aproximadamente 4 mb en la memoria. . Hacer lo contrario (obtener una lista de todos los identificadores en fs.files y consultar la colección principal para ver si existen) también lleva una eternidad.

¿Alguien se ha enfrentado a esto y ha desarrollado una solución más rápida?

En primer lugar, tomemos el tiempo para considerar qué es realmente GridFS . Y como iniciador, leamos la página de manual a la que se hace referencia:

GridFS es una especificación para almacenar y recuperar archivos que exceden el límite de tamaño de documento BSON de 16 MB.

Así que con eso fuera del camino, y ese puede ser su caso de uso. Pero la lección que hay que aprender aquí es que GridFS no es automáticamente el método “ir a” para almacenar archivos.

Lo que sucedió aquí en su caso (y en otros) se debe a la especificación de “nivel de controlador” que es (y MongoDB no hace magia aquí), sus “archivos” se han “dividido” en dos colecciones. Una recostackción para la referencia principal al contenido y la otra para los “fragmentos” de datos.

Su problema (y otros) es que ha logrado dejar atrás los “trozos” ahora que se ha eliminado la referencia “principal”. Así que con un gran número, cómo deshacerse de los huérfanos.

Su lectura actual dice “bucle y compare”, y dado que MongoDB no hace uniones , entonces realmente no hay otra respuesta. Pero hay algunas cosas que pueden ayudar.

Así que en lugar de gastar un gran $nin , intente hacer algunas cosas diferentes para romper esto. Considere trabajar en el orden inverso, por ejemplo:

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

Entonces, lo que está haciendo allí es obtener los distintos valores de “files_id” (que son las referencias a fs.files ), de todas las entradas, para comenzar con 5000 de sus entradas. Luego, por supuesto, vuelve al bucle, revisando fs.files para fs.files si hay un _id coincidente. Si no se encuentra algo, elimine los documentos que coincidan con “files_id” de sus “fragmentos”.

Pero eso fue solo 5000, así que mantén el último ID encontrado en ese conjunto, porque ahora vas a ejecutar la misma instrucción agregada de nuevo, pero de manera diferente:

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

Así que esto funciona porque los valores de ObjectId son monotónicos o “siempre en aumento”. Así que todas las nuevas entradas son siempre mayores que las últimas. Luego puede volver a hacer un bucle con esos valores y hacer las mismas eliminaciones donde no se encuentran.

Será esto “tomar para siempre”. Pues si Podría emplear db.eval() para esto, pero lea la documentación. Pero en general, este es el precio que pagas por usar dos colecciones.

De vuelta al principio. La especificación GridFS está diseñada de esta manera porque específicamente quiere trabajar alrededor de la limitación de 16MB. Pero si esa no es su limitación, entonces pregunte por qué está utilizando GridFS en primer lugar.

MongoDB no tiene problemas para almacenar datos “binarios” dentro de cualquier elemento de un documento BSON determinado. Por lo tanto, no es necesario utilizar GridFS solo para almacenar archivos. Y si lo hubiera hecho, entonces todas sus actualizaciones serían completamente “atómicas”, ya que solo actúan sobre un documento en una colección a la vez.

Dado que GridFS divide deliberadamente los documentos en colecciones, entonces si lo usas, entonces vives con el dolor. Por lo tanto, BinData si lo necesita , pero si no lo hace, simplemente almacene BinData como un campo normal, y estos problemas desaparecerán.

Pero al menos tiene un mejor enfoque que cargar que cargar todo en la memoria.

Me gustaría añadir mi parte a esta discusión. Dependiendo del tamaño de la diferencia, puede que le resulte razonable primero encontrar las identidades de los archivos, que debe mantener primero, y que eliminar los trozos, que no deben conservarse. Puede suceder cuando está administrando grandes cantidades de archivos temporales.

En mi caso, tenemos una gran cantidad de archivos temporales que se guardan en GridFS a diario. Actualmente tenemos algo así como archivos temporales de 180k y algunos no temporales. Cuando llega el índice de vencimiento, terminamos con aprox. 400k huérfanos.

Lo que es útil saber al intentar encontrar esos archivos es que ObjectID se basa en la marca de tiempo. Como tal, puede limitar sus búsquedas entre fechas, pero el rango de _id en _id o files_id .

Para comenzar a buscar archivos, comienzo con un bucle en fechas como esta:

 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 } 

Dentro estoy creando variables para buscar en el rango de IDs:

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

y la recostackción de ID de variable de archivos, que existe en la colección .files :

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

Por ahora tengo aproximadamente 50 IDs en la variable found . Ahora, para eliminar una gran cantidad de huérfanos en la colección de .chunks , estoy buscando en bucle 100 ID para eliminar todo el tiempo, ya que no encontré nada:

 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; } } 

y luego incrementar las fechas para acercarse a las actuales:

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

Una cosa que no puedo resolver por ahora es que la operación de eliminación lleva bastante tiempo. La búsqueda y eliminación de fragmentos en función de files_id requiere de 3 a 5 segundos por cada 200 fragmentos (100 identificaciones únicas). Probablemente tengo que crear algún índice inteligente para hacer que los hallazgos sean más rápidos.

Mejora

Lo empacé en una tarea "pequeña", que está creando un proceso de eliminación en el servidor Mongo y se desconecta. Obviamente es un JavaScript, puedes enviarlo a mongo shell en, por ejemplo. base diaria

 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. } }); 

EDITAR: El uso de distinct tiene una limitación de 16MB, por lo que es posible que esto no funcione si tiene muchos fragmentos diferentes. En ese caso, puede limitar la operación distinta a un subconjunto de 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+" ]"); }