Hace unos días escribí un artículo para GenbetaDev a propósito de la actualización de datos en MongoDB
En ese artículo además de hablar sobre como hacer actualizaciones de datos, comentaba cómo almacena MongoDB los documentos en la versión 2.4. Cuándo tenemos una colección vacía y empezamos a insertar documentos, MongoDB los almacena sin dejar espacio entre ellos. Es decir, que por cada documento almacenado, se reserva el espacio justo que se necesita. Si un documento se actualiza, habrá que moverlo para conseguir el espacio que necesita. Para evitar movimientos constantes -que penalizan el rendimiento- MongoDB calcula dinámicamente un factor de separación de manera que se reserva espacio extra en el documento. Si se vuelve a actualizar, como tiene espacio libre adicional, es posible que no sea necesario moverlo.
Por tanto cuándo no hemos realizado ninguna operación de actualización, el factor de separación (en inglés padding factor), es 1. Es decir que MongoDB no está dejando espacio entre documentos. Si se producen actualizaciones, habrá que mover los documentos, por lo que MongoDB aumentará el factor de separación. Un valor de 1.5, implica que se está añadiendo un 50% más al espacio que necesita un documento. Si el documento ocupa 1KB se reservará un espacio de 1,5KB.
Con la nueva versión 2.6 de MongoDB -ahora en release candidate-, este sistema se va a modificar para pasar a utilizar por defecto usePowerOf2Sizes.
¿Qué es usePowerOf2Sizes?
Este sistema se puede utilizar desde hace varias versiones de MongoDB. A partir de la versión 2.6, será el sistema utilizado por defecto, en lugar del actual cálculo dinámico del padding factor.
La idea es muy sencilla, y se trata de redondear el espacio que ocupa un documento a la potencia de 2 superior más cercana. Si nuestro documento tiene un tamaño de 3 bytes, se reservarán 4 bytes. Si su tamaño es de 90 bytes, se reservarán 128 bytes. Y si tiene un tamaño de 1800 bytes se guardará ocupando 2048 bytes.
Esto tiene una ventaja y un inconveniente. La ventaja es que MongoDB tendrá que mover menos documentos al realizar actualizaciones. Eso significa que el rendimiento mejorará. La desventaja es que, en teoría, el consumo de disco aumentará. Si por cada documento reservamos más espacio del necesario, está claro que necesitaremos más disco.
Las eliminaciones de documentos también influyen. Cuándo se borra un documento, el espacio que ocupa queda libre. Si se va a insertar un documento, y el espacio libre es suficiente, se utiliza para almacenarlo.
Ahora que hemos visto la teoría, vamos a realizar unos test para ver como se comportan ambos sistemas.
Componentes necesarios para los test
Necesitamos dos colecciones para hacer las pruebas
db.createCollection("noPowerOf2Sizes");
db.createCollection("powerOf2Sizes");
db.runCommand( {collMod: "powerOf2Sizes", usePowerOf2Sizes : true });
La primera colección, la creamos de forma normal. Con la segunda establecemos el parámetro usePowerOf2Sizes a true para usar potencias de 2 para el almacenamiento.
También creamos una función JavaScript que genere números aleatorios en un intervalo dado.
function getRandomInt (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
Otro elemento que voy a utilizar, es un script para generar documentos e insertarlos en las dos colecciones.
//Definición de variables
var myArray;
var startDate;
var noPowerOf2Milliseconds =0;
var powerOf2Milliseconds = 0;
//Insertamos 1.000.000 documentos
for(var i=0;i<=1000000;i++){
myArray=new Array();
for (var a=0;a< getRandomInt(1,10);a++)
{
myArray[a] = getRandomInt(1,10) * getRandomInt(1,50);
}
startDate= new Date();
db.noPowerOf2Sizes.insert(
{
_id:i,
foo:myArray
});
noPowerOf2Milliseconds += Math.abs(new Date() - startDate);
startDate= new Date();
db.powerOf2Sizes.insert(
{
_id:i,
foo:myArray
});
powerOf2Milliseconds += Math.abs(new Date() - startDate);
}
print("noPowerOf2 " + noPowerOf2Milliseconds);
print("powerOf2 "+ powerOf2Milliseconds);
print("");
El script crea 1.000.000 de documentos, con un campo _id y un campo foo que contendrá un array de tamaño y contenido aleatorio. El script hace dos inserciones, una en cada colección, y va almacenando los tiempos que tardan esas inserciones. Posiblemente haya alguna forma mejor de medir los tiempos, pero yo creo que para los test que voy a realizar es más que suficiente. Aquí un ejemplo de documento insertado:
{
"_id" : 142184,
"foo" : [
387,
153,
32,
230,
220,
26,
40,
126,
160
]
}
El último script, es el que ejecutaré varias veces para ver como se comportan los dos sistemas de almacenamiento. Es el siguiente:
//Definición de variables
var startDate;
var noPowerOf2Milliseconds =0;
var powerOf2Milliseconds = 0;
var myArray;
//recuperamos el _id más alto que haya sido insertado
var lastElement=db.powerOf2Sizes.find().sort({_id:-1}).limit(1).toArray()[0]._id;
//Realizamos 100.000 actualizaciones aleatorias
for (i=0;i<=100000;i++){
objectToUpdate= getRandomInt(0,lastElement);
myArray = new Array();
for (a=0;a< getRandomInt(1,10); a++){
myArray[a] = getRandomInt(1,10) * getRandomInt(1,50);
}
startDate= new Date();
db.noPowerOf2Sizes.update(
{ _id:objectToUpdate},
{$pushAll:{foo:myArray}}
);
noPowerOf2Milliseconds += Math.abs(new Date() - startDate);
startDate = new Date();
db.powerOf2Sizes.update(
{ _id:objectToUpdate},
{$pushAll:{foo:myArray}}
);
powerOf2Milliseconds += Math.abs(new Date() - startDate);
}
//Realizamos 20.000 eliminaciones aleatorias
for (d=0;d<=20000;d++){
objectToUpdate= getRandomInt(0,lastElement);
startDate = new Date();
db.noPowerOf2Sizes.remove({_id:objectToUpdate});
noPowerOf2Milliseconds += Math.abs(new Date() - startDate);
startDate = new Date();
db.powerOf2Sizes.remove({_id:objectToUpdate});
powerOf2Milliseconds += Math.abs(new Date() - startDate);
}
//Insertamos 20.000 documentos nuevos
for (j=lastElement + 1; j<= (lastElement + 20000); j++){
myArray=new Array();
for (p=0;p< getRandomInt(1,10); p++)
{
myArray[p] = getRandomInt(1,10) * getRandomInt(1,50);
}
startDate= new Date();
db.noPowerOf2Sizes.insert(
{
_id:j,
foo:myArray
});
noPowerOf2Milliseconds += Math.abs(new Date() - startDate);
startDate= new Date();
db.powerOf2Sizes.insert(
{
_id:j,
foo:myArray
});
powerOf2Milliseconds += Math.abs(new Date() - startDate);
}
//Mostramos los tiempos
print("noPowerOf2 " + noPowerOf2Milliseconds);
print("powerOf2 "+ powerOf2Milliseconds);
print("");
El funcionamiento es sencillo. Primero realizamos 100.000 actualizaciones filtrando por el campo _id. Luego borramos 20.000 documentos y finalmente insertamos otros 20.000. Tanto con las actualizaciones, como con las eliminaciones, es posible que no se realice ninguna operación. Esto es debido a que al ejecutar varias veces el script, es posible que el documento que se trata de borrar o actualizar aleatoriamente, ya no exista.
En el script he usado $pushAll por simplificar las consultas, pero en la versón 2.4 ya ha sido marcado como deprecated. Para cosas más serias es mejor utilizar $push junto con el operador $each.
En todas las operaciones, se realiza la misma acción sobre las dos colecciones.
Tomando medidas
Lo primero que hacemos es ejecutar el script de inserción. Una vez ha finalizado el proceso, se lanza el comando stats en cada colección. Por ejemplo, esto es lo que hay en las colecciones noPowerOf2Sizes y powerOf2Sizes después de la inserción de 1.000.001 documentos.
db.noPowerOf2Sizes.stats();
{
"ns" : "test.noPowerOf2Sizes",
"count" : 1000001,
"size" : 71490776,
"avgObjSize" : 71.4907045092955,
"storageSize" : 123936768,
"numExtents" : 11,
"nindexes" : 1,
"lastExtentSize" : 37625856,
"paddingFactor" : 1,
"systemFlags" : 1,
"userFlags" : 0,
"totalIndexSize" : 27921040,
"indexSizes" : {
"_id_" : 27921040
},
"ok" : 1
}
db.powerOf2Sizes.stats();
{
"ns" : "test.powerOf2Sizes",
"count" : 1000001,
"size" : 107887632,
"avgObjSize" : 107.8875241124759,
"storageSize" : 123936768,
"numExtents" : 11,
"nindexes" : 1,
"lastExtentSize" : 37625856,
"paddingFactor" : 1,
"systemFlags" : 1,
"userFlags" : 1,
"totalIndexSize" : 27921040,
"indexSizes" : {
"_id_" : 27921040
},
"ok" : 1 }
Para las mediciones, además de los tiempos que devuelven los scripts, nos vamos a fijar en el parámetro size que nos muestra el comando stats. Este parámetro indica el tamaño de los documentos almacenados (que no es lo mismo que el espacio que ocupan los datos en disco).
Una vez insertados los datos, realizamos varias pasadas del segundo script. Con cada pasada obtenemos el tiempo que tarda en realizarse la operación para cada script, y con stats el tamaño que ocupan los datos.
Resultados
Los datos que yo he obtenido son los siguientes:
ms noPowerOf2 | ms PowerOf2 | Espacio noPowerof2 | Espacio PowerOf2 | |
---|---|---|---|---|
1 | 20970 | 16797 | 87763784 | 113968192 |
2 | 24109 | 19084 | 102019032 | 119836736 |
3 | 20690 | 18000 | 114495248 | 125442720 |
4 | 17660 | 20309 | 125581096 | 130835712 |
5 | 18284 | 18519 | 135444200 | 136044416 |
6 | 23637 | 30972 | 144322368 | 141061744 |
7 | 24374 | 23672 | 152318472 | 145963744 |
8 | 16919 | 17293 | 159640784 | 150733920 |
9 | 22379 | 18692 | 166309496 | 155405968 |
10 | 17199 | 15448 | 172489584 | 159979968 |
11 | 15915 | 25677 | 178227256 | 164397712 |
12 | 18456 | 17995 | 183622696 | 168715264 |
13 | 21171 | 19542 | 188779584 | 172981552 |
14 | 22439 | 33075 | 193657648 | 177145888 |
15 | 21426 | 30141 | 198368448 | 181273584 |
16 | 24909 | 26866 | 202882568 | 185336912 |
17 | 24919 | 22370 | 207172096 | 189340096 |
18 | 28614 | 20103 | 211301216 | 193269904 |
19 | 24280 | 33770 | 215432120 | 197177120 |
20 | 14813 | 17014 | 219394088 | 200983296 |
21 | 23832 | 17642 | 223309000 | 204765904 |
Para verlo más claro, unos gráficos.
Conclusiones
Lo primero que podemos ver es que con usePowerOf2Sizes se requiere más espacio de disco al inicio. Sin embargo, el consumo de disco con este sistema tiene un crecimiento más sostenido y predecieble. Por eso, llega un momento en el que los documentos ocupan menos espacio que el modelo de reserva de espacio tradicional. El modelo tradicional, que utiliza el padding factor, crece con saltos pronunciados. Es algo curioso, porque a priori podríamos pensar que el usePowerOf2Sizes utilizaría mayor espacio en disco. El problema yo creo que está en la fragmentación. El método que usa el factor de separación mueve los documentos, pero deja más huecos libres, dejando el espacio disponible fragmentado. Con useOfPowerOf2Sizes el espacio que deja un documento cuándo se mueve, es más fácil de identificar, y por tanto el hueco se puede reutilizar de manera más sencilla, reduciendo la fragmentación.
En cuánto a la velocidad de ejecución de las operaciones, podemos decir que utilizar usePowerOf2Sizes es más o menos igual que el sistema tradicional. Dependiendo de las operaciones realizadas, y de los documentos que haya que mover, el rendimiento variará bastante. Si puedo decir, que haciendo pruebas con menos documentos, el uso de usePowerfOfSizes mejora un poco el rendimiento.
Con este sencillo test, hemos podido comprobar por qué usePowerOf2Sizes será el modo de reserva de espacio que la nueva versión 2.6 de MongoDB utilizará por defecto. El espacio requerido por la base de datos será más previsible, y estable que con el método tradicional.
Eso sí, hay que tener en cuenta que el método actual de padding factor no se elimina. Simplemente pasará a ser opcional.
Recuerda que puedes ver el índice del tutorial y acceder a todos los artículos de la serie desde aquí.
¿Quiéres que te avisemos cuando se publiquen nuevas entradas en el blog?
Suscríbete por correo electrónico o por RSS