MongoDB desde Cero: Introducción e Instalación Las bases de datos relacionales están pasando de moda, los desarrolladores optan cada vez más por opciones novedosas de NoSQL debido a sus altos niveles de rendimiento y fácil escalabilidad. Hace unas semanas hablamos de las bondades deRedis; sin embargo algunos andan temerosos por tener poco tiempo y prefieren una solución con un poco más de reputación, es por esto que esta semana hablaremos de las base de datos NoSQL más utilizada, MongoDB.
¿Qué es MongoDB? Es una base de datos NoSQL de código abierto, este tipo de soluciones se basan en el principio de almacenar los datos en una estructura tipo llave-valor; MongoDB por su lado se enfoca específicamente en que los valores de estas llaves (llamadas colecciones) son estructuras tipo JSON (llamados documentos), es decir objetos Javascript, lenguaje sobre el cual se basa esta solución de base de datos. Esto facilitará su manipulación a muchos que ya conozcan el lenguaje. MongoDB posee varias estrategias de manejo de datos que la han posicionado donde se encuentra hoy en día, tales como sus procesos de división de datos en distintos equipos físicos o también conocido como clusterización , también el caso similar de documentos muy grandes que superen el limite estipulado de 16MB se aplica una estrategia llamada GridFS que automáticamente divide el documento en pedazos y los almacena por separado, al recuperar el documento el driver se encarga de armar automáticamente el documento nuevamente. La estructura de almacenamiento es tan flexible que uno de los hechos importantes que se comparten al introducir esta base de datos es que:
Distintos documentos en la misma colección no deben tener obligatoriamente los mismos campos o estructura. Inclusive documentos con campos en común no tienen necesariamente que tener el mismo tipo de dato.
¿Cómo lo instalo? Bien, ya estamos ansiosos y desesperados, procedamos con la instalación.
Mac OS X Si haz leído varios de nuestros Cómo Lo Hago, sabrás que el método por excelencia para instalar en Mac OS X es…Hombrew 1 $ brew install mongodb
Para gestionar el servicio de MongoDB basta con ejecutar: 1 $ brew services [ start | stop | restart ] mongodb
Windows Dirigete a la página de oficial de MongoDB y descarga el comprimido según la arquitectura de tu sistema operativo. A partir de la versión 2.2, MongoDB no es compatible con Windows XP.
Si tienes Windows Server 2008 o 7, debes instalar esta actualización para evitar un problema conocido con archivos mapeados en memoria. Luego lo descomprimiremos y, según las recomendaciones, creamos un directorio mongodb en el directorio raíz C: donde colocaremos el contenido del comprimido, luego crearemos en C: un directorio data y dentro de este un directorio db, aquí será donde MongoDB almacenará la información de las bases de datos. Para hacer que MongoDB funcione como un servicio primero crea el directorio log dentro de C:\mongodb\ y luego ejecutaremos el siguiente comando para crear el archivo de configuración asociado: 1 $ echo logpath=C:\mongodb\log\mongo.log > C:\mongodb\mongod.cfg
Luego instalemos el servicio:
1 $ C:\mongodb\bin\mongod.exe --config C:\mongodb\mongod.cfg --install
Ahora para gestionar el servicio de MongoDB basta con ejecutar: 1 $ net [ start | stop ] MongoDB
¿Cómo lo configuro? Buscaremos y editaremos el archivo de configuración.
Mac OS X 1 /usr/local/etc/mongod.conf
Sistemas Debian, Fedora, CentOS y RedHat 1 /etc/mongodb.conf
Windows 1 C:\mongodb\mongod.cfg
Hablemos de algunas de las variables de mayor uso en este archivo. especificación del puerto donde escucha la base de datos. permite delimitar especificamente qué IPs pueden interactuar con la base de datos. maxConns cantidad máxima de conexiones que serán aceptados, el valor por defecto depende de la cantidad de descriptores de archivos que maneja el sistema operativo. objcheck habilitado por defecto, obliga a mongod a verificar cada petición para asegurar que la estructura de los documentos que insertan los clientes sea siempre válida. Cabe destacar que para documentos complejos esta opción puede afectar un poco el rendimiento. fork inhabilitado por defecto, permite ejecutar mongod como un daemon. auth inhabilitado por defecto, permite limitar el acceso remoto a la base de datos al implementar un mecanismo de autenticación. dbpath especifica el directorio donde la instancia de base de datos almacena toda su información.
port
bind_ip
inhabilitado por defecto, ofrece la opción de que la información de cada base de datos presente en la instancia se almacene en carpetas separadas. journal al habilitarse permite que las operaciones realizadas sobre la data sean almacenadas en una bitácora para en caso de ocurrir una falla, el sistema sea capaz de reconstruir la información que haya podido perderse. smallfiles inhabilitado por defecto, ofrece la opción de que los archivos creados sean más pequeños y, por ende, más fáciles de entender, procesar directoryperdb
y monitorear en variaselocasiones. syncdelay especifica lapso en segundos
que tardará la instancia en pasar
la información en la bitácora a persistencia. También se ofrecen varias opciones para el uso de SSL, configuración de replicación y clusterización lo cual tocaremos a medida que avance el curso.
Entrar a la consola Bien ahora ya estamos listos para entrar a la base de datos y comenzar a jugar con ella. Para ello luego de tener el servicio de MongoDB corriendo, ejecutaremos el comando mongo y esto nos llevará a la consola interna de la instancia.
Para ir a la consola de MongoDB en sistemas Windows, si seguiste las instrucciones en el curso anterior, luego de iniciar el servicio, ejecuta el archivoC:\mongodb\bin\mongo.exe
Conclusión Esta semana vimos solo un abreboca de lo que es capaz MongoDB, lo instalamos y vimos algunos detalles sobre su configuración, que la desesperación no te impida llegar a la semana que viene para aprender a utilizarlo. Hasta entonces.
MongoDB desde Cero: Operaciones Básicas Luego que hemos instalado MongoDB en la entrada pasada, seguramente querrás comenzar a realizar inserciones y queriespara probar las ventajas de esta solución y poner tus habilidades en práctica, comencemos con algunas operaciones básicas para saber como manipular los datos en MongoDB.
Creación de registros -
.insert()
Las operaciones son como funciones de Javascript, así que llamaremos al objeto base de datos db y crearemos una nueva propiedad o lo que se asemejaría al concepto de tabla con el nombre de autores y le asociaremos su valor correspondiente (un objeto autor), es decir, una colección con un documento asociado: 1 > db.autores.insert({ 2
nombre
: 'Jonathan',
3
apellido : 'Wiesel',
4
secciones : ['Como lo hago' , 'MongoDB']
5 });
Los documentos se definen como los objetos Javascript, u objetos JSON. Inclusive es posible declarar el documento como un objeto, almacenarlo en una variable y posteriormente insertarlo de la siguiente manera:
1 > var autorDelPost = { 2
nombre
: 'Jonathan',
3
apellido
4
secciones : ['Como lo hago' , 'MongoDB']
: 'Wiesel',
5 }; 6 7 > db.autores.insert(autorDelPost);
Ahora si ejecutamos el comando show collections podremos ver que se encuentra nuestra nueva colección de autores: 1 autores 2 ...
Agreguemos un par de autores más:
1 > db.autores.insert({ 2
nombre
: 'Oscar',
3
apellido
4
secciones : ['iOS' , 'Objective C' , 'NodeJS' ],
5
socialAdmin : true
: 'Gonzalez',
6 }); 7 > db.autores.insert({ 8
nombre
: 'Alberto',
9
apellido
: 'Grespan',
10
secciones : 'Git',
11
genero
: "M"
12 });
Veamos que insertamos nuevos documentos en la colección de autores que tienen otra estructura, en MongoDB esto es completamente posible y es una de sus ventajas.
Búsqueda de registros -
.find()
Hagamos un query o una busqueda de todos registros en la colección de autores. > db.autores.find(); 1 2 { "_id" : ObjectId("5232344a2ad290346881464a"), "nombre" : "Jonathan", "apellido" : "Wiesel", "secciones" : 3 [ "Como lo hago", "Noticias" ] } 4 5
{ "_id" : ObjectId("523236022ad290346881464b"), "nombre" : "Oscar", "apellido" : "Gonzalez", "secciones" : [ "iOS", "Objective C", "NodeJS" ], "socialAdmin" : true } { "_id" : ObjectId("5232383a2ad290346881464c"), "nombre" : "Alberto", "apellido" : "Grespan", "secciones" : "Git", "genero" : "M" }
Notemos que la búsqueda nos arroja los objetos resultantes, en este caso los documentos de los 3 autores que insertamos acompañados del identificador único que crea MongoDB, este campo _id se toma además como indice por defecto.
Si lo deseas puedes manualmente especificar el valor del campo _id cuando estas insertando los registros con el comando db.coleccion.insert() ; sin embargo ten en cuenta que debes asegurar que este valor sea único, de lo contrario los registros con dicho campo duplicado resultarán en error clave Una búsqueda como la anterior seríapor similar enprimaria SQL a: duplicada. SELECT * FROM autores
Filtros Digamos que ahora queremos hacer la búsqueda pero filtrada por los algún parámetro. Para esto sólo debemos pasar el filtro deseado a la función find(), busquemos a los administradores sociales para probar: 1
> db.autores.find({ socialAdmin: true });
2 3
{ "_id" : ObjectId("523236022ad290346881464b"), "nombre" : "Oscar", "apellido" : "Gonzalez", "secciones" : [ "iOS", "Objective C", "NodeJS" ], "socialAdmin" : true }
En SQL sería similar a: SELECT * FROM autores WHERE socialAdmin = true
Probemos ahora filtrar por varias condiciones, primero probemos con filtros donde TODOS se deben cumplir: 1
> db.autores.find({ genero: 'M', secciones: 'Git' });
2 3
{ "_id" : ObjectId("5232383a2ad290346881464c"), "nombre" : "Alberto", "apellido" : "Grespan", "secciones" : "Git", "genero" : "M" }
Es importante destacar que si el documento resultante hubiese tenido en la propiedadsecciones un arreglo en lugar de una cadena de caracteres, si dicho arreglo tuviese el valor Git también cumple la condición. En SQL sería similar a: SELECT * FROM autores WHERE genero = 'M' AND secciones = 'Git'
Veamos ahora un ejemplo un poco más avanzado de filtros con condiciones donde queremos que solo ALGUNA de ellas se cumpla: > db.autores.find({ 1
$or: [
2 {socialAdmin : true}, 3 {genero: 'M'} 4 ] 5 }); 6 7 { "_id" : ObjectId("523236022ad290346881464b"), "nombre" : "Oscar", "apellido" : "Gonzalez", "secciones" : 8 [ "iOS", "Objective C", "NodeJS" ], "socialAdmin" : true } 9 { "_id" : ObjectId("5232383a2ad290346881464c"), "nombre" : "Alberto", "apellido" : "Grespan", "secciones" : "Git", "genero" : "M" }
En este caso estamos filtrando por aquellos autores que son administradores sociales ó aquellos que tengan el campo género con el carácter M.
En SQL sería similar a: SELECT * FROM autores WHERE socialAdmin = true OR genero = 'M'
Limitar y Ordenar Si quisiéramos limitar los resultados a un número máximo especificado de registros es tan fácil como agregar .limit(#) al final del comando .find() : 1
> db.autores.find().limit(1)
2 3
{ "_id" : ObjectId("5232344a2ad290346881464a"), "nombre" : "Jonathan", "apellido" : "Wiesel", "secciones" : [ "Como lo hago", "MongoDB" ] }
En SQL sería similar a: SELECT * FROM autores LIMIT 1
La misma modalidad sigue la funcionalidad de ordenar los registros por un campo en particular, el cual servirá de argumento a la función .sort() : 1
> db.autores.find().sort({apellido : 1})
2 { "_id" : ObjectId("523236022ad290346881464b"), "nombre" : "Oscar", "apellido" : "Gonzalez", "secciones" :
3 [ "iOS", "Objective C", "NodeJS" ], "socialAdmin" : true } 4 { "_id" : ObjectId("5232383a2ad290346881464c"), "nombre" : "Alberto", "apellido" : "Grespan", "secciones" : "Git", "genero" : "M" } 5 { "_id" : ObjectId("5232344a2ad290346881464a"), "nombre" : "Jonathan", "apellido" : "Wiesel", "secciones" : [ "Como lo hago", "MongoDB" ] }
El número 1 que acompaña al argumento de ordenamiento es el tipo de orden, 1 para descendiente y -1 para ascendente En SQL* sería SELECT FROM similar autoresa:ORDER
BY
apellido DESC
También podemos combinar ambas funciones tan solo llamando una después de otra: 1
> db.autores.find().sort({apellido : 1}).limit(1)
2 3
{ "_id" : ObjectId("523236022ad290346881464b"), "nombre" : "Oscar", "apellido" : "Gonzalez", "secciones" : [ "iOS", "Objective C", "NodeJS" ], "socialAdmin" : true }
Otros filtros Existen varios operadores más para filtrar las búsquedas; eso lo dejaremos para más adelante para no sobrecargarte de información tan rápido, pero no te preocupes que no lo pasaremos por alto.
Eliminación de registros -
.remove()
y
.drop()
Si entendiste como buscar registros pues eliminarlos es igual de fácil. Para esto existen 4 posibilidades:
Eliminar los documentos de una colección que cumplan alguna condición. Eliminar todos los documentos de una colección. Eliminar la colección completa.
Probemos eliminandome a mí de la colección de autores: 1 > db.autores.remove({ nombre: 'Jonathan' });
En SQL sería similar a: DELETE FROM autores WHERE nombre = 'Jonathan'
¿Fácil no?. Eliminemos ahora a los demás autores:
1 > db.autores.remove();
En SQL sería similar a: DELETE FROM autores
Ahora que la colección ha quedado vacía deshagamonos de ella: 1 > db.autores.drop();
En SQL sería similar a: DROP TABLE autores
Conclusión Seguramente te estarás preguntando: ¿Y qué pasó con los update?. La modificación de registros involucra múltiples maneras para que puedas manipular la información a tu gusto, por ello lo dejaremos para la próxima entrada, no desesperes. Estos son los primeros pasos a tomar para que comiences a usar MongoDB, todavía queda un largo camino por delante ya que hay mucho que aprender sobre esta magnifica solución de base de datos.
MongoDB desde Cero: Actualizaciones / Updates Como mencionamos en la entrada pasada, la parte de actualizaciones o updates la dejamos por separado para tratar de ser un poco más detallados y extendernos en esta área para que puedas dominar con mayor destreza la manipulación de los datos.
Estructura Para modificar los documentos que ya se encuentran almacenados usaremos el comando .update() el cual tiene una estructura como esta: 1 db.coleccion.update( 2
filtro,
3
cambio,
4
{
5
upsert: booleano,
6
multi: booleano
7 8 );
}
Aclaremos un poco lo que nos indica la estructura. filtro – debemos especificar como encontrar el registro que desemos modificar, sería el mismo tipo de filtro que usamos en las búsquedas o finders. cambio – aquí especificamos los cambios que se deben hacer. Sin embargo ten en cuenta que hay 2 tipos de cambios que se pueden hacer: Cambiar el documento completo por otro que especifiquemos. Modificar nada más los campos especificados.
(opcional, false por defecto) – este parametro nos permite especificar en su estado true que si el filtro no encuentra ningun resutlado entonces el cambio debe ser insertado como un nuevo registro. multi (opcional, false por defecto) – en caso de que el f iltro devuelva más de un resultado, si especificamos este parametro como true, el cambio se realizará a todos los resultados, de lo contrario solo se le hará al primero (al de menor Id). upsert
Actualización sobrescrita (overwrite) Bien, probemos insertando nuevo autor, el cual modificaremos luego: 1 > db.autores.insert({ 2
nombre
: 'Ricardo',
3
apellido : 'S'
4 });
Ahora probemos el primer caso, cambiar todo el documento, esto significa que en lugar de cambiar solo los campos que especifiquemos, el documento será sobreescrito con lo que indiquemos: 1 > db.autores.update( 2
{nombre: 'Ricardo'},
3
{
4
nombre: 'Ricardo',
5
apellido: 'Sampayo',
6
secciones: ['Ruby','Rails'],
7
esAmigo: false
8
}
9 );
Notemos que como primer parámetro indicamos el filtro, en este caso que el nombre sea Ricardo , luego indicamos el cambio que hariamos, como estamos probando el primer caso indicamos el documento completo que queremos que sustituya al actual. Si hacemos db.autores.find({nombre:'Ricardo'}); podremos ver que en efecto el documento quedó como especificamos: 1
{ "_id" : ObjectId("523c91f2299e6a9984280762"), "nombre" : "Ricardo", "apellido" : "Sampayo", "secciones" : [ "Ruby", "Rails" ], "esAmigo" : false }
Sobreescbirir el documento no cambiará su identificador único
_id.
Operadores de Modificación Ahora probemos cambiar los campos que deseamos, para este caso haremos uso de lo que se denominan como operadores de modificación. Hablemos un poco sobre algunos de estos operadores antes de verlos en acción:
incrementa en una cantidad numerica especificada el valor del campo a en cuestión. $rename – renombrar campos del documento. $set – permite especificar los campos que van a ser modificados. $unset – eliminar campos del documento. $inc –
Referentes a arreglos: elimina el primer o último valor de un arreglo. elimina los valores de un arreglo que cumplan con el filtro indicado.
$pop –
$pull –
$pullAll – elimina los valores especificados $push – agrega un elemento a un arreglo.
de un arreglo.
agrega elementos a un arreglo solo sí estos no existen ya. para ser usado en conjunto con $addToSet o $push para indicar varios elementos a ser agregados al arreglo.
$addToSet –
$each –
Hagamos una prueba sobre nuestro nuevo documento: 1 > db.autores.update( 2
{ nombre: 'Ricardo' },
3
{
4
$set: { esAmigo: true , age : 25 }
5
}
6 );
En este caso estamos usando el operador $set para 2 propositos a la vez:
Actualizar el valor de un campo (cambiamos esAmigo de false a true). Creamos un campo nuevo (age) asignandole el valor 25.
Supongamos que Ricardo cumplió años en estos días, así que para incrementar el valor de su edad lo podemos hacer así: 1 > db.autores.update( 2
{ nombre: 'Ricardo' },
3
{
4 5
$inc: { age : 1 } }
6 );
También podemos indicar números negativos para decrementar. Aquí hablamos español, así que cambiemos ese campo age por lo que le corresponde: 1 > db.autores.update( 2
{ nombre: 'Ricardo' },
3
{
4 5
$rename: { 'age' : 'edad' } }
6 );
Los que trabajamos en Codehero somos todos amigos así que no es necesario guardar el campo esAmigo : 1 > db.autores.update( 2
{ nombre: 'Ricardo' },
3
{
4 5
$unset: { esAmigo : '' } }
6 );
El valor que le “asignes” al campo a eliminar no tendrá ningun efecto,
pero es necesario escribirlo por motivos de sintaxis. Pasemos ahora a la parte de modificación de arreglos, agreguemosle algunas secciones extra a nuestro autor: 1 > db.autores.update( 2
{ nombre: 'Ricardo' },
3
{
4 5
$push: { secciones : 'jQuery' } }
6 );
Si se quiere asegurar que el elemento no esté duplicado se usaría $addToSet Esto agregará al final del arreglo de secciones el elemento jQuery . Agreguemos algunas secciones más en un solo paso: 1 > db.autores.update( 2
{ nombre: 'Ricardo' },
3
{
4 5
$push: { secciones : { $each : ['Haskell','Go','ActionScript'] } } }
6 );
Bueno en realidad Ricardo no maneja desde hace un tiempo ActionScript así que eliminemos ese ultimo elemento del arreglo: 1 > db.autores.update( 2
{ nombre: 'Ricardo' },
3
{
4 5
$pop: { secciones : 1 } }
6 );
Para eliminar el último elemento se coloca 1, para el primero -1. Ricardo hace tiempo que no nos habla sobre jQuery asi que hasta que no se reivindique quitemoslo de sus secciones:
1 > db.autores.update( 2
{ nombre: 'Ricardo' },
3
{
4 5
$pull: { secciones : 'jQuery' } }
6 );
Pensandolo bien, Ricardo nunca nos ha hablado de Haskell ni Go tampoco, eliminemoslos también: 1 > db.autores.update( 2
{ nombre: 'Ricardo' },
3
{
4 5
$pullAll: { secciones : ['Haskell','Go'] } }
6 );
Comando .save() Otra manera para actualizar o insertar registros es mediante el uso del comando .save() . Este comando recibe como parámetro únicamente un documento. Insertar un registro es tal cual como si hicieramos un .insert() :
1 > db.autores.save({ 2
nombre:
'Ramses'
3 });
En cuanto al caso de actualización de registros te estarás preguntando:
¿Si solo recibe un documento, como sabe Mongo que documento debe actualizar? En estos casos puedes hacer el equivalente a una actualización sobrescrita con tan solo indicar el _id del registro a actualizar como parte del nuevo documento.
1 > db.autores.find({nombre: 'Ramses'}); 2 3 { "_id" : ObjectId("5246049e7bc1a417cc91ec8c"), "nombre" : "Ramses" } 4 5 > db.autores.save({ 6
_id:
ObjectId('5246049e7bc1a417cc91ec8c')
7
nombre:
8
apellido: 'Velasquez',
9
secciones: ['Laravel', 'PHP']
'Ramses',
10 });
En esta caso particular, debido a que Mongo le asigno automáticamente ese ID autogenerado poco amigable, ese mismo es el que pusimos en nuestro documento para que Mongo sepa que debe actualizar ese registro, algunos recomiendan usar _id que no sean establecidos por Mongo sino por la librería del cliente para evitar trabajar con este tipo de identificadores poco amigables.
Conclusión Como has podido notar, MongoDB es muy poderoso y ofrece muchas ventajas en cuento a la modificación de sus registros. Claro, las búsquedas también se pueden tornar bastante interesantes según nuestras necesidades y eso lo veremos más adelante, a medida que vamos avanzando iremos tocando la mayoría de los temas que puedan servir de ayuda para que logres dominar al máximo esta solución NoSQL.
MongoDB desde Cero: Modelado de Datos MongoDB desde Cero: Modelado de Datos
Una de las dificultades que encuentran aquellos que se adentran al mundo del NoSQL es al tratar de transformar su esquema de base de datos relacional para que funcione de la mejor manera segun el enfoque NoSQL orientado a documentos, como lo es MongoDB. Aquí aprenderemos sobre como realizar correctamente el modelado de datos para que puedas comenzar a pensar en migrar tus proyectos a este tipo de base de datos con la menor dificultad posible.
Tipos de Datos Al comienzo de la serie explicamos que los documentos de MongoDB son como objetos JSON, para ser especificos son de tipo BSON (JSON Binario), esta estrategia permite la serialización de documentos tipo JSON codificados binariamente. Veamos algunos de los tipos de datos que soporta:
String – Cadenas de
Integer –
caracteres. Números enteros.
Números con decimales. Booleanos verdaderos o falsos. Date – Fechas. Timestamp – Estampillas de tiempo. Null – Valor nulo. Array – Arreglos de otros tipos de dato. Object – Otros documentos embebidos. ObjectID – Identificadores únicos creados por MongoDB al crear
Double –
Boolean –
documentos sin– especificar el campo Data Binaria Punteros a valores archivospara binarios. Javascript –
_id.
código y funciones Javascript.
Patrones de Modelado Existen 2 patrones principales que nos ayudarán a establecer la estructura que tendrán los documentos para lograr relacionar datos que en una base de datos relacional estarían en diferentes tablas.
Embeber Este patrón se enfoca en incrustar documentos uno dentro de otro con la finalidad de hacerlo parte del mismo registro y que la relación sea directa.
Si tienes experiencia en bases de datos orientadas o objetos, este patrón seguiría el mismo principio para la implementación de un TDA (Tipo de Dato Abstracto).
Referenciar Este patrón busca imitar el comportamiento de las claves foráneas para relacionar datos que deben estar en colecciones diferentes.
Debes tomar en cuenta cuando estés en el proceso de modelado que si piensas hacer muchas actualizaciones sobre datos de colecciones relacionadas (en especial modificaciones atómicas), trata en lo posible de que aquellos datos que se vayan a modificar se encuentren en el mismo documento.
Modelado de Relaciones Bien, ha llegado el momento de aprender a transformar las relaciones de las tablas en las bases de datos relacionales. Empecemos con lo más básico.
Relaciones 1-1. Muchas opiniones concuerdan que las relaciones 1 a 1 deben ser finalmente normalizadas para formar una única tabla; sin embargo existen consideraciones especiales donde es mejor separar los datos en tablas diferentes. Supongamos el caso que tenemos una tabla persona y otra tabla docu mentos personale s , donde una persona tiene un solo juego de documentos personales y que un juego de documentos personales solo puede pertenecer a una persona.
Si traducimos esto tal cual a lo que sabemos hasta ahora de MongoDB sería algo así: 1 Persona = { 2
nombre
: 'Jonathan',
3
apellido
: 'Wiesel',
4
genero
: 'M'
5 } 6 7 DocumentosPersonales = { 8
pasaporte
: 'D123456V7',
9
licencia
10
seguro_social : 'V-543523452'
: '34567651-2342',
11 }
Para los casos de relaciones 1-a-1 se utiliza el patrón de embeber un documento en otro, por lo que el documento final quedaría así:
1 Persona = { 2
nombre
: 'Jonathan',
3
apellido
: 'Wiesel',
4
genero
: 'M',
5
documentos : {
6
pasaporte
7
licencia
8
seguro_social : 'V-543523452'
9
: 'D123456V7', : '34567651-2342',
}
10 }
Relaciones 1-* Supongamos ahora el caso de una tabla persona y otra tabla dirección . Donde una persona puede poseer varias direcciones.
Por el momento no nos pondremos creativos al pensar que una dirección puede pertenecer a más de una persona. Traduciendolo tal cual a MongoDB tendríamos algo así: 1 Persona = { 2
nombre
3
apellido
: 'Wiesel',
4
genero
: 'M'
5 }
: 'Jonathan',
6 7 Direccion1 = { 8
pais
: 'Venezuela',
9
estado
: 'Distrito Capital'
10
ciudad
: 'Caracas'
11
urbanizacion
12
avenida
13
edificio
14
piso
15
apartamento
: 'La Florida', : ...,
: ..., : ..., : ...
16 } 17 18 Direccion2 = { 19
pais
: 'Estados Unidos',
20
estado
: 'Florida'
21
ciudad
: 'Miami'
22
urbanizacion
23
avenida
24
edificio
25
piso
26
apartamento
: 'Aventura', : ...,
: ..., : ..., : ...
27 }
Ahora para transformar la relación tenemos 2 opciones. Podemos embeber las direcciones en el documento de la persona al establecer un arreglo de direcciones embebidas: 1 Persona = { 2
nombre
3
apellido
: 'Wiesel',
4
genero
: 'M',
5
direcciones : [{
6
pais
: 'Jonathan',
: 'Venezuela',
7
estado
: 'Distrito capital'
8
ciudad
: 'Caracas',
9
urbanizacion
10
avenida
11
edificio
12
piso
13
apartamento
14
: 'La Florida', : ...,
: ..., : ..., : ...
},{
15
pais
16
estado
: 'Florida'
17
ciudad
: 'Miami'
18
urbanizacion
19
avenida
20
edificio
21
piso
22 23
: 'Estados Unidos',
: 'Aventura', : ...,
: ..., : ...,
apartamento
: ...
}]
24 }
ó podemos dejarlo en documentos separados. Para esta segunda opción tenemos 2 enfoques. Uno sería agregar un campo de referencia a dirección en persona : 1 Direccion1 = { 2
_id
: 1,
3
pais
: 'Venezuela',
4
estado
: 'Distrito Capital',
5
ciudad
: 'Caracas'
6
urbanizacion
7
avenida
8
edificio
9
piso
10
apartamento
: 'La Florida', : ...,
: ..., : ..., : ...
11 } 12 13 Direccion2 = { 14
_id
: 2,
15
pais
: 'Estados Unidos',
16
estado
: 'Florida',
17
ciudad
: 'Miami'
18
urbanizacion
19
avenida
20
edificio
21
piso
22
apartamento
: 'Aventura', : ...,
: ..., : ..., : ...
23 } 24 25 Persona = { 26
nombre
27
apellido
: 'Wiesel',
: 'Jonathan',
28
genero
: 'M',
29
direcciones : [1,2]
30 }
y el otro sería agregar un campo de referencia a persona en dirección : 1 Direccion1 = { 2
_id
: 1,
3
pais
: 'Venezuela',
4
estado
: 'Distrito Capital',
5
ciudad
: 'Caracas'
6
urbanizacion
7
avenida
8
edificio
9
piso
10
apartamento
: 'La Florida', : ...,
: ..., : ..., : ...,
11
persona_id
: 1
12 } 13 14 Direccion2 = { 15
_id
: 2,
16
pais
: 'Estados Unidos',
17
estado
: 'Florida',
18
ciudad
: 'Miami'
19
urbanizacion
20
avenida
21
edificio
22
piso
23
apartamento
24
persona_id
: 'Aventura', : ...,
: ..., : ..., : ..., : 1
25 } 26 27 Persona = { 28
_id
: 1
29
nombre
30
apellido
: 'Wiesel',
31
genero
: 'M'
: 'Jonathan',
32 }
En lo posible trata de utilizar la opción de embeber si los arreglos no variarán mucho ya que al realizar la búsqueda de la persona obtienes de una vez las direcciones, mientras que al trabajar con referencias tu aplicación debe lo manejar unaellógica múltiples búsquedas para resolver las referencias, que sería equivalente a los joins. En caso de utilizar la segunda opción, ¿Cual de los 2 últimos enfoques utilizar?. En este caso debemos tomar en cuenta que tanto puede crecer la lista de direcciones, en caso que la tendencia sea a crecer mucho, para evitar arreglos mutantes y en constante crecimiento elsegundo enfoque sería el más apropiado.
Relaciones *-* Finalmente nos ponemos creativos a decir que, en efecto, varias personas pueden pertenecer a la misma dirección. Al aplicar normalización quedaría algo así:
Para modelar este caso es muy similar al de relaciones uno a muchos con referencia por lo que colocaremos en ambos tipos de documento un arreglo de referencias al otro tipo. Agreguemos una persona adicional para demostrar mejor el punto: 1 Direccion1 = { 2
_id
: 1,
3
pais
: 'Venezuela',
4
estado
: 'Distrito Capital',
5
ciudad
: 'Caracas'
6
urbanizacion
7
avenida
8
edificio
9
piso
10
apartamento
11
personas
: 'La Florida', : ...,
: ..., : ..., : ..., : [1000]
12 } 13 14 Direccion2 = { 15
_id
: 2,
16
pais
: 'Estados Unidos',
17
estado
: 'Florida',
18
ciudad
: 'Miami'
19
urbanizacion
20
avenida
21
edificio
22
piso
23
apartamento
24
personas
: 'Aventura', : ...,
: ..., : ..., : ..., : [1000,1001]
25 } 26 27 Persona1 = { 28
_id
: 1000,
29
nombre
30
apellido
: 'Wiesel',
31
genero
: 'M',
32
direcciones : [1,2]
: 'Jonathan',
33 } 34 35 Persona2 = { 36
_id
: 1001,
37
nombre
38
apellido
: 'Cerqueira',
39
genero
: 'M',
40
direcciones : [2]
: 'Carlos',
41 }
Seguro debes estar esperando el caso más complejo de todos, aquellas ocasiones donde la tabla intermedia tiene campos adicionales.
Tomando como base el ejemplo anterior, agregaremos el campo adicional usando el patrón para embeber de la siguiente manera: 1 Direccion1 = { 2
_id
: 1,
3
pais
: 'Venezuela',
4
estado
: 'Distrito Capital',
5
ciudad
: 'Caracas'
6
urbanizacion
7
avenida
8
edificio
9
piso
10
apartamento
11
personas
: 'La Florida', : ...,
: ..., : ..., : ..., : [1000]
12 } 13 14 Direccion2 = { 15
_id
: 2,
16
pais
: 'Estados Unidos',
17
estado
: 'Florida',
18
ciudad
: 'Miami'
19
urbanizacion
20
avenida
: 'Aventura', : ...,
21
edificio
: ...,
22
piso
23
apartamento
24
personas
: ..., : ..., : [1000,1001]
25 } 26 27 Persona1 = { 28
_id
: 1000,
29
nombre
30
apellido
: 'Wiesel',
31
genero
: 'M',
32
direcciones : [{
: 'Jonathan',
33
direccion_id
: 1,
34
viveAqui
: true
35
},{
36
direccion_id
: 2,
37
viveAqui
: false
38
}]
39 } 40 41 Persona2 = { 42
_id
: 1001,
43
nombre
44
apellido
: 'Cerqueira',
45
genero
: 'M',
46
direcciones : [{
: 'Carlos',
47
direccion_id
: 2,
48
viveAqui
: true
49
}]
50 }
De igual manera se pudiera embeber el campo viveAqui del lado de las direcciones, esto dependerá de como planeas manipular los datos.
Conclusión En este curso hemos aprendido a modelar los datos que conforman una base de datos de MongoDB, con este nuevo conocimiento ya podemos evaluar la estructura de una base de datos relacional para determinar si en efecto podemos transformarla fácilmente a NoSQL orientado a documentos y lograr aprovechar sus numerosas ventajas y flexibilidad.
MongoDB desde Cero: Índices. Parte I Cuando estamos construyendo nuestro esquema de bases de datos es común olvidarnos de implementar índices y solemos continuar con la siguiente tarea; sin embargo son estos los que ayudan significativamente en el rendimiento de la base de datos, especialmente cuando el volumen de datos va incrementando. En esta entrada hablaremos sobre ellos y cómo implementarlos en MongoDB.
¿Qué es un índice? Un índice en bases de datos es una estructura de datos que toma los valores de campos particulares de una tabla (los ID por defecto) y se almacena en un espacio de rápido acceso, esto con la intención de que al hacer un query cuyo campo de filtrado este indexado, el registro es localizado a partir del indice y la velocidad de respuesta del proceso sea mucho más rápido, tal cómo el índice de un libro. En el caso particular de MongoDB, al no buscar por un campo indexado el proceso de mongod debe recorrer todo el documento de los registros de una colección para hacer realizar la búsqueda, este proceso es ineficiente y requiere un alto numero de recursos para manipular todo el volumen de información. Una mala práctica que utilizan algunos es indexar la mayor cantidad de campos que puedan lo cual termina casi duplicando en volumen la base de datos y el proceso que busca en los índices se torna igual de lento que buscar normalmente ya que los índices son almacenados en memoria y al agotarse el espacio reservado, el almacenamiento de índices empieza a escribirse en disco, el cual es de acceso mucho mas lento. Es por esto que debemos resaltar y aconsejar que sólo debes crear índices sobre campos que vayan a ser consultados con
alta frecuencia.
Tipos de índice Hablemos un poco sobre los diferentes tipos de índice que podemos implementar en MongoDB:
de un registro es elde índice por defecto, Índice _id – el identificador si no especificas uno al crearprincipal un documento el proceso *mongod* le asignará uno automaticamente de tipo ObjectID. Índice sencillo – definidos sobre un único campo de un documento. Índice compuesto – definidos sobre varios campos de un documento, será tomado como un índice único por parte de MongoDB. Índice mutillave – utilizado en casos de que el campo a indexar pertenezca a subdocumentos dentro de un arreglo del documento padre. Índice geoespacial – utilizado para indexar campos que sean coordenadas de tipo GeoJSON. Índice texto – al momento de escritura se encuentra fase beta y se utiliza para buscar contenidos de cadenas de caracteres en los documentos. Índice tipo hash – utilizado en la estrategia de llaves de fragmento hasheadas que se lleva a cabo en los procesos de fragmentación de datos.
En este curso no tocaremos los últimas 3.
Propiedades Existen un par de propiedades interesantes de los índices que puedes aprovechar:
Unicidad – esta propiedad permite establecer el índice como campo único, uniquesobre un campo particular evitará es al declarar un índice comocon quedecir, existan documentos diferentes el mismo valor para dicho campo ya que al tratar de insertar un documento bajo estas circunstancias resultará en un error de llave duplicada. Esto resulta muy útil para campos que sirven de identificadores externos que suelen necesitar indexación.
Si tratas de insertar un documento que no tenga valor en este campo, automáticamente se le asignará el valor null al valor del indice para este registro. Si tratas nuevamente de hacer lo mismo ocurrirá un error de llave duplicada ya que el índice de valor null para dicho campo ya existe.
Dispersión – esta propiedad permite establecer el filtrado de los resultados
basado en el campo indexado. Lo entenderás mejor cuando lo apliquemos más adelante.
Preparación Bien, ahora que sabemos la teoría es hora de poner manos a la obra. Tomemos una situación de ejemplo para manejar el caso. La semana pasada jugamos unas 15 rondas de poker, creamos una colección puntuaciones y anotamos las siguientes puntuaciones finales: 1 { 2
nombre:
'Ricardo',
3
ganadas:
3,
4
perdidas: 3,
5
retiradas: 9,
6
dinero:
7
mejores:
8
comentarios:[
9
15, ['1 trío', '2 pares'],
{
10
hora: '22:10:10',
11
texto: 'Hoy será mi día de suerte'
12
},{
13
hora: '23:50:32',
14
texto: 'Quizás otro día'
15 16
} ]
17 } 18 19 { 20
nombre:
'Ramses',
21
ganadas:
8,
22
perdidas: 4,
23
retiradas: 3,
24
dinero:
25
mejores:
26
comentarios:[
27
120, ['Escalera real', 'Full house'],
{
28
hora: '22:12:56',
29
texto: 'Los humillaré a todos'
30
},{
31
hora: '23:55:59',
32
texto: 'Gracias por darme su dinero'
33 34
} ]
35 } 36 37 { 38
nombre:
'Oscar',
39
ganadas:
4,
40
perdidas: 6,
41
retiradas: 5,
42
dinero:
43
mejores:
44
comentarios:[
45
30, ['Full house', '1 trío'],
{
46
hora: '22:09:12',
47
texto: '¿En realidad vamos a jugar nada mas para mostrar un ejemplo de Mongo?'
48
},{
49
hora: '23:59:59',
50
texto: 'ZZZZZZzzzzzzz'
51 52
} ]
53 }
Ya sabes como se insertan estos documentos así que no hay excusa =).
Obtener índices Como mencionamos anteriormente el campo _id es el índice por defecto que crea MongoDB. Podemos comprobarlo de la siguiente manera: 1 > db.puntuaciones.getIndexes() 2 3 [ 4
{
5
"v" : 1,
6
"key" : {
7
"_id" : 1
8
},
9
"ns" : "codehero.puntuaciones",
10
"name" : "_id_"
11
}
12 ]
Por el momento podremos notar un solo documento en el arreglo que especifica lo siguiente:
indica la versión del índice (versiones de MongoDB previas a la 2.0 mostrarán el valor 0). key – muestra las llaves atadas al indice, en este caso es el campo _id y el número 1 nos indica el ordenamiento ascendente del campo. ns – especifica el contexto del namespace para el índice. name – es el nombre otorgado al índice el cual suele estar compuesto de las llaves o campos que lo componen junto con el orden de los mismos. (para este caso el número de ordenamiento no está presente debido a que se usa el por defecto). v–
Creación de índices Ahora creemos nuestros propios índices.
Simples Para este caso digamos que solo nos interesa el dinero con el que terminamos al final, para ello solo debemos hacer lo siguiente: 1 > db.puntuaciones.ensureIndex({ dinero : 1 })
Esto creará un índice simple ordenado ascendentemente y le permitirá al proceso de mongod realizar operaciones a través de él cuando se le involucre como filtro. Por ejemplo veamos quién termino con más de 50 en dinero: > db.puntuaciones.find({dinero : { $gt : 50 }}) 1 2 { "_id" : ObjectId("525a31aa4582c960d118b1de"), "nombre" : "Ramses", "ganadas" : 8, "perdidas" : 4, "retiradas" : 3, "dinero" : 120, "mejores" : [ "Escalera real", "Full house" ], "comentarios" : [ { "hora" : 3 "22:12:56", "texto" : "Los humillaré a todos" }, { "hora" : "23:55:59", "texto" : "Gracias por darme su dinero" } ] }
Esta búsqueda realizada utilizando el índiceun que acabamos de crear. ¿Cómo saber sies una operación está utilizando índice? Tranquilo, en la segunda parte lo podrás averiguar.
Compuestos Digamos ahora que queremos enfocarnos en la búsqueda en conjunto de los parámetros de partidas perdidas y retiradas para analizar un poco la situación. Para esto creemos un índice compuesto con ambos campos: 1 > db.puntuaciones.ensureIndex({ perdidas : 1, retiradas: 1 })
En el caso de los índices compuestos el orden en que los coloques es muy importante. Para el ejemplo que planteamos el orden de cada campo nos indica que MongoDB podrá utilizar el indice para soportar queries que incluyan el campo perdidas y aquellos que incluyan ambos campos (perdidas y retiradas); sin embargo aquellas búsquedas que solo incluyan el campo retiradas no podrán utilizar este índice. 1 > db.puntuaciones.find({perdidas: {$gt:3}}) 2 { "_id" : ObjectId("525a31aa4582c960d118b1de"), "nombre" : "Ramses", "ganadas" : 8, "perdidas" : 4, "retiradas" : 3, "dinero" : 120, "mejores" : [ "Escalera real", "Full house" ], "comentarios" : [ { "hora" : 3 "22:12:56", "texto" : "Los humillaré a todos" }, { "hora" : "23:55:59", "texto" : "Gracias por darme su
4 dinero" } ] } 5 { "_id" : ObjectId("525a31ae4582c960d118b1df"), "nombre" : "Oscar", "ganadas" : 4, "perdidas" : 6, "retiradas" : 5, "dinero" : 30, "mejores" : [ "Full house", "1 trío" ], "comentarios" : [ { "hora" : "22:09:12", "texto" : "¿En 6 realidad vamos a jugar nada mas para mostrar un ejemplo de Mongo?" }, { "hora" : "23:59:59", "texto" : "ZZZZZZzzzzzzz" } ] }
> db.puntuaciones.find({perdidas: {$gt:3}, retiradas:{$gt: 3}}) { "_id" : ObjectId("525a31ae4582c960d118b1df"), "nombre" : "Oscar", "ganadas" : 4, "perdidas" : 6, "retiradas" : 5, "dinero" : 30, "mejores" : [ "Full house", "1 trío" ], "comentarios" : [ { "hora" : "22:09:12", "texto" : "¿En realidad vamos a jugar nada mas para mostrar un ejemplo de Mongo?" }, { "hora" : "23:59:59", "texto" : "ZZZZZZzzzzzzz" } ] }
Suponiendo el caso que hubiesemos incluido el campo ganadas en el índice, específicamente en la posición entre perdidas y retiradas, una búsqueda que incluya los campos perdidas yretiradas sin duda continua siendo posible pero será más ineficiente que como lo definimos previamente.
Conclusión El tema de los índices es uno de gran importancia si quieres ajustar el rendimiento de tu esquema de datos, en esta entrada aprendimos sobre ellos y comenzamos a raspar la superficie sobre su implementación en MongoDB. No te preocupes, en la siguiente entrada retomaremos donde nos quedamos y te seguiremos introduciendo a sus bondades. Solo es una semana, no desesperes.
MongoDB desde Cero: Índices. Parte II En la entrada pasada comenzamos a hablar sobre la importancia de los índices en una base de datos e iniciamos el proceso de implementación de algunos de ellos en MongoDB, esta semana seguiremos aprendiendo sobre ellos para cubrir las posibilidades del uso de los mismos. Recuerda retomar el ejemplo del juego de
poker de la entrada pasada.
Índices multillave Habíamos mencionado que los índices multillave se basan en utilización de arreglos. Tenemos 2 enfoques y para ello utilizaremos los arreglos que creamos en nuestro jugadores estrella, los campos mejores (indicando sus mejores combinaciones de cartas en la noche) y comentarios que fueron algunos de los captados en la noche del encuentro.
Arreglos básicos Veamos el primer caso con los arreglos sencillos del campo mejores: 1 > db.puntuaciones.ensureIndex({ mejores : 1 });
Este índice creará varias llaves de índice, una para cada valor del arreglo, algo parecido a esto: 1 Full house 2 1 trio
=> [Ramses, Oscar] => [Ricardo, Oscar]
3 Escalera real => [Ramses]
Por lo que es posible utilizar estos valores en las búsquedas al buscar por el campo mejores utilizando el índice: > db.puntuaciones.find({ mejores:'Full house' })
1
{ "_id" : ObjectId("525a31aa4582c960d118b1de"), "nombre" : "Ramses", "ganadas" : 8, "perdidas" : 4, "retiradas"
2 : 3, "dinero" : 120, "mejores" : [ "Escalera real", "Full house" ], "comentarios" : [ { "hora" : "22:12:56", "texto" : "Los humillaré a todos" }, { "hora" : "23:55:59", "texto" : "Gracias por darme su 3 dinero" } ] } 4 { "_id" : ObjectId("525a31ae4582c960d118b1df"), "nombre" : "Oscar", "ganadas" : 4, "perdidas" : 6, "retiradas" : 5, "dinero" : 30, "mejores" : [ "Full house", "1 trío" ], "comentarios" : [ { "hora" : "22:09:12", "texto" : "¿En realidad vamos a jugar nada mas para mostrar un ejemplo de Mongo?" }, { "hora" : "23:59:59", "texto" : "ZZZZZZzzzzzzz" } ] }
Arreglos con documentos anidados En este caso se toma el campo del documento interno para hacerlo índice, digamos que nos interesan los comentarios que expresaron nuestros jugadores durante la noche: 1 > db.puntuaciones.ensureIndex({ 'comentarios.texto' : 1 });>
Con la notación de punto podemos especificar el campo interno del arreglo del documento padre para que nos sirva de índice. Lo podríamos utilizar en un query como este: > db.puntuaciones.find({'comentarios.texto' : 'Los humillaré a todos'}) 1 2 { "_id" : ObjectId("525a31aa4582c960d118b1de"), "nombre" : "Ramses", "ganadas" : 8, "perdidas" : 4, "retiradas" : 3, "dinero" : 120, "mejores" : [ "Escalera real", "Full house" ], "comentarios" : [ { "hora" : 3 "22:12:56", "texto" : "Los humillaré a todos" }, { "hora" : "23:55:59", "texto" : "Gracias por darme su dinero" } ] }
Uso de propiedades Propiedad de unicidad Cómo mencionamos anteriormente, esta propiedad nos permite asegurar que el valor de este campo sea unico. En nuestro caso crearemos un índice para los
nombres de nuestro jugadores y además nos aseguraremos que ningún otro pueda llamarse igual: 1 > db.puntuaciones.ensureIndex({ nombre : 1 }, { unique : true })
Ahora si tratamos de introducir un nuevo jugador con un nombre que ya exista obtendremos un error de índice duplicado: 1 > var otroRicardo = { 2
nombre :
'Ricardo'
3} 4 5 > db.puntuaciones.insert(otroRicardo) 6 7 E11000 duplicate key error index: test.puntuaciones.$nombre_1 dup key: { : "Ricardo" }
Propiedad de dispersión Esta propiedad particular de los índices nos permite filtrar los resultados buscados por el campo indexado dejando por fuera aquellos registros que no poseen el campo. Para esto insertemos un nuevo jugador que (entre otras cosas que no nos interesan para el ejemplo) no posea el campo ganadas: 1 > var carlos = { 2
nombre : 'Carlos'
3} 4 5 > db.puntuaciones.insert(carlos)
Ahora crearemos el índice de dispersón sobre el campo ganadas: 1 > db.puntuaciones.ensureIndex({ ganadas : 1 }, { sparse : true })
Probemos ahora como los resultados son filtrados: 1
> db.puntuaciones.find().sort({ ganadas : 1 })
2 3 4
{ "_id" : ObjectId("525a31a74582c960d118b1dd"), "nombre" : "Ricardo", "ganadas" : 3, "perdidas" : 3, "retiradas" : 9, "dinero" : 15, "mejores" : [ "1 trío", "2 pares" ], "comentarios" : [ { "hora" : "22:10:10", "texto" : "Hoy será mi día de suerte" }, { "hora" : "23:50:32", "texto" : "Quizás otro día" } ] } { "_id" : ObjectId("525a31ae4582c960d118b1df"), "nombre" : "Oscar", "ganadas" : 4, "perdidas" : 6, "retiradas" :
5 5, "dinero" : 30, "mejores" : [ "Full house", "1 trío" ], "comentarios" : [ { "hora" : "22:09:12", "texto" : "¿En realidad vamos a jugar nada mas para mostrar un ejemplo de Mongo?" }, { "hora" : "23:59:59", "texto" : "ZZZZZZzzzzzzz" } ] } { "_id" : ObjectId("525a31aa4582c960d118b1de"), "nombre" : "Ramses", "ganadas" : 8, "perdidas" : 4, "retiradas" : 3, "dinero" : 120, "mejores" : [ "Escalera real", "Full house" ], "comentarios" : [ { "hora" : "22:12:56", "texto" : "Los humillaré a todos" }, { "hora" : "23:55:59", "texto" : "Gracias por darme su dinero" } ] }
Podemos notar que Carlos no se encuentra entre los resultados, esto se debe a que al colocar el índice con la propiedad de dispersión sobre el campo ganadas (el cual es parte del filtro de la búsqueda al utilizarlo para ordenar) esto evita que aquellos registros que no posean el campo sean excluidos de los resultados.
Si el indice para este campo no tuviese la propiedad de dispersión, en los resultados de la búsqueda Carlos estuviese de primero, según el patrón de ordenamiento ascendente indicado.
Construcción en el fondo Uno de los factores a considerar en la creación de índices es que mientras estos son construidos la base de datos que los contiene se bloquea para lectura y escritura. Es posible que al tener un volumen elevado de datos, el proceso de construcción de índices tome un tiempo, lo cual llevaría a una interrupción en el servicio que usa este medio de almacenamiento. Para estos casos existe la opción de construcción de índices en el fondo(o background construction). Esto evita que la base de datos se bloquee al crear un índice en particular: 1 > db.puntuaciones.ensureIndex({ campo_que_desees_indexar : 1 }, { background : true })
Ten en cuenta que el proceso de creación de índices en el fondo es más lento que la creación normal. Inclusive si los índices utilizan más espacio que la memoria disponible, el proceso se torna considerablemente más lento.
Reconstrucción de índices Estar jugando con los índices a medida que vamos introduciendo data nueva causará que algunos registros no estén indexados, por lo que necesitaremos reconstruirlos, con MongoBD este proceso es muy sencillo, tan solo debemos ejecutar el método de reindexación sobre la colección deseada: 1 > db.puntuaciones.reIndex()
Medición de uso Durante este par de entradas quizás te habrás estado preguntando: ¿Cómo saber si en realidad las búsquedas están utilizando los índices en lugar del procedimiento normal?. Para eso tenemos un par métodos de gran utilidad. explain()
Este método pretende explicarnos brevemente su plan de ejecución para una operación en particular, hagamos la prueba con una búsqueda sencilla para ver de qué se trata: 1 > db.puntuaciones.find().explain() 2 { 3
"cursor" : "BasicCursor",
4
"isMultiKey" : false,
5
"n" : 4,
6
"nscannedObjects" : 4,
7
"nscanned" : 4,
8
"nscannedObjectsAllPlans" : 4,
9
"nscannedAllPlans" : 4,
10
"scanAndOrder" : false,
11
"indexOnly" : false,
12
"nYields" : 0,
13
"nChunkSkips" : 0,
14
"millis" : 0,
15
"indexBounds" : {
16 17
},
18
"server" : "Mordor.local:27017"
19 }
Aquí podemos notar un plan de ejecución común donde podemos apreciar varias estadísticas sobre lo que la operación hará, entre ellas la cantidad de objetos que serán escaneados, la duración del proceso, y varios detalles sobre la utilización de
índices, en este caso ningún índice es utilizado, probemos con la búsqueda del primer índice que creamos en la entrada pasada: 1 > db.puntuaciones.find({dinero : { $gt : 50 }}).explain() 2 3
"cursor" : "BtreeCursor dinero_1",
4
"isMultiKey" : false,
5
"n" : 1,
6
"nscannedObjects" : 1,
7
"nscanned" : 1,
8
"nscannedObjectsAllPlans" : 1,
9
"nscannedAllPlans" : 1,
10
"scanAndOrder" : false,
11
"indexOnly" : false,
12
"nYields" : 0,
13
"nChunkSkips" : 0,
14
"millis" : 0,
15
"indexBounds" : {
16
"dinero" : [
17
[
18
50,
19
1.7976931348623157e+308
20
]
21
]
22
},
23
"server" : "Mordor.local:27017"
24 }
Notaremos que en este caso las estadísticas son algo diferentes, el cursor es diferente debido a que ahora posee una estructura de tipo BTREE para recorrer los nodos de índices. El número de objetos escaneadas en menor y además nos especifica el campo del índice y sus límites.
hint()
Este método nos permite indicarle a una búsqueda qué índice debe utilizar. Probemos con una búsqueda sencilla que obliguemos a utilizar el índice de dinero: > db.puntuaciones.find().hint({ ganadas : 1 })
1 { "_id" : ObjectId("525a31a74582c960d118b1dd"), "nombre" : "Ricardo", "ganadas" : 3, "perdidas" : 3, "retiradas" : 9, "dinero" : 15, "mejores" : [ "1 trío", "2 pares" ], "comentarios" : [ { "hora" : "22:10:10", 2 será mi día de suerte" }, { "hora" : "23:50:32", "texto" : "Quizás otro día" } ] }
"texto" : "Hoy
3 { "_id" : ObjectId("525a31ae4582c960d118b1df"), "nombre" : "Oscar", "ganadas" : 4, "perdidas" : 6, "retiradas" : 5, "dinero" : 30, "mejores" : [ "Full house", "1 trío" ], "comentarios" : [ { "hora" : "22:09:12", "texto" : "¿En 4 realidad vamos a jugar nada mas para mostrar un ejemplo de Mongo?" }, { "hora" : "23:59:59", "texto" : 5 "ZZZZZZzzzzzzz" } ] } { "_id" : ObjectId("525a31aa4582c960d118b1de"), "nombre" : "Ramses", "ganadas" : 8, "perdidas" : 4, "retiradas" : 3, "dinero" : 120, "mejores" : [ "Escalera real", "Full house" ], "comentarios" : [ { "hora" : "22:12:56", "texto" : "Los humillaré a todos" }, { "hora" : "23:55:59", "texto" : "Gracias por darme su dinero" } ] }
Inclusive podemos utilizar este método en conjunto con anterior para ver su plan de ejecución: 1 > db.puntuaciones.find().hint({ ganadas : 1 }).explain() 2 3 { 4
"cursor" : "BtreeCursor ganadas_1",
5
"isMultiKey" : false,
6
"n" : 3,
7
"nscannedObjects" : 3,
8
"nscanned" : 3,
9
"nscannedObjectsAllPlans" : 3,
10
"nscannedAllPlans" : 3,
11
"scanAndOrder" : false,
12
"indexOnly" : false,
13
"nYields" : 0,
14
"nChunkSkips" : 0,
15
"millis" : 0,
16
"indexBounds" : {
17
"ganadas" : [
18
[
19
{
20
"$minElement" : 1
21
},
22
{
23
"$maxElement" : 1
24
}
25
]
26
]
27
},
28
"server" : "Mordor.local:27017"
29 }
También es posible forzar el desuso de índices indicando el operador $natural : 1 > db.puntuaciones.find({dinero : { $gt : 50 }}).hint({ $natural : 1 }).explain() 2 3 { 4
"cursor" : "BasicCursor",
5
"isMultiKey" : false,
6
"n" : 1,
7
"nscannedObjects" : 4,
8
"nscanned" : 4,
9
"nscannedObjectsAllPlans" : 4,
10
"nscannedAllPlans" : 4,
11
"scanAndOrder" : false,
12
"indexOnly" : false,
13
"nYields" : 0,
14
"nChunkSkips" : 0,
15
"millis" : 0,
16
"indexBounds" : {
17 18
},
19
"server" : "Mordor.local:27017"
20 }
Eliminación de índices Si consideramos que un índice ya no es necesario podemos eliminarlo manualmente de la siguiente manera: 1 > db.puntuaciones.dropIndex({ campo_que_deseo_eliminar_el_indice : 1 })
Incluso podemos eliminar todos los índices de la colección a excepción del _id por defecto de la siguiente manera: 1 > db.puntuaciones.dropIndexes()
Conclusión El uso de índices ayudan mucho en el rendimiento de una base de datos si son utilizados de la manera correcta, ahora con estos conocimientos puedes llevar un poco más allá tus estrategias de manipulación de datos para hacer tus aplicaciones dependientes de bases de datos MongoDB más rápidas y mejor organizadas.
MongoDB desde Cero: Autoincremento y Búsquedas Avanzadas La esencia de una base de datos es la capacidad de almacenar datos; sin embargo su propósito principal es obtener información específica basada en los parámetros que necesarios para un momento determinado, esto con la finalidad de no tener que recorrer manualmente todos los datos que poseemos para obtener lo que deseamos. Para ello, esta semana hablaremos de las búsquedas avanzadas y las secuencias auto-incrementadas.
Secuencias Auto-incrementadas Una de las necesidades con la cual nos hemos encontrado en algún punto al tener nuestro esquema de base de datos es la de poseer aquella estructura que permite asignar automáticamente el siguiente valor de la secuencia de un campo particular al insertar un nuevo registro. A esta funcionalidad se le conoce como secuencias auto-incrementadas, algunas bases de datos permiten establecer un campo con esta propiedad con tan solo definir una restricción o constraint; sin embargo el esquema de datos de MongoDB no adopta nativamente dicho aspecto pero permite su implementación siguiendo el patrón abajo descrito. Para entender el comportamiento supongamos el caso que tenemos una colección de autores y deseamos establecer el campo _id como auto-incrementado. Este patrón se basa en el uso de una colección y función auxiliar que permita llevar y obtener los valores siguientes de la secuencia incremental. Para esto primero crearemos una colección de contadores de la siguiente manera:
1 > var usuariosAutoincrement = { 2
_id:
'autoresid',
3
secuencia: 0
4} 5 6 > db.contadores.insert(usuariosAutoincrement)
Al especificar el campo secuencia como 0 indicará que la misma comenzará con este número. Ahora crearemos una función Javascript la cual se encargará de buscar el próximo número en la secuencia, veamos de que se trata: 1 > function proximoEnSecuencia(nombre){ 2
var resultado = db.contadores.findAndModify({
3
query: { _id: nombre },
4
update: { $inc: { secuencia: 1 } },
5
new:
6
true
});
7 8
return resultado.secuencia;
9}
Bien, veamos en detalle que hace nuestra función:
Sobre la colección contadores hacemos una búsqueda y actualización al mismo tiempo (findAndModify ). El parámetro query nos especifica qué documento de la colección contadores debemos buscar, es decir, aquel con el _id que se especifique como parámetro de la función. El parámetro update indica que luego de haber encontrado el documento en cuestión se debe incrementar ($inc) el campo secuencia en 1. El parámetro new le indica al método findAndModify que debe arrojar como resultado el nuevo documento en lugar del srcinal, es decir, aquel que ya ha sido actualizado con el incremento. El resultado de este método findAndModify es asignado a una variable y debido a que el resultado es un documento, podemos finalmente retornar el campo secuencia de dicho documento, el cual será el próximo número en la secuencia.
Ahora cuando queramos hacer uso de dicha función para que se encargue de asignar automáticamente el siguiente _id para nuestra colección de autores lo haremos de la siguiente manera: 1 > var oscar = { 2
_id:
3
nombre:
4
edad:
proximoEnSecuencia('autoresid'), 'Oscar',
25
5 }; 6 7 > var alberto = { 8
_id:
9
nombre:
10
edad:
proximoEnSecuencia('autoresid'), 'Alberto', 'veintiseis'
11 }; 12 13 > var jonathan = { 14
_id:
proximoEnSecuencia('autoresid'),
15
nombre:
16
apellido: 'Wiesel'
'Jonathan',
17 }; 18 19 > db.autoresAutoIncrement.insert(oscar); 20 > db.autoresAutoIncrement.insert(alberto); 21 > db.autoresAutoIncrement.insert(jonathan);
Probemos que en efecto nuestra solución ha hecho su trabajo: 1 > db.autoresAutoIncrement.find() 2 { "_id" : 1, "nombre" : "Oscar", "edad" : 25 } 3 { "_id" : 2, "nombre" : "Alberto", "edad" : "veintiseis" } 4 { "_id" : 3, "nombre" : "Jonath an", "apellido": "Wiesel" } 5 6 > db.contadores.find()
7 { "_id" : "autoresid", "secuencia" : 3 }
Notemos que nuestro autores han tomado su respectivo valor de la secuencia mientras que el documento de la secuencia como tal permanece actualizado con el último valor utilizado.
Selectores de búsqueda Como mencionamos anteriormente el poder que ofrece una base de datos reside en la capacidad que esta tiene para poder ofrecer los datos que necesitamos en un momento especifico según las necesidades que se nos presenten en dicha situación. Ciertamente vimos como filtrar las búsquedas en nuestra segunda entrada del curso; sin embargo en esta entrada veremos algo un poco m ás avanzado. Veamos algunas de las diferentes maneras de filtrar nuestras búsquedas haciendo uso de diferentes tipos de operadores o selectores de búsquedas. Adicionalmente notaremos que se enfoca a lo mismo que conocemos en SQL.
Comparativos mayor a X valor. mayor o igual a X valor. $lt – menor a X valor. $lte – menor o igual a X valor. $ne – distinto a X valor. $in – entre los siguientes [ X, Y, ... ] $nin no está entre los siguientes [ X, Y, ... ]
$gt –
$gte –
Los primeros 4 evidentemente están enfocados a valores numéricos y pueden ser utilizados de las siguiente manera: 1 > db.autoresAutoIncrement.find({ _id : { $gt : 1 } }) 2 { "_id" : 2, "nombre" : "Alberto", "edad" : "veintiseis" } 3 { "_id" : 3, "nombre" : "Jonath an", "apellido": "Wiesel" }
En SQL sería algo como
SELECT * FROM autoresAutoIncrement WHERE _id >
1
El operador $ne (distino de…) como podrás adivinar puede utilizarse para campos numéricos y no numéricos. Mientras que los últimos 2 operadores se enfocan en la comparación con arreglos de valores: 1 > db.autoresAutoIncrement.find({ nombre : { $in : ['Alberto', 'Ricardo', 'Oscar'] } }) 2 { "_id" : 1, "nombre" : "Oscar", "edad" : 25 }
3 { "_id" : 2, "nombre" : "Alberto", "edad" : "veintiseis" }
En SQL sería algo como
SELECT * FROM autoresAutoIncrement WHERE nombre in ('Alberto', 'Ricardo', 'Oscar')
Lógicos
$or
$and
$nor
$not
Estos operadores lógicos nos permiten juntar múltiples condiciones y dependiendo del cumplimiento de alguna de ellas ( $or), todas ellas ($and) o ninguna de ellas ($nor) obtendremos lo que deseamos, inclusive si lo que deseamos es completamente lo opuesto ( $not) a lo que especificamos como condición de búsqueda.
1 > db.autoresAutoIncrement.find({ $or : [{_id: 1}, {nombre: 'Jonathan'}] }) 2 { "_id" : 1, "nombre" : "Oscar", "edad" : 25 } 3 { "_id" : 3, "nombre" : "Jonath an", "apellido": "Wiesel" }
En SQL sería algo como
SELECT * FROM autoresAutoIncrement WHERE _id =
1 OR nombre = 'Jonathan'
En el caso del operador $and, si has prestado atención a lo largo de la serie te darás cuenta que MongoDB maneja implícitamente este tipo de operador, si no lo recuerdas puedes visitar el curso de operaciones básicas para refrescar la memoria. Para el operador $nor seguiríamos la misma notación que el ejemplo anterior con la diferencia que obtendríamos como resultado aquellos registros que ni tengan el _id = 1 ni el nombre = Jonathan. Por lo que obtendríamos a Alberto únicamente. Finalmente el operador $not actúa sobre el operador que le siga y como podrás imaginar, devolverá el resultado contrario. 1 > db.autoresAutoIncrement.find({ _id : { $not: { $gt: 2 }} }) 2 { "_id" : 1, "nombre" : "Oscar", "edad" : 25 } 3 { "_id" : 2, "nombre" : "Alberto", "edad" : "veintiseis" }
Al principio pensarás:
¿Por qué usar este operador si pude haber utilizado el $lte ? Una de las ventajas que quizás pasaste por alto es que suponiendo el caso donde dicho filtro se hace sobre otro campo distinto al de _id el cual no es obligatorio,
usar el operador $lte obtendrá aquellos documentos con el campo mayor o igual al valor indicado; sin embargo al utilizar el operador $not también obtendremos aquellos documentos que ni siquiera poseen el campo.
Elementales
$exists
$type
Este tipo de operadores elementales permiten hacer comparaciones referentes a las propiedades del campo como tal. En el caso de $exist , es un operador booleano que permita filtrar la búsqueda tomando en cuenta la existencia de un campo en particular: 1 > db.autoresAutoIncrement.find({ apellido: { $exists: true }}) 2 { "_id" : 3, "nombre" : "Jonath an", "apellido" : "Wiesel" }
Notaremos que hemos filtrado la búsqueda para que arroje únicamente los documentos que poseen el campoapellido . Para el caso de $type podemos filtrar por la propiedad de tipo de campo y como valor especificaremos el ordinal correspondiente a su tipo de dato BSON basado en lo siguiente:
1 – Double 2 – String 3 – Objeto 4 – Arreglo 5 – Data binaria 6 – Indefinido (deprecado) 7 – Id de objeto 8 – Booleano 9 – Fecha 10 – Nulo 11 – Expresión regular 13 – Javascript 14 – Símbolo 15 – Javascript con alcance definido 16 – Entero de 32bit 17 – Estampilla de tiempo 18 – Entero de 64bit 127 – Llave máxima
255 – Llave mínima 1 > db.autoresAutoIncrement.find({ edad: { $type: 1 }})
2 { "_id" : 1, "nombre" : "Oscar", "edad" : 25 } 3 4 > db.autoresAutoIncrement.find({ edad: { $type: 2 }}) 5 { "_id" : 2, "nombre" : "Alberto", "edad" : "veintiseis" }
Notemos que para el primer caso indicamos el tipo de campo Double en lugar de uno entero, esto se debe a que el único tipo de dato numérico nativo existente en Javascript es de tipoDouble y al ser insertado por la consola de MongoDB se torna en este tipo de dato.
Conclusión Hemos dado las herramientas para que puedas hacer los filtros que necesites para tu aplicación y finalmente lograr obtener la información específica necesaria para manejarla correctamente. Ten en cuenta que algunos ORM (modeladores de relaciones y objetos) utilizan una sintaxis parecida a la que vimos en esta entrada por lo que recordarla te podrá ayudar en el futuro para otras cosas. ¡Hasta la próxima!
MongoDB desde Cero: Seguridad Hemos progresado mucho desde que empezamos esta serie; sin embargo, antes de pensar en implementar este tipo de base de datos en un ambiente de producción debemos saber como protegerla para que no pueda ser violentada y la información que esta contiene no sea robada o alterada, para ello esta semana hablaremos sobre las consideraciones de seguridad a tomar en cuenta al usar MongoDB.
Autenticación El primer método de protección de la base de datos es quizás el más común. Evitar que cualquiera pueda acceder a la instancia mediante unas credenciales. El procedimiento es bastante intuitivo, debemos ingresar a la instancia, crear un usuario administrador y reiniciar dicha instancia. Veamos de que se trata: Primero ingresemos a la instancia y nos cambiaremos a la base de datos administradora que reside dentro de ella: 1 $ mongo 2 MongoDB shell version: 2.4.7 3 ... 4 > use admin 5 switched to db admin
Ahora creemos nuestro nuevo usuario administrador:
1 > db.addUser('jonathan','c0d3h3r0')
En la versión 2.6 el método addUser ha quedado desaprobado o deprecado, se debe usarcreateUser en su lugar (Fuente: Documentación de MongoDB). Bien, ahora en el archivo de configuración de la instancia habilitaremos la autenticación: 1 auth = true
Finalmente reiniciemos la instancia, volvamos a acceder a ella y tratemos de listar las bases de datos: 1 2 3 4 5
$ mongo MongoDB shell version: 2.4.7 ... > show dbs Sun Nov 10 17:14:05.505 listDatabases failed:{ "ok" : 0, "errmsg" : "unauthorized" } at src/mongo/shell/mongo.js:46
Si no recuerdas donde está el archivo de configuración para tu tipo de sistema o cómo reiniciar los servicios, vuelve a nuestra primera entrada de la serie. Notaremos que hemos recibido un error de unauthorized (no autorizado). Suministremos nuestras credenciales para poder trabajar con normalidad: 1 > use admin 2 switched to db admin 3 > db.auth('jonathan','c0d3h3r0') 41 5 > show dbs 6 admin 0.203125GB 7 codehero
0.203125GB
8 local 0.078125GB 9 test
0.203125GB
Como nuestras credenciales residen en la base de datos administrador, notemos que debimos cambiar a ella para autorizarnos. Si quisiéramos crear un usuario para una base de datos en particular, luego de estar autorizado como el usuario administrador nos cambiamos a la base de datos que queramos y creamos un usuario de la misma manera que antes. Este usuario solo podrá realizar operaciones sobre esta base de datos especificada:
1 ... 2 > use codehero 3 switched to db codehero 4 > db.addUser('blogAdmin','1234') 5 { 6
"user" : "blogAdmin",
7
"readOnly" : false,
8
"pwd" : "49e2220035d3d25cf3010bb9ff9f8ad9",
9
"_id" : ObjectId("5280083e4c857f20fa16e052")
10 }
Autorización Cuando estamos creando los usuarios sobre una base de datos es común que queramos definir ciertas restricciones para cada uno de ellos solo pueda realizar ciertas operaciones sobre la base de datos. A este conjunto de restricciones o privilegios se les conoce como Roles, veamos los diferentes tipos de roles que hay:
permite realizar operaciones de lectura sobre las colecciones que componen una BD. readWrite – operaciones de lectura y escritura sobre las colecciones de una BD. dbAdmin – permite realizar diversas tareas administrativas de una BD. userAdmin – ofrece acceso de lectura y escrituro a la colección de usuarios de una BD. read –
Múltiples bases de datos Estos roles permiten realizar las operaciones que se indicaron anteriormente pero para cualquier base de datos, por lo tanto deben ser definidas en la base de datos admin .
readAnyDatabase
readWriteAnyDatabase
userAdminAnyDatabase
dbAdminAnyDatabase
Administrativo
permite el acceso a diferentes operaciones referentes al sistema como tal, utilizado especialmente en estrategias de replicación y fragmentación. clusterAdmin –
Para definir los roles que un usuario puede tener, cuando estemos agregando el usuario lo haremos mediante un documento como este: 1 db.addUser({ 2
user: 'nombre_de_usuario',
3
pwd: 'contraseña',
4
roles: ['rol1','rol2', ... ]
5 }) 6
Exposición Otra manera de controlar el acceso a nuestra base de datos es manipulando los niveles a los que esta se encuentra expuesta la instancia, veamos algunas estrategias de como lograrlo.
Página de estatus Si accedes por medio del explorador de internet a la dirección donde se encuentra tu instancia de MongoDB especificando el puerto 28017 (por defecto), podremos notar una página como esta:
Evidentemente es una página que no queremos quede al descubierto en nuestro ambiente de producción ya que revela muchísima información sensible. Por esto, en nuestro archivo de configuración colocaremos lo siguiente: 1 nohttpinterface = true
Si deseas mantenerla disponible solo para aquellas personas en las que confías, es recomendable ajustar tus opciones en elfirewall para que información tan sensible no quede a la vista de todos.
Interface REST Si al entrar en la página anterior intentaste acceder a alguno de los vínculos de comandos o estatus, te habrás dado cuenta que ha ocurrido un error de que no tienes REST activado. Esta interface interactiva permite realizar algunas tareas administrativas que pueden ser de utilidad. Para activarlo, solo debemos colocar en nuestra archivo de configuración: 1 rest = true
Esta interface no implementa ningún tipo de autenticación por lo que se recomienda tener cuidado al ser expuesta.
Ligado de IPs Otra manera muy efectiva de limitar el acceso es usar el parámetro bind_ip , esto definirá qué direcciones IP pueden tener acceso a la instancia. Para activarlo basta con especificarlo en nuestro archivo de configuración: 1 bind_ip = 127.0.0.1 # suponiendo que solo quieremos que acceda algo en el mismo equipo 2 3 # o si queremos especificar varios, lo podemos hacer separándolos por coma: 4 5 bind_ip = 127.0.0.1, 192.168.0.105, 192.168.0.111
Esto no es una garantía absoluta ya que un especialista en seguridad podría hacerse pasar por una IP en la que tu instancia confía mediante lo que se conoce como IP Spoofing.
El puerto Quizás uno de los detalles más pasados por alto. Usar el puerto principal por defecto (27017) puede llevar a que cualquiera que sepa que estás utilizando MongoDB sabrá el puerto que debe atacar para hacerse con la base de datos. Es recomendable cambiar este puerto en un ambiente de producción, basta tan solo especificarlo en nuestro archivo de configuración: 1 port = 22622
En caso de cambiar el puerto de la instancia, la página de estatus se encontrará en el puerto especificado + 1000, en este caso se encontraría en el 23622.
Firewall Siendo el método más confiado, establecer políticas de firewall permite que únicamente aquellos que le sea permitido el acceso al servidor puedan acceder a la instancia de base de datos. En caso de instalar en un VPS, es probable que tengas alguna interfaz gráfica en el área de administración que te permita manipular las opciones del firewall. De lo contrario siempre puedes utilizar soluciones personalizadas de firewall como iptables en sistemas Linux, ipfw en sistemas Mac OS X / FreeBSD y netsh en sistemas Windows.
Conclusión Proteger los datos que residen en las bases de datos las cuales alimentan las aplicaciones que ofrecemos es un derecho y nuestro deber como desarrolladores ya que a nadie le gustaría que la información que le confiaste a una aplicación en particular ande rondando por ahí a la vista de todos. Ahora que sabes como hacerlo, estás un paso más cerca de ser un héroe en MongoDB.
MongoDB desde Cero: Colecciones Limitadas y Expiraciones Una base de datos puede crecer rápidamente para algunos casos de uso en los cuales se desea almacenar información histórica como eventos, bitácoras personalizadas, información de sesiones y otros. Más importante aun es que muchas veces dicha información solo nos es útil por un margen de tiempo determinado, luego terminan siendo desechos y a medida que pasa el tiempo pueden afectar el rendimiento. Por esta razón esta semana hablaremos de como mantener solo la información que necesitamos y aquella que es vieja, desecharla y que deje de ser parte de la base de datos.
Expiraciones o Tiempos de Vida (TTL) Esta funcionalidad nos permite establecer un tiempo de vigencia sobre los documentos de unaespecial colección. de vida mediante el uso de un índice conMongoDB el mismomaneja nombreestos (TTL tiempos index), uno de los hilos del proceso demongod se encarga de realizar las búsquedas necesarias usando estos índices con el fin de determinar aquellos documentos que cumplan con la condición de vida preestablecida y los elimina automáticamente. Además veremos las 2 maneras de establecer el vencimiento de un documento.
El proceso de búsqueda y eliminación es ejecutado cada 60 segundo aproximadamente, por lo que es posible que un documento continúe
siendo parte de la colección luego de su tiempo de vida preestablecido, adicionalmente se puede producir un retraso
dependiendo de la carga de trabajo que tenga la instancia en dicho momento. Existen algunas consideraciones respecto a los índices TTL que debemos tomar en cuenta cuando vayamos a implementar este tipo de solución.
Restricciones
Los índices TTL deben hacerse sobre campos que no posean otro índice. El campo a indexar debe ser de tipo fecha o un arreglo de fechas. En caso de tener un arreglo de fechas, el documento expirará cuando la menor fecha sea alcanzada. El índice TTL no puede ser compuesto.
Expiración en cuenta regresiva Esta estrategia se basa en definir la duración en segundos que tendrán de vigencia los documentos de una colección especifica. Para ello crearemos una colección con un par de documentos: > var fantasma1 = { 1 fecha:
new Date(),
2 mensaje:
3
'buuuu'
} 4 5 > var fantasma2 = { 6 fecha:
new Date(),
7 mensaje:
'no durare mucho tiempo'
8 } 9 10 > db.fantasmas.insert(fantasma1) 11 > db.fantasmas.insert(fantasma2) 12 13 > db.fantasmas.find() 14 { "_id" : ObjectId("52891f4d9f5ebcdb2a850063"), "fecha" : ISODate("2013-11-17T19:55:46.097Z"), "mensaje" : 15 "buuuu" } 16 { "_id" : ObjectId("52891f4f9f5ebcdb2a850064"), "fecha" : ISODate("2013-11-17T19:55:50.721Z"), "mensaje" : "no durare mucho tiempo" }
El comando new este momento.
Date()
creará un objeto tipo fecha con la fecha y hora de
Bien, ahora crearemos el índice TTL y especificaremos la duración de vigencia que deben tener los documentos. 1 > db.fantasmas.ensureIndex({ fecha : 1 }, { expireAfterSeconds : 300 })
En este caso especificamos que aquellos documentos que tengan en su campo fecha un valor mayor a 300 segundos (5 minutos) de antigüedad deben ser eliminados. Luego de que 5 minutos si volvemos buscar los documentos colección estoshayan ya nopasado existirán. Ten en cuenta que sia un documento no poseede el la campo fecha o si el campo no es de tipo date este simplemente no se vencerá.
Expiración en hora especifica Esta segunda estrategia se basa en la definición especifica en cada documento de cuando este debe vencer, esto nos permitirá establecer un comportamiento dinámico para cada documento y que cada uno pueda tener más o menos vigencia que aquellos que comparten la misma colección. Para demostrarlo crearemos un par de documentos con fechas diferentes, una con una fecha bastante próxima, digamos 5 minutos y otro para el año que viene, lo cual nos permitirá apreciar el comportamiento: 1 > var fantasma3 = { 2
fecha:
3
mensaje:
new Date('Nov 17, 2013. 16:00:00'), 'solo un susto y me voy'
4 } 5 6 > var fantasma4 = { 7
fecha:
8
mensaje:
new Date('Nov 17, 2014. 16:00:00'), 'estare aqui un largo tiempo'
9 } 10 11 > db.otrosFantasmas.insert(fantasma3) 12 > db.otrosFantasmas.insert(fantasma4) 13 14 > db.otrosFantasmas.find() 15 { "_id" : ObjectId("528927c79f5ebcdb2a85006a"), "fecha" : ISODate("2013-11-17T20:30:00Z"), "mensaje" : "solo un susto y me voy" } 16 { "_id" : ObjectId("5289281f9f5ebcdb2a85006b"), "fecha" : ISODate("2014-11-17T20:30:00Z"), "mensaje" :
"estare aqui un largo tiempo" }
Podemos notar que la hora que fue insertada no es la misma que la que indicamos, esto se debe a que la hora almacenada se encuentra en el horario GMT 0, como en Venezuela estamos en GMT-4:30 por ello se almacena la hora como a las 8:30pm en lugar de las 4:00pm Por último colocaremos el índice TTL pero en este caso colocaremos la “vigencia”
como cero, esto le indicará a MongoDB que debe vencer los documentos según la fecha indicada en el campo fecha de cada uno: 1 > db.otrosFantasmas.ensureIndex({ fecha : 1 }, { expireAfterSeconds : 0 })
Ahora si esperamos a que se cumple la hora que estipulamos y buscamos los documentos de nuestra colección veremos que el que se vencía próximamente ya no existe, y el del año que viene todavía lo sigue ahí: 1 2
> db.otrosFantasmas.find() { "_id" : ObjectId("5289281f9f5ebcdb2a85006b"), "fecha" : ISODate("2014-11-17T20:30:00Z"), "mensaje" : "estare aqui un largo tiempo" }
Colecciones Limitadas Este tipo de colecciones cumple el propósito de almacenamiento circular de documentos, es decir, la colección al alcanzar un tamaño determinado se empieza a sobreescribir desde el inicio, muy similar a lo que sería un buffer circular. Otra ventaja de las colecciones limitadas es su alto rendimiento de inserción ya que su información es almacenada (y devuelta al consultar) en el orden natural que fueron insertados. Sin embargo estas características no vienen sin sacrificio alguno, hay ciertos aspectos a considerar.
Restricciones
No se pueden eliminar documentos particulares. Al actualizar documentos ya existentes, estos no pueden crecer su tamaño en disco. TTL no es compatible con estas colecciones.
Esto viene dado por el hecho de que al declarar una colección limitada, se reserva un espacio en disco para ella y los documentos que contendrá, a su vez, como los documentos se almacenarán de manera continua en disco, esto limita que no se
puedan eliminar documentos ni incrementar su tamaño para que la estructura del espacio alocado no sea alterado.
Creación Para crear una colección limitada lo haremos de una manera un poco diferente a como estamos acostumbrados, ya que debemos especificar la capacidad que tendrá: 1 > db.createCollection( 'eventos', { capped: true, size: 1000000, max: 10000 } )
En este caso estamos creando una colección llamada eventos la cual especificamos que tiene que ser limitada ( capped), también delimitamos el espacio en disco (en bytes) que deberá ser alocado, en este caso un poco menos de 1MB,opcionalmente podemos además especificar la cantidad máxima de documentos que podrá contener esta colección, para este caso 10000 documentos.
Conversión Es posible convertir una colección normal a limitada sin necesidad de crearla desde cero, para esto debemos ejecutar el comando de conversión y especificar los límites: 1 > db.otrosFantasmas.isCapped() 2 false 3 4 > db.runCommand({"convertToCapped": "otrosFantasmas", size: 1000000}); 5 { "ok" : 1 } 6 7 > db.otrosFantasmas.isCapped() 8 true
Hemos convertido una colección con índices TTL a limitada, por lo tanto si ejecutamos el comando db.otrosFantasmas.getIndexes() notaremos que ya el índice no existe.
Conclusión Hemos visto como evitar que nuestra base de datos se llene con información que luego de un tiempo deja de ser relevante, esto es parte vital que impacta sobre el rendimiento de la misma ya que mientras más información existe, más tardarán las búsquedas en realizarse. Hasta la próxima semana.
MongoDB desde Cero: Respaldos y Restauración Como bien sabemos, parte importante del mantenimiento de una base de datos es la prevención al desastre por medio del respaldo de la información que esta contiene, de igual manera debemos estar preparados para saber como restaurar dicha información nuevamente. Veamos como llevar a cabo estas tareas.
Volcado (dump) MongoDB posee una herramienta muy útil que nos permite hacer un volcado de la información de la base de datos a un archivo de respaldo. Esta herramienta se llama mongodump , y se utiliza por medio de la consola o terminal de comandos. Un uso muy básico sería simplemente ejecutar: 1 $ mongodump
Esto se conecta a la instancia de Mongo que se encuentra ejecutandose en el mismo equipo, en el puerto por defecto 27017 y crea un archivo de respaldo de todas las bases de datos de la instancia (menos local) y lo almacena en un directoriodump/ de la ruta de donde se ejecutó el comando. Ciertamente podemos agregarle algunos parámetros a este comando para adaptarlo a nuestras necesidades:
se especifica un directorio distinto al por defecto dump/ para que se almacene el respaldo. --out –
se especifica un puerto, en caso que no se utilice el por defecto 27017 . --host – se especifica la dirección donde reside la instancia, en caso que no se utilice el por defecto localhost . --db – se especifica una base de datos particular en lugar de tomar todas. --collection – usado en conjunto con --db se especifica una colección particular que se quiera extraer de dicha base de datos. --dbpath – se especifica el directorio que contiene los archivos de las bases --port –
de datos. Esto es sumamente útil enacceder caso dedirectamente que el proceso de mongod no esté ejecutandose ya que podemos a sus archivos. --username y --password – permite especificar credenciales de usuario en caso de que estas sean requeridas.
Para nuestro ejemplo volquemos la información de la base de datos codehero : 1
$ mongodump --db codehero
2 3 4 5
connected to: 127.0.0.1 Fri Nov 29 23:17:51.202
DATABASE: codehero to
Fri Nov 29 23:17:51.203
codehero.system.indexes to dump/codehero/system.indexes.bson
6 Fri Nov 29 23:17:51.204 7 8 9 10 11 12 13 14
15 16 17 18
Fri Nov 29 23:17:51.204 Fri Nov 29 23:17:51.228
21
6 objects codehero.autores to dump/codehero/autores.bson 5 objects
Fri Nov 29 23:17:51.228
Metadata for codehero.autores to dump/codehero/autores.metadata.json
Fri Nov 29 23:17:51.229
codehero.system.users to dump/codehero/system.users.bson
Fri Nov 29 23:17:51.230
0 objects
Fri Nov 29 23:17:51.230
Metadata for codehero.system.users to dump/codehero/system.users.metadata.json
Fri Nov 29 23:17:51.230
codehero.fantasmas to dump/codehero/fantasmas.bson
Fri Nov 29 23:17:51.237
0 objects
Fri Nov 29 23:17:51.237
Metadata for codehero.fantasmas to dump/codehero/fantasmas.metadata.json
Fri Nov 29 23:17:51.245
codehero.otrosFantasmas to dump/codehero/otrosFantasmas.bson
Fri Nov 29 23:17:51.259
1 objects
Fri Nov 29 23:17:51.259 Metadata for codehero.otrosFantasmas to dump/codehero/otrosFantasmas.metadata.json
19 20
dump/codehero
$ cd dump/codehero ~/dump/codehero $ ls
22 23 autores.bson
fantasmas.metadata.json
system.indexes.bson
otrosFantasmas.bson
system.users.bson
24 autores.metadata.json 25 fantasmas.bson
otrosFantasmas.metadata.json system.users.metadata.json
Dicha base de datos debería estar presente y con algunas colecciones si le has seguido el paso a la serie.
Restauración El proceso de restauración es bastante similar al de volcado, el comando para dicha acción es mongorestore . Primero borremos nuestra base de datos codehero de la instancia local: 1 $ mongo 2 > use codehero 3 switched to db codehero 4 > db.dropDatabase() 5 { "dropped" : "codehero", "ok" : 1 } 6 > show dbs 7 admin 0.203125GB 8 codehero
(empty)
9 local 0.078125GB 10 test
0.203125GB
11 > exit
Notaremos que la base de datoselserespaldo encuentra vacía. Ahora probaremos restaurando quetotalmente hicimos anteriormente: 1 $ mongorestore --db codehero dump/codehero 2 3 connected to: 127.0.0.1 4 Fri Nov 29 23:33:07.464
dump/codehero/autores.bson
5 Fri Nov 29 23:33:07.464
going into namespace [codehero.autores]
6 5 objects found 7 Fri Nov 29 23:33:07.465
Creating index: { key: { _id: 1 }, ns: "codehero.autores", name: "_id_" }
8 Fri Nov 29 23:33:07.919
dump/codehero/fantasmas.bson
9 Fri Nov 29 23:33:07.919
going into namespace [codehero.fantasmas]
10 Fri Nov 29 23:33:07.999 1} 11
Created collection codehero.fantasmas with options: { "create" : "fantasmas", "flags" :
12 Fri Nov 29 23:33:07.999 file dump/codehero/fantasmas.bson empty, skipping Fri Nov 29 23:33:07.999 Creating index: { key: { _id: 1 }, ns: "codehero.fantasmas", name: "_id_" } 13 Fri Nov 29 23:33:07.999 Creating index: { key: { fecha: 1 }, ns: "codehero.fantasmas", name: "fecha_1", 14 expireAfterSeconds: 300 } 15 Fri Nov 29 23:33:08.002
dump/codehero/otrosFantasmas.bson
16 Fri Nov 29 23:33:08.002
going into namespace [codehero.otrosFantasmas]
17 Fri Nov 29 23:33:08.005 Created collection codehero.otrosFantasmas with options: { "create" : "otrosFantasmas", "capped" : true, "size" : 1000000 } 18 19 20 21 22 23
1 objects found Fri Nov 29 23:33:08.007
Creating index: { key: { _id: 1 }, ns: "codehero.otrosFantasmas", name: "_id_" }
Fri Nov 29 23:33:08.008
dump/codehero/system.users.bson
Fri Nov 29 23:33:08.009
going into namespace [codehero.system.users]
Fri Nov 29 23:33:08.051
file dump/codehero/system.users.bson empty, skipping
Fri Nov 29 23:33:08.051
Creating index: { key: { _id: 1 }, ns: "codehero.system.users", name: "_id_" }
Fri Nov 29 23:33:08.053 Creating index: { key: { user: 1, userSource: 1 }, unique: true, ns: "codehero.system.users", name: "user_1_userSource_1" }
Ahora entremos nuevamente a la instancia para verificar la información: 1 $ mongo 2 > show dbs 3 admin 0.203125GB 4 codehero
0.203125GB
5 local 0.078125GB 6 test
0.203125GB
7 > use codehero 8 switched to db codehero 9 > show collections 10 autores
11 fantasmas 12 otrosFantasmas 13 system.indexes 14 system.users 15 > db.autores.find() 16 { "_id" : ObjectId("5232344a2ad290346881464a"), "nombre" : "Jonathan", "apellido" : "Wiesel", "secciones" : 17 [ "Como lo hago", "MongoDB" ] } { "_id" : ObjectId("523236022ad290346881464b"), "nombre" : "Oscar", "apellido" : "Gonzalez", "secciones" : 18 [ "iOS", "Objective C", "NodeJS" ], "socialAdmin" : true } 19 { "_id" : ObjectId("5232383a2ad290346881464c"), "nombre" : "Alberto", "apellido" : "Grespan", "secciones" : "Git", "genero" : "M" } { "_id" : ObjectId("5246049e7bc1a417cc91ec8c"), "nombre" : "Ramses", "apellido" : "Velazquez", "secciones" : [ "Laravel", "PHP" ] }
Notaremos que la información efectivamente ha sido restaurada con éxito.
Migración Es posible que nos encontremos en la situación donde debamos migrar una base de datos de una instancia a otra, afortunadamente MongoDB hace este proceso sumamente sencillo. Como instancia destino usaré una máquina virtual de Vagrant, pero puedes probarlo con cualquier otro equipo con MongoDB que tengas a la mano.
Puedes volver a la primera entrada de la serie si quieres instalar MongoDB en tu equipo destino. Primero debemos acceder a la instancia de Mongo del equipo destino y tan solo haremos lo siguiente: 1 > db.copyDatabase('codehero','codeheroRemoto','192.168.0.100') 2 { "ok" : 1 } 3 4 > show dbs 5 codeheroRemoto 0.0625GB 6 local 0.03125GB 7 > use codeheroRemoto
8 switched to db codeheroRemoto 9 > show collections 10 autores 11 fantasmas 12 otrosFantasmas 13 system.indexes 14 system.users 15 > db.autores.find() 16 { "_id" : ObjectId("5232344a2ad290346881464a"), "nombre" : "Jonathan", "apellido" : "Wiesel", "secciones" : [ "Como lo hago", "MongoDB" ] } 17 { "_id" : ObjectId("523236022ad290346881464b"), "nombre" : "Oscar", "apellido" : "Gonzalez", "secciones" : 18 [ "iOS", "Objective C", "NodeJS" ], "socialAdmin" : true } 19 { "_id" : ObjectId("5232383a2ad290346881464c"), "nombre" : "Alberto", "apellido" : "Grespan", "secciones" : "Git", "genero" : "M" } { "_id" : ObjectId("5246049e7bc1a417cc91ec8c"), "nombre" : "Ramses", "apellido" : "Velazquez", "secciones" : [ "Laravel", "PHP" ] }
Recuerda que si tienes la opción
bind_ip
asignada a una IP particular en
tu archivo de configuración de la instancia de srcen (generalmente localhost ), solo esa IP podrá acceder a ella y efectivamente bloqueará las conexiones para copiar una base de datos a otra instancia. Notemos que el comando copyDatabase recibió como opciones:
Base de datos srcen. – codehero Base de datos destino. – codeheroRemoto Dirección de instancia de srcen – 192.168.0.100 Podriamos concatenarle el puerto de ser necesario –
192.168.0.100:27017
En caso que la base de datos tuviese restringido el acceso por autorización de usuario podríamos pasarle un par de opciones más con el nombre de usuario y clave.
Conlusión Hemos aprendido como manipular los respaldos de una base de datos de MongoDB y ya estamos en capacidad de migrar su información de manera sencilla. Debemos resaltar que estas son solo algunas a de las medidas de prevención a tomar, más adelante veremos algunas más avanzadas y como responder a una situación catastrófica.
MongoDB desde Cero: Replicación– Parte I Una de las técnicas utilizadas ampliamente en sistemas de producción es la replicación de los datos a través de distintas instancias y computadores para asegurar que la información esté siempre disponible y reducir los riesgos de pérdida o corrupción de la misma. Esta semana veremos como encargarnos de esto en MongoDB.
Funcionamiento conceptual El principal propósito de la implementación de estrategias de replicación de datos es incrementar la redundancia de los mismos, esto nos permite tener una base de datos de alta disponibilidad e integridad. Una base de datos al tener varias copias exactas en diferentes infraestructuras separadas asegura que si una de las instancias falla, ya sea una falla catastrófica a nivel de hardware o situaciones diversas que pudiesen corromper y/o evitar el acceso a la data, el sistema que dicha base de datos alimente no se vea afectado ya que existen otras instancias espejo que tomarán el lugar de la srcinal mediante un proceso transparente para los usuarios finales.
Arquitectura En MongoDB, al grupo de instancias que poseerán la misma información se les denomina replica set o grupo de replicación. Un replica set en MongoDB está compuesto por 2 tipos principales de miembros, instancias primarias y secundarias, teniendo una única instancia primaria la cual aceptará todas las operaciones de escritura provenientes de los sistemas cliente. Las operaciones de lectura son dirigidas por defecto a la instancia primaria; sin embargo es posible configurar la lectura a instancias secundarias.
Estas operaciones que alteran los datos son escritas en un archivo llamado oplog o bitácora de operaciones, los miembros secundarios replican este archivo del miembro primario y ejecutan las mismas operaciones sobre su conjunto de datos, esto permite tener la misma información en las diferentes instancias.
Debemos tomar en cuenta que debido a que el procedimiento de replicación se realiza de manera asíncrona, es posible que clientes que consulten directamente a miembros secundarios no obtengan la información más reciente.
Tipos de miembros secundarios Existen 3 configuraciones especiales para los miembros secundarios, esto nos permitirá delimitar su funcionamiento para un fin específico.
Mie m bro d e prioridad 0 Se le denomina a un miembro secundario configurado con prioridad 0, esto evitará que dicho miembro pueda ser elegido a convertirse en miembro primario en caso de que aquel falle.
Mi embro escondido Son miembros secundarios de tipo prioridad 0 pero que además se les niega la posibilidad de ser accesibles para lectura por parte de los sistemas cliente.
Mie m bro retra sado También son miembros de prioridad 0 y poseen la cualidad particular de mantener un estado retrasado de la base de datos, suele utilizarse como instancias de respaldo ya que no han sido afectadas por las últimas operaciones que pudiesen estar alterando de manera no deseada la información. Debido al estado retrasado de este miembro se recomienda que también se defina como un miembro escondido.
Elecciones La arquitectura de los replica set dicta que los miembros deben enviar latidos o pings entre ellos cada 2 segundos, si en un período de 10 segundos el latido no es devuelto, se marca al miembro en cuestión como inaccesible.
Llamar a elección Una elección se lleva a cabo cuando no existe un miembro primario, este deja de responder o este es obligado a darse de baja, veamos las diferentes condiciones:
La iniciación de un nuevo replica set. Un miembro secundario pierde contacto con el primario. El miembro primario es obligado a convertirse en secundario. Si un secundario es elegible para elecciones y posee un mayor indice de prioridad.
Prioridad La elección de un nuevo miembro primario se basa inicial y principalmente en la comparación de prioridades de aquellos miembros elegibles, esta prioridad es por defecto 1, esto para darles a todos los miembros la posibilidad de ser elegidos. Si un miembro de mayor prioridad alcanza a tener al día su información con al menos 10 segundos con respecto al oplog, se declara una elección para darle a dicho nodo de mayor prioridad la oportunidad de convertirse en primario.
Recuerda que los miembros con prioridad 0 no pueden ser elegidos como primarios.
Optime Se toma en cuenta la estampilla de tiempo más reciente de la última operación que el miembro aplicó del oplog, por ello se llama optime o tiempo de operación.
Conectividad Otro aspecto a considerar es la capacidad que tiene el candidato para conectarse con la mayoría de de los miembros en el grupo.
En este caso particular se le llama “miembros” a aquellos que poseen la
capacidad para votar. Al configurar un miembro del replica set se puede indicar si a este se le permite votar o no y cuantos votos posee cada uno.
Negaciones en elecciones Cualquier miembro del replica set puede vetar o negar una elección (incluyendo aquellos miembros que no votan) si ocurre alguna las siguientes situaciones:
Si el miembro que busca la elección no es parte del grupo de votantes. Si el miembro que busca la elección no está al día con las operaciones más recientes en el oplog que son accesibles por el replica set. Si el miembro que busca la elección posee una prioridad menor a algún otro miembro que también sea elegible.
Si el miembro primario para el momento posee un optime mayor o igual que aquel que busca la elección (desde el punto de vista del miembro votante).
Consideraciones Existen varios aspectos a considerar cuando se desea implementar una estrategia de replicación, varias de ellas aplican a sistemas de bases de datos comunes.
Miembros impares Procura que la cantidad de miembros en tu replica set sea impar, esto facilitará los procesos de elecciones y solicitará menos recursos y menos tiempo. Para estos casos es posible configurar un miembro árbitro. Este tipo de miembros no poseen una copia del conjunto de datos y por ende no pueden volverse primarios. Pueden ser de gran utilidad cuando no posees la infraestructura para soportar un miembro más pero deseas seguir el estándar de miembros impares ya que la cantidad de recursos que necesita es mucho menor.
Número de miembros Un replica set puede tener un máximo de 12 miembros y solo 7 de ellos con la capacidad de votar. En caso de que tu solución necesite un mayor número de miembros es posible implementar la estrategia precedente a los replica set conocida comoreplicación maestro-esclavo. Se debe tomar en cuenta que esta estrategia ofrece menos redundancia y no soporta el proceso automático de recuperación a fallas como lo hacen los replica set con los latidos y elecciones.
Distribución geográfica Procura subdividir los miembros de tu replica set en distintos centros de datos, esto asegurará que si algo le sucede al centro de datos, tu base de datos se mantendrá activa. De igual manera trata de mantener una mayoría en uno de los centros para garantizar que los miembros puedan elegir a un primario en caso de que la comunicación entre otros miembros localizados en otro centro se vea dificultado. Inclusive puedes tener un miembro tipo prioridad 0 en un centro de datos aparte como respaldo.
Balanceo de carga Como mencionamos inicialmente, es posible configurar el replica set para soportar la lectura de miembros secundarios, esto permitirá balancear la carga en ambientes que puedan encontrarse bajos altos niveles de estrés y concurrencia.
Conclusión Esta semana hemos visto mucha teoría de lo que amerita implementar una estrategia de replicación para nuestra base de datos en MongoDB, ciertamente es mucho que digerir pero no te preocupes que la semana que viene llevaremos esto a la práctica para que lo puedas ver con mayor facilidad. Sin embargo nos vemos en la necesidad de tocar todos estos aspectos debido a que es parte clave de lo será tu ambiente de producción final.
MongoDB desde Cero: Replicación– Parte II La semana pasada aprendimos la teoría de replicación en MongoDB mejor conocido como replica set. Ahora estamos listos para tomar esos conocimientos y llevarlos a la práctica.
Convertir a Replica Set Uno de los casos de uso comunes al implementar estrategias de replicación es la de primero trabajar con una instancia independiente y luego convertirla a replica set. Veamos lo que debemos hacer para llevar a cabo este sencillo proceso: Primero debemos detener la instancia de mongod . Luego debemos especificar el nombre del replica set que será formada, para ello podemos especificarlo en el archivo de configuración como: 1 replSet = nombre_del_replica_set
O si lo prefieres puedes pasarlo como argumento al comando de ejecución de la instancia cuando no se ejecuta como un servicio: 1 mongod --replSet nombre_del_replica_set
Posteriormente al levantar la instancia de mongod , entraremos al la consola de mongo: 1 $ mongo
2 ... 3 > rs.status() 4 { 5
"startupStatus" : 3,
6
"info" : "run rs.initiate(...) if not yet done for the set",
7
"ok" : 0,
8
"errmsg" : "can't get local.system.replset config from self or any seed (EMPTYCONFIG)"
9 } 10 > rs.conf() 11 null
El comando rs.status() nos indica el estado actual del replica set, en este caso podemos observar que en efecto a la instancia le ha sido indicada que debe trabajar como un replica set; sin embargo esta no ha sido iniciada y por eso no tiene asignada ninguna configuración (accesible con el comando rs.config() ). Iniciemos el replica set ejecutando el comando rs.initiate() : 1 > rs.initiate() 2{ 3
"info2" : "no configuration explicitly specified -- making one",
4
"me" : "Mordor.local:27017",
5
"info" : "Config now saved locally. Should come online in about a minute.",
6
"ok" : 1
7}
Para este caso mongod creará una configuración sencilla base para el replica set. Vemos lo que esta iniciación ha logrado: 1 miRS:PRIMARY> rs.status() 2 { 3
"set" : "miRS",
4
"date" : ISODate("2013-12-15T15:43:27Z"),
5
"myState" : 1,
6
"members" : [
7 8
{ "_id" : 0,
9
"name" : "Mordor.local:27017",
10
"health" : 1,
11
"state" : 1,
12
"stateStr" : "PRIMARY",
13
"uptime" : 401,
14
"optime" : Timestamp(1387122082, 1),
15
"optimeDate" : ISODate("2013-12-15T15:41:22Z"),
16
"self" : true
17
}
18
],
19
"ok" : 1
20 } 21 miRS:PRIMARY> rs.conf() 22 { 23
"_id" : "miRS",
24
"version" : 1,
25
"members" : [
26
{
27
"_id" : 0,
28
"host" : "Mordor.local:27017"
29 30
} ]
31 }
Ahora la consola de mongo nos indica el miembro especifico sobre el cual estamos ejecutando los comandos, en este caso sobre el primario ( PRIMARY ) del replica set de nombre miRS. Podemos notar también mucha información con respecto a los miembros del replica set como la estampilla de tiempo de la última operación realizada (optime), su estado funcional, su función dentro del replica set, entre otros.
Agregar miembros Ya hemos convertido una instancia independiente en un replica set; sin embargo no nos sirve de nada una solución de este tipo con un solo miembro, para demostrar esto utilizaré una instancia de Ubuntu con Vagrant, pero si lo deseas también puedes utilizar otros equipos que tengas a la mano o incluso levantar otras instancias de mongod en tu mismo equipo utilizando archivos de configuración diferentes o puertos distintos. Lo primero que debemos hacer es obviamente tener instalado MongoDB en nuestro equipo secundario. Luego debemos asegurarnos de que el directorio de data de dicha instancia esté vacía ya que este miembro copiará toda la información del miembro primario.
También puedes copiar manualmente la información del miembro primario, esto reducirá el tiempo de preparación de este miembro secundario. Ahora en nuestro equipo secundario debemos indicarle el nombre del replica set de la misma manera que lo hicimos con la primaria, indicandolo en su archivo de configuración o al levantar manualmente la instancia. (en este caso utilizamos el nombre miRS) 1 vagrant@precise32:~$ sudo nano /etc/mongodb.conf 2 3 # mongodb.conf 4 ... 5 # in replica set configuration, specify the name of the replica set 6 replSet = miRS
Reiniciamos la instancia: 1 vagrant@precise32:~$ sudo service mongodb restart 2
* Restarting database mongodb
3 vagrant@precise32:~$ mongo 4 MongoDB shell version: 2.4.8 5 ... 6 > rs.status() 7 { 8
"startupStatus" : 3,
9
"info" : "run rs.initiate(...) if not yet done for the set",
[ OK ]
10
"ok" : 0,
11
"errmsg" : "can't get local.system.replset config from self or any seed (EMPTYCONFIG)"
12 }
En este caso no ejecutaremos rs.inititate() ya que el replica set se encuentra iniciado por otro lado y este miembro será uno que agregaremos a este ya existente. mongod para poder Tomemos nota del host agregarla al replica set:donde se encuentra esta instancia de 1 vagrant@precise32:~$ ifconfig 2 eth1
Link encap:Ethernet HWaddr **:**:**:**:**:**
3
inet addr:192.168.33.10 Bcast:192.168.33.255 Mask:255.255.255.0
4
...
Ahora volveremos a nuestra instancia primaria para agregar el nuevo miembro al replica set: 1 miRS:PRIMARY> rs.add('192.168.33.10:27017') 2 { "ok" : 1 }
Y si esperamos un poco a que MongoDB haga su magia podremos notar algo como esto: 1 miRS:PRIMARY> rs.status() 2 { 3
"set" : "miRS",
4
"date" : ISODate("2013-12-15T17:40:32Z"),
5
"myState" : 1,
6
"members" : [
7
{
8
"_id" : 0,
9
"name" : "Mordor.local:27017",
10
"health" : 1,
11
"state" : 1,
12
"stateStr" : "PRIMARY",
13
"uptime" : 2010,
14
"optime" : Timestamp(1387129181, 1),
15
"optimeDate" : ISODate("2013-12-15T17:39:41Z"),
16
"self" : true
17
},
18
{
19
"_id" : 1,
20
"name" : "192.168.33.10:27017",
21
"health" : 1,
22
"state" : 2,
23
"stateStr" : "SECONDARY",
24
"uptime" : 51,
25
"optime" : Timestamp(1387129181, 1),
26
"optimeDate" : ISODate("2013-12-15T17:39:41Z"),
27
"lastHeartbeat" : ISODate("2013-12-15T17:40:31Z"),
28
"lastHeartbeatRecv" : ISODate("2013-12-15T17:40:30Z"),
29
"pingMs" : 1,
30
"lastHeartbeatMessage" : "syncing to: Mordor.local:27017",
31
"syncingTo" : "Mordor.local:27017"
32
}
33
],
34
"ok" : 1
35 } 36 miRS:PRIMARY> rs.conf() 37 { 38
"_id" : "miRS",
39
"version" : 2,
40
"members" : [
41
{
42
"_id" : 0,
43
"host" : "Mordor.local:27017"
44
},
45
{
46
"_id" : 1,
47
"host" : "192.168.33.10:27017"
48 49
} ]
50 }
En mi caso tuve que colocar en el archivo /etc/hosts de mi equipo secundario la asociación del host Mordor.local a la IP de mi equipo principal 192.168.0.100 , ya que de lo contrario el nuevo miembro no podría resolver ese nombre a nivel de DNS para lograr conectarse con el miembro principal.
Agregar árbitro Si quisiéramos agregar un árbitro ejecutaríamos en lugar de rs.add(..) , el comando rs.addArb(...) . Recuerda que el directorio especificado para este miembro donde se almacenaría la data será únicamente utilizado para almacenar configuración, NO el conjunto de datos, ya que los árbitros no poseen una copia del conjunto de datos.
Configuración de miembros Como vimos la semana pasada existen varios tipos de miembros secundarios además de algunas consideraciones especiales que se pueden especificar para los miembros del replica set, si recordamos bien, delimitar estas funcionalidades se basan en una sencilla configuración del miembro para el fin especifico.
Estas configuraciones deben realizarse desde el miembro primario. Configurarlo es muy sencillo, hagamos uso de nuestros conocimientos de Javascript para esto. Veamos el último comando que ejecutamos: 1 miRS:PRIMARY> rs.conf() 2
{
3
"_id" : "miRS",
4
"version" : 2,
5
"members" : [
6
{
7
"_id" : 0,
8
"host" : "Mordor.local:27017"
9
},
10
{
11
"_id" : 1,
12
"host" : "192.168.33.10:27017"
13
}
14 15
] }
Es aquí donde debemos definir la configuración para cada miembro. Será tan fácil como asignarle dicho comando a una variable y empezaremos a manipular el objeto como lo haríamos normalmente en Javascript: 1 config = rs.conf()
Pongamos como ejemplo la configuración de un miembro retrasado el cual sabemos ahora que es un miembro de prioridad 0, que debe ser además un miembro escondido y posee un tiempo de retraso determinado, hagamos esto con nuestro miembro secundario: 1 config.members[1].priority = 0 2 config.members[1].hidden = true 3 config.members[1].slaveDelay = 3600
Si quisieramos podríamos definir también la cantidad de votos que puede tener este miembro para determinar su influencia en elecciones con el atributo votes . Y ahora solo reconfiguramos el replica set de la siguiente manera : 1 rs.reconfig(config) 2 ... 3 miRS:PRIMARY> rs.conf() 4 { 5
"_id" : "miRS",
6
"version" : 5,
7
"members" : [
8
{
9
"_id" : 0,
10
"host" : "Mordor.local:27017"
11
},
12
{
13
"_id" : 1,
14
"host" : "192.168.33.10:27017",
15
"priority" : 0,
16
"slaveDelay" : 3600,
17
"hidden" : true
18 19
} ]
20 }
Muy bien ahora ya tenemos configurado un miembro retrasado en nuestro replica set. También puedes configurar directo el miembro cuando lo estás agregando al replica set especificando los parámetros directamente de la siguiente manera: 1 rs.add( { _id: 1, host: '192.168.33.10:27017', priority: 0, hidden: true, slaveDelay: 3600 } )
Eliminación de miembros Supongamos el caso que deseamos eliminar uno de los miembros del replica set. Si dicho miembro es el primario debemos primero relevarlo de su cargo y dejar que un nuevo primario sea elegido. Para esto ejecutaríamos el comando rs.stepDown() en el miembro primario, esto lo forzará a ceder su papel como primario y evitará ser elegido en la siguiente elección durante la cantidad de segundos indicada. Posteriormente podremos eliminar un miembro desde el primario de la siguiente manera: 1 rs.remove('192.168.33.10:27017')
Si revisamos el estado y configuración del replica set luego de esto, podremos ver que en efecto esa instancia ya no forma parte de la misma.
1 miRS:PRIMARY> rs.status() 2 { 3
"set" : "miRS",
4
"date" : ISODate("2013-12-15T19:57:23Z"),
5
"myState" : 1,
6
"members" : [
7
{
8
"_id" : 0,
9
"name" : "Mordor.local:27017",
10
"health" : 1,
11
"state" : 1,
12
"stateStr" : "PRIMARY",
13
"uptime" : 10221,
14
"optime" : Timestamp(1387137431, 1),
15
"optimeDate" : ISODate("2013-12-15T19:57:11Z"),
16
"self" : true
17
}
18
],
19
"ok" : 1
20 } 21 22 miRS:PRIMARY> rs.conf() 23 { 24
"_id" : "miRS",
25
"version" : 6,
26
"members" : [
27
{
28
"_id" : 0,
29
"host" : "Mordor.local:27017"
30 31
} ]
32 }
De igual manera si accedemos a nuestro antiguo miembro podremos ver que se encuentra con estado REMOVED : 1 miRS:REMOVED> rs.status() 2 { 3
"set" : "miRS",
4
"date" : ISODate("2013-12-15T20:07:29Z"),
5
"myState" : 10,
6
"members" : [
7
{
8
"_id" : 1,
9
"name" : "192.168.33.10:27017",
10
"health" : 1,
11
"state" : 10,
12
"stateStr" : "REMOVED",
13
"uptime" : 8811,
14
"optime" : Timestamp(1387130876, 1),
15
"optimeDate" : ISODate("2013-12-15T18:07:56Z"),
16
"self" : true
17
}
18
],
19
"ok" : 1
20 }
Convertir miembro en independiente Para utilizar este antiguo miembro secundario como una instancia aislada nuevamente podemos volver a ejecutar el comando de inicio de la instacia sin el parámetro --replSet o eliminarlo del archivo de configuración (dependiendo de cómo hayas decidido iniciar la instancia de mongod ).
Luego reiniciemos la instancia y borraremos los rastros del replica set al borrar la base de datos local donde se almacena la información de la misma: 1 vagrant@precise32:~$ sudo nano /etc/mongodb.conf 2 ... 3 # mongodb.conf 4 ... 5 # in replica set configuration, specify the name of the replica set 6 # replSet = miRS #eliminamos o comentamos esta linea 7 ... 8 9 vagrant@precise32:~$ sudo service mongodb restart 10 * Restarting database mongodb
[ OK ]
11 vagrant@precise32:~$ mongo 12 ... 13 > use local 14 switched to db local 15 > db.dropDatabase() 16 { "dropped" : "local", "ok" : 1 }
Conclusión Ya sabemos como tener un cluster de replicación en MongoDB, esto nos permitirá tener una alta disponibilidad de los datos y aseguraremos la durabilidad de los mismos por el incremento de la redundancia. De igual manera estaremos protegidos en caso que sucedan situaciones catastróficas inesperadas. Incluso podrías configurar en el driver de MongoDB de tu aplicación cliente para que lea de los miembros secundarios en caso de que sea necesario. Más adelante llevaremos el concepto de clusterización mucho más lejos cuando hablemos de fragmentación. Hasta entonces.
MongoDB desde Cero: Fragmentación – Parte I En las entradas pasadas hemos iniciado a hablar de temas de clusterización, es decir, poseer varias instancias para escalar nuestra solución de base de datos. Sin embargo podemos llevar nuestro concepto de cluster más alla de lo que hemos visto con los replica sets al repartir información entre diferentes instancias, por ello esta semana hablaremos de la fragmentación de datos en MongoDB.
Propósito Si estas desarrollando un servicio que se va haciendo popular o los niveles de acceso a base de datos son cada vez más altos, empezarás a notar que tu base de datos está siendo martillada por el exceso de tráfico y tu servidor esté sufriendo por los altos niveles de procesamiento continuo y te podrías ver en la necesidad de actualizar tu infraestructura para soportar la demanda. Entra en juego la fragmentación de datos, esta permite separar las colecciones por conjuntos de documentos en diferentes instancias o fragmentos. Esta estrategia te permite escalar tu base de datos horizontalmente al agregar más equipos para repartir la información en lugar de obligar a mejorar el que tienes.
La mayoría de las veces resulta más costoso tener un único computador de altas capacidades que varios de gama inferior. Por lo tanto si tenemos una colección muy grande, digamos de 1TB por ejemplo, resultaría prudente particionarla en diferentes fragmentos, digamos 5, para que la información de dicha colección pueda ser distribuida en 200GB entre cada uno de ellos, esto a su vez distribuye la carga a nivel de procesamiento.
En MongoDB la unidad de base de datos que se fragmenta son las colecciones. Por lo tanto una colección que sea declarada como fragmentada podría poseer distintos documentos en los fragmentos del cluster.
Un único documento nunca estará repartido entre fragmentos. Un documento puede tener un tamaño máximo de 16MB, en caso de necesitar mayor tamaño para un documento se necesitaría implementar la solución de GridFS el cual separa el documento en varios trozos ochunks.
Arquitectura Un cluster de fragmentación suele poseer una arquitectura como esta:
Como puedes notar existen 4 componentes claves de la arquitectura. Hablemos un poco sobre cada uno de ellos:
Aplicación y Driver Las aplicaciones cuando necesitan comunicarse con la base de datos de MongoDB lo hacen a traves de undriver, estos tienen implementados los métodos
y protocolos necesarios para comunicarse correctamente con la base de datos encapsulando la complejidad del proceso al desarrollador.
Fragmento Un fragmento o shard es aquel que posee los datos f ragmentados de las colecciones que componen la base de datos como tal, este suele estar compuesto por un replica set preferiblemente; sin embargo en ambientes de desarrollo podría ser una única instancia por fragmento.
Router
Debido a que las aplicaciones ven la base de datos como un todo, el router es el encargado de recibir las peticiones y dirigir las operaciones necesarias al fragmento o fragmentos correspondiente(s).
En ambientes de producción es común tener varios routers para balancear la carga de peticiones de los clientes.
Servidores de configuración Este tipo de instancias se encargan de almacenar la metadata del cluster de fragmentación, es decir, qué rangos definen un trozo de una colección y qué Esta inf es almacenada en trozos se el encuentran quéun fragmento. caché por router paraen lograr óptimo tiempo deormación procesamiento. En ambientes de producción se deben tener 3 servidores de configuración ya que si solo se posee uno y este falla, el cluster puede quedar inaccesible.
Escoger llave de fragmentación MongoDB separa una colección en los trozos correspondientes para repartir a los diferentes por medio de una llave fragmentación. Esta llave vienelas siendo unofragmentos de los campos perteneciente a losde documentos el cual debe poseer siguientes características:
Cardinalidad y Divisibilidad Una llave de fragmentación debe tener una alta cardinalidad para asegurar que los documentos puedan ser efectivamente divididos en los distintos fragmentos, es decir, suponiendo que escogemos una llave de fragmentación que posee solo 3 valores posibles y tenemos 10 fragmentos, no podríamos separar los documentos en los 10 fragmentos al solo tener 3 valores posibles para separar. Mientras más
valores posibles pueda tener la llave de fragmentación será más fácil y eficiente la separación de los trozos en los fragmentos.
Incluso si solo tienes 3 fragmentos puedes correr el riesgo al no cumplir la característica que veremos a continuación.
Aleatoriedad Adicionalmente es muy importante que la llave de fragmentación posea un alto nivel de aleatoriedad, esto se debe a que si utilizamos una llave que siga un patrón incremental como una fecha o un ID, traerá como consecuencia que cuando estemos insertando documentos, el mismo fragmento estará siendo utilizando constantemente durante el rango de valores definido para él, esto sin duda mantendrá los datos separados óptimamente pero pondrá siempre bajo estrés a un fragmento en lapsos de tiempo mientras que los otros posiblemente queden con muy poca actividad (a este comportamiento se le conoce como hotspotting ).
Para casos donde los campos de tus documentos se ven limitados para cumplir con estas condiciones, es posible tener una llave de fragmentación compuesta. Incluso es posible escoger un campo que siga patrones incrementales y utilizarlo como llave de fragmentaciónhasheada , lo cual creará un hash del valor del campo y esto logrará que tenga un alto nivel de aleatoriedad. Adicionalmente debemos recalcar que una llave de fragmentación siempre deberá poseer un índice, de lo contrario el rendimiento del sistema no sería muy bueno y se estaría sacrificando a costas de poder escalar nuestro sistema. Normalmente si dicho campo no posee un índice, la tratar de agregar el fragmento al cluster MongoDB te obligará a crearlo o éste lo creará por ti.
Conclusión Al igual que cuando hablamos de replicación, esto es un tema con mucha teoría y aspectos a considerar por lo que dejaremos la parte práctica para la semana siguiente. Como estarás notando esta estrategia de clusterización tiene mucho que ofrecer y verás no es tan difícil de implementar aunque se deben tomar en consideración muchos aspectos y la arquitectura es un poco más compleja de lo que estamos acostumbrados, hasta la semana que viene.
MongoDB desde Cero: Fragmentación – Parte II La semana pasada comenzamos a hablar sobre la fragmentación de datos en MongoDB, vimos cómo nos ayuda a escalar nuestra solución de almacenamiento y sus diferentes ventajas. Además conocimos gran parte de la materia teórica que esto implica. De seguro estás ansioso por poner todo ello en práctica, por eso está semana nos ponemos en acción para aplicar lo aprendido y armaremos nuestro propio cluster de fragmentación.
Creando un cluster de fragmentación… Es hora de ponernos a trabajar para crear nuestro primer cluster de f ragmentación, por razones de facilidad educativa crearemos todo el cluster en el mismo equipo, para ello solo deberemos crear cada instancia en un puerto distinto.
Servidores de configuración Comencemos creando nuestros servidores de configuración que según vimos deben ser 3: 1 $ mkdir configServer1 2 $ mkdir configServer2 3 $ mkdir configServer3 4
5 $ mongod --configsvr --dbpath configServer1 --port 27019 --fork 6 $ mongod --configsvr --dbpath configServer2 --port 27020 --fork 7 $ mongod --configsvr --dbpath configServer3 --port 27021 --fork
La opción --fork ejecutará en el fondo a la instancia para que el comando regrese al terminal en lugar de quedarse escuchando al servidor. Para crear nuestro router debemos pasarle como parámetro los hostnames de cada servidor de configuración, para ello entraremos a cualquiera de los que acabamos de crear y tomaremos nota de él: 1 $ mongo --port 27019 2 ... 3 configsvr> hostname() 4 Mordor.local
Ya que todas las instancias se encuentran en el mismo equipo, los hostnames son todos iguales y lo único que cambia son los puertos.
Routers Bien, ahora crearemos nuestro enrutador. Estos a diferencia de todos los otros tipos de componentes, son instanciasmongos en lugar de mongod. Le debemos pasar una cadena de caracteres con las direcciones de los servidores de configuración: 1
$ mongos --configdb Mordor.local:27019,Mordor.local:27020,Mordor.local:27021 --port 27030 --fork --logpath routerLog
En ambientes de producción se recomienda que se tengan múltiples instancias enrutadoras, esto evitará que se forme un cuello de botella a nivel de acceso de las aplicaciones. Un buen número para tomar como referencia es uno por fragmento, y distribuidos de manera acorde.
Fragmentos Ahora debemos crear nuestras instancias fragmentos, en ambientes productivos se recomienda ampliamente que cada fragmento sea un replica set pero para no hacer esta entrada tan larga y posiblemente confusa utilizaremos una única instancia por fragmento: 1 $ mkdir shard1 2 $ mkdir shard2 3 $ mkdir shard3
4 5 $ mongod --shardsvr --dbpath shard1 --port 27040 --fork 6 $ mongod --shardsvr --dbpath shard2 --port 27041 --fork 7 $ mongod --shardsvr --dbpath shard3 --port 27042 --fork
Deberíamos a estas alturas tener corriendo 7 procesos de MongoDB, siendo 3 servidores de configuración, 1 router y 3 instancias fragmentos: $ ps -ax | grep mongo | grep -v grep
1
1844 ?? 0:00.65 /usr/local/Cellar/mongodb/2.4.8/mongod --configsvr --dbpath configServer1 --port 27019 -fork --config /usr/local/etc/mongod.conf
1887 ?? 0:00.62 /usr/local/Cellar/mongodb/2.4.8/mongod --configsvr --dbpath configServer2 --port 27020 -2 fork --config /usr/local/etc/mongod.conf 3 1928 ?? 0:00.59 /usr/local/Cellar/mongodb/2.4.8/mongod --configsvr --dbpath configServer3 --port 27021 -fork --config /usr/local/etc/mongod.conf 4 5
1944 ?? 0:00.14 mongos --configdb Mordor.local:27019,Mordor.local:27020,Mordor.local:27021 --port 27030 --fork --logpath routerLog
6
2002 ?? 0:00.14 /usr/local/Cellar/mongodb/2.4.8/mongod --shardsvr --dbpath shard1 --port 27040 --fork -7 config /usr/local/etc/mongod.conf 2043 ?? 0:00.12 /usr/local/Cellar/mongodb/2.4.8/mongod --shardsvr --dbpath shard2 --port 27041 --fork -8 config /usr/local/etc/mongod.conf 2084 ?? 0:00.12 /usr/local/Cellar/mongodb/2.4.8/mongod --shardsvr --dbpath shard3 --port 27042 --fork -config /usr/local/etc/mongod.conf
Bien, ahora agreguemos los fragmentos al cluster, para ello debemos ingresar a la instancia router y agregarlos de la siguiente manera: 1 $ mongo --port 27030 2 ... 3 mongos> sh.addShard("Mordor.local:27040") 4 { "shardAdded" : "shard0000", "ok" : 1 } 5 mongos> sh.addShard("Mordor.local:27041") 6 { "shardAdded" : "shard0001", "ok" : 1 } 7 mongos> sh.addShard("Mordor.local:27042") 8 { "shardAdded" : "shard0002", "ok" : 1 } 9 mongos> sh.status() 10 --- Sharding Status --11 sharding version: {
12
"_id" : 1,
13
"version" : 3,
14
"minCompatibleVersion" : 3,
15
"currentVersion" : 4,
16
"clusterId" : ObjectId("52dc95a944281854002ed8e7")
17 } 18 shards: 19
{ "_id" : "shard0000", "host" : "Mordor.local:27040" }
20
{ "_id" : "shard0001", "host" : "Mordor.local:27041" }
21
{ "_id" : "shard0002", "host" : "Mordor.local:27042" }
22 databases: 23
{ "_id" : "admin", "partitioned" : false, "primary" : "config" }
Habilitar fragmentación Perfecto tenemos nuestro cluster armado, solo nos falta activar la fragmentación, para ello en la misma instancia router la habilitaremos para la base de datos codehero y fragmentaremos la colección pruebaFragmentacion por su campo _idde manera hasheada lo cual nos permitirá cumplir con las reglas de elección de llaves de fragmentación como vimos en la entrada pasada: 1 mongos> use codehero 2 switched to db codehero 3 mongos> sh.enableSharding("codehero") 4 { "ok" : 1 } 5 mongos> db.pruebaFragmentacion.ensureIndex({ _id : "hashed" }) 6 mongos> sh.shardCollection("codehero.pruebaFragmentacion", { "_id": "hashed" } ) 7 { "collectionsharded" : "codehero.pruebaFragmentacion", "ok" : 1 }
Demo Muy bien ya tenemos nuestra colección fragmentada, ahora crearemos un montón de documentos para ver como se distribuyen entre los fragmentos: 1 mongos> for(var i=0; i < 100001; i++) db.pruebaFragmentacion.insert({})
Finalmente veamos como se encuentran distribuidos estos documentos en las colecciones:
1 mongos> db.pruebaFragmentacion.getShardDistribution() 2 3 Shard shard0000 at Mordor.local:27040 4
data : 687KiB docs : 29350 chunks : 1
5
estimated data per chunk : 687KiB
6
estimated docs per chunk : 29350
7 8 Shard shard0001 at Mordor.local:27041 9
data : 980KiB docs : 41839 chunks : 1
10 estimated data per chunk : 980KiB 11 estimated docs per chunk : 41839 12 13 Shard shard0002 at Mordor.local:27042 14 data : 675KiB docs : 28812 chunks : 1 15 estimated data per chunk : 675KiB 16 estimated docs per chunk : 28812 17 18 Totals 19 data : 2.28MiB docs : 100001 chunks : 3 20 Shard shard0000 contains 29.34% data, 29.34% docs in cluster, avg obj size on shard : 24B 21 Shard shard0001 contains 41.83% data, 41.83% docs in cluster, avg obj size on shard : 24B 22 Shard shard0002 contains 28.81% data, 28.81% docs in cluster, avg obj size on shard : 24B
Veremos que la información se ha distribuido bastante bien entre los distintos fragmentos y que los datos no han sido todos asignados a uno solo, lo cual nos indica que hemos escogido correctamente nuestra llave de fragmentación y hemos logrado obtener el escalamiento de base de datos horizontal que estamos buscando. También podemos ver varios aspectos del cluster al ejecutar el comando sh.status() : 1 mongos> sh.status() 2 --- Sharding Status --3
sharding version: {
4
"_id" : 1,
5
"version" : 3,
6
"minCompatibleVersion" : 3,
7
"currentVersion" : 4,
8
"clusterId" : ObjectId("52d2f649c3d6590b6ddbb99b")
9 } 10 shards: 11
{ "_id" : "shard0000", "host" : "Mordor.local:27040" }
12
{ "_id" : "shard0001", "host" : "Mordor.local:27041" }
13
{ "_id" : "shard0002", "host" : "Mordor.local:27042" }
14 databases: 15
{ "_id" : "admin", "partitioned" : false, "primary" : "config" }
16
{ "_id" : "codehero", "partitioned" : true, "primary" : "shard0000" }
17
codehero.pruebaFragmentacion
18
shard key: { "_id" : "hashed" }
19
chunks:
20
shard0001 1
21
shard0002 1
22
shard0000 1
23
{ "_id" : { "$minKey" : 1 } } -->> { "_id" : NumberLong("-1492793005875893056") } on : shard0001 Timestamp(2, 0)
24 { "_id" : NumberLong("-1492793005875893056") } -->> { "_id" : 25 NumberLong("3847987569150422320") } on : shard0002 Timestamp(3, 0) { "_id" : NumberLong("3847987569150422320") } -->> { "_id" : { "$maxKey" : 1 } } on : shard0000 Timestamp(3, 1)
Notarás en la parte inferior los rangos que ha tomado cada fragmento sobre la llave de fragmentación _id hasheada para distribuir los documentos. De igual manera podremos notar que exiten 3 chunks o trozos, de los cuales existe uno en cada fragmento. Un chunk está delimitado por un rango definido por MongoDB sobre la llave de fragmentación, en este caso cada fragmento posee un únicochunk, si este llegara a pasar los 64MB (o lo que se haya especificado en configuraciones avanzadas) se realizará automaticamente una operación de separación o splitting la cual dividirá el trozo en 2 para lograr mantener un alto nivel de rendimiento. Es posible también que si un fragmento comienza a tener varios chunks en com paración con sus hermanos, se ejecute una operación
de migración fragmento.
de chunks, este moverá chunks en los extremos de su rango a otro
Es posible que si pruebas con menor cantidad de documentos no notes que la información se separe en los diferentes fragmentos ni chunks, esto se debe a que la información es todavía muy pequeña para que MongoDB considere separarla, ya que como puedes ver hemos insertado 100.000 documentos y estos solo ocupan un tamaño de 2.28MB debido a la ausencia de complejidad en su estructura.
Conclusión Hemos recorrido un largo camino, hemos llegado a uno de los temas más avanzados de MongoDB, cuando nos encontramos hablando de este tipo de temas en porque nos interesa que una gran infraestructura de base de datos sea lo más escalable y mantenible posible, ciertamente es un tema enfocado más a los DBAs que a los desarrolladores pero es importante para ambos conocer las implicaciones de estas situaciones ya que la cooperación de ambos ayudará a determinar un rendimiento óptimo, especialmente al determinar aspectos críticos como la llave de fragmentación.
MongoDB desde Cero: Agregación – Parte I Estamos llegando al final de la serie, hemos pasado por una gama amplia de tópicos, desde lo más básico a los más avanzado, tocando temas de desarrollo y de administración de la base de datos. Una funcionalidad muy interesante en MongoDB es aquella que nos permitirá transformar la data antes de sacarla de la base de datos, a esto se le llama el Aggregation Framework o simplemente Agregación.
Propósito El proceso de agregación se define como una serie de operaciones a las cuales se somete una colección para obtener un conjunto de resultados calculados, formateados y/o filtrados de manera diferente a como se encuentran en los documentos, en general con el objetivo de agrupar y/o calcular datos que residen en los documentos de acuerdo a una necesidad particular. Para aquellos que les suene familiar el concepto probablemente hayan trabajado con el modelo de MapReduce en otras bases de datos, en efecto MongoDB también soporta esta modalidad; sin embargo haremos énfasis en la implementación particular de MongoDB.
Tubería de agregación Este concepto es la modalidad de implementación de agregación que es parte del núcleo de MongoDB a partir de la versión 2.2. Este se basa en someter a una colección a un conjunto de operaciones o etapas las cuales irán convirtiendo un conjunto de documentos pertenecientes a una colección hasta obtener un conjunto de documentos con el resultado computado deseado. Se le llama tubería ya que cada etapa irá modificando, moldeando y calculando la estructura de los documentos para pasarlo a la etapa que le sigue. Ciertamente podemos repetir las etapas según sea necesario, no existe limitación al respecto; sin embargo si debemos tomar en cuenta las ventajas a nivel de rendimiento que puede ofrecer el orden de las etapas ya que los procesos de agregación son tareas que pueden llegar a consumir altos niveles de recursos si no sabemos bien lo que hacemos.
Etapas Ciertamente suena un poco ambiguo en teoría pero veamos en práctica como podemos manipular los documentos de una colección usando la modalidad de tubería de agregación. 1 db.ordenes.aggregate([ 2 3
{ $etapa1: {
4
...
5
}
6
},{
7
$etapa2: {
8 9 10
... } },...
11 ])
Si prestamos atención a la sintaxis notaremos que le estamos indicando a la colección ordenes que debe someterse a un un proceso de agregación
(aggregate ) el cual consiste de varias (notemos los corchetes) etapas, cada una de ellas está definida por un conjunto de opciones, campos y/o argumentos que veremos a continuación para cada tipo de etapa. Para que nos sea más fácil entender como se comporta cada una de estas etapas, tomaremos como premisa que la colecciónordenes luce algo así: 1{ 2
_id: 1,
3
id_cliente: 10,
4
monto: 200,
5
modo_de_pago: 'efectivo',
6
articulos: ['harina', 'aceite', 'papel de baño']
7}
Si deseas probar en vivo la funcionalidad puedes descargar el archivo de ordenes.json del repositorio de esta entrada e importarlo a tu base de datos para que puedas seguir en práctica lo que aquí veremos. Ciertamente una colección que almacene datos como órdenes debería tener más campos y estar mejor estructurada pero para nuestra ejemplo será suficiente para lograr demostrar como funciona la agregación.
Filtrar ($match)
La opción de filtrar es bastante análoga a lo que ya hemos visto con los filtros convencionales en las búsquedas, simplemente filtrará los documentos según los valores que indiquemos. Si quisiéramos obtener las órdenes que realizó el cliente de ID = 2 lo haríamos parecido a como estamos acostumbrados pero con la sintaxis de la tubería de agregación: 1 > db.ordenes.aggregate([ 2
{
3
$match: {
4
id_cliente: 2
5 6 7 ]) 8 9 {
} }
10
"result" : [
11
{
12
"_id" : 3,
13
"id_cliente" : 2,
14
"monto" : 220,
15
"modo_de_pago" : "efectivo",
16
"articulos" : [
17
"pasta",
18
"ketchup",
19
"papel de baño"
20
]
21
},
22
{
23
"_id" : 8,
24
"id_cliente" : 2,
25
"monto" : 89,
26
"modo_de_pago" : "efectivo",
27
"articulos" : [
28
"harina",
29
"aceite",
30
"papel de baño"
31
]
32
}
33
],
34
"ok" : 1
35 }
Agrupar ($group) La agrupación es quizás la etapa más utilizada en el proceso de agregación ya que es la que permite agrupar y realizar cálculos sobre los documentos. Esta suele ser la etapa más complicada de entender, así que trataremos de explicarla paso a paso.
Ya que esta etapa tomará los documentos srcinales de la colección y los convertirá en una serie de nuevos documentos, debemos especificar como estará compuesto este nuevo, es decir, los campos que contendrá. Digamos que queremos agrupar las ordenes por modo_de_pago y queremos que los documentos finales tengan la cantidad de ordenes para cada modo de pago y la suma de sus correspondientes montos. Por lo tanto dichos documentos resultantes tendrán una estructura como esta: 1{ 2
_id: 'tarjeta',
3
cantidad_de_ordenes : ...,
4
monto_total: ...
5}
Es obligatorio especificar un campo _id para estos nuevos documentos ya que estos serán los valores a agrupar. Es posible tener agrupaciones múltiples como por ejemplo agrupar pormodo_de_pago y por id_cliente lo cual permitiría sacar cálculos para cada tipo de pago para cada cliente ya que por cada cliente se obtendrían una cantidad de documentos igual a la cantidad de tipos de pago que este utilizó. Bien, ahora te estarán surgiendo algunas preguntas.
¿Cómo puedo hacer para tomar los valores de los campos? Ciertamente si analizamos los valores del campo modo_de_pago de la colección notaremos que existen 2 valores posibles, tarjeta y efectivo , pero debemos indicarle al proceso de agregación que tome dichos valores de los campos. Los valores de los campos se toman colocándole al mismo el símbolo prefijo $. Por lo tanto para agrupar por modo_de_pago como dijimos anteriormente usaríamos algo así: 1{ 2
_id: "$modo_de_pago"
3
...
4}
¿Y cómo hago para hacer los cálculos? Existen varios operadores para hacer cálculos en esta etapa:
$addToSet $push $first $last $min $max
$avg $sum
Si has seguido la serie verás que los nombres te parecen conocidos y su funcionamiento en el proceso de agregación es bastante similar. Los 2 primeros, $addToSet y $push permitirán crear un arreglo de los valores correspondientes a los campos cuando estemos agrupando. Con $first y $last , podrás tomar de dicho campo el primer o último valor encontrado. (Estas son utilizadas después de la etapa de ordenamiento ya que de lo contrario su resultado es impredecible). Posteriormente $min y $max, podrás tomar el mínimo y máximo valor de dicho campo. Y por último, los operadores $avg y $sum te permitirán sacar un promedio de los valores de dicho campo y sumar su cantidad o su ocurrencia para cada agrupación. Ahora volviendo a nuestro ejemplo y retomando los pasos que hemos visto, queremos agrupar por modo_de_pago y obtener para cada uno el m onto total y la cantidad de ordenes. Para ello haremos los siguiente: 1 > db.ordenes.aggregate([ 2
{
3
$group: {
4
_id: "$modo_de_pago",
5
cantidad_de_ordenes: { $sum : 1 },
6
monto_total: { $sum : "$monto" }
7 8
} }
9 ]) 10 11 { 12 13
"result" : [ {
14
"_id" : "tarjeta",
15
"cantidad_de_ordenes" : 11,
16
"monto_total" : 5154
17
},
18
{
19
"_id" : "efectivo",
20
"cantidad_de_ordenes" : 9,
21
"monto_total" : 2100
22
}
23
],
24
"ok" : 1
25 }
Conclusión Hemos empezado a ver uno de los temas más avanzados en MongoDB, este te permite manipular los documentos para realizar cálculos que son de gran utilidad bajo circunstancias particulares. Aun faltan algunas etapas y mostraremos también un ejemplo completo de todas las etapas trabajando juntas, no te lo pierdas la semana que viene.
MongoDB desde Cero: Agregación – Parte II La semana pasada comenzamos a hablar del Agg rega tion Frame work , iniciamos viendo en qué consiste, las ventajas que ofrece y algunas de las etapas que componen la tubería de agregación, esta semana seguiremos viendo el resto de las etapas y veremos como utilizarlas juntas en un ejercicio.
Etapas (continuación) Recuerda que puedes acceder al repositorio de esta entrada para obtener algunos datos de prueba en el archivo ordenes.json que te ayudarán a practicar y probar en vivo lo que haremos aquí.
Proyectar ($project) La etapa de proyección nos permite especificar qué campos estarán en el documento resultante de esta etapa, a su vez también podemos renombrar el campo de ser necesario: 1 > db.ordenes.aggregate([ 2 3
{ $project : {
4
monto: 1,
5
cliente: "$id_cliente"
6 7
} }
8 ]) 9 10 { 11
"result" : [
12
{
13
"_id" : 1,
14
"monto" : 200,
15
"cliente" : 10
16
},
17
{
18
"_id" : 2,
19
"monto" : 180,
20
"cliente" : 10
21
},...
22
],
23
"ok" : 1
24 }
Al asignarle al nombre de un campo el valor booleano 1 estaremos indicandole al proceso de agregación que queremos incluir este campo en el documento resultante. En cuanto al campo id_cliente podremos ver que lo que hicimos fue renombrarlo a cliente , esto puede ser muy util para trabajar de manera más facil los documentos en etapas siguientes de la tubería. Probablemente te estarás preguntando: ¿Por qué el campo _id está presente si no especifiqué que lo deseaba como resultado?. El campo _id por defecto es incluido a menos que se especifique lo contrario mediante una exclusión explícita _id : 0
Desenvolver ($unwind) La etapa de desenvolvimiento permite tomar un campo de los documentos que sea de tipo arreglo y generar un documento para cada valor del mismo. Esta etapa suele combinarse con la de agrupación cuando la finalidad es realizar algún calculo que involucre a los valores de un campo tipo arreglo.
1 > db.ordenes.aggregate([ 2
{
3 4
$unwind : "$articulos" }
5 ]) 6 7 { 8
"result" : [
9
{
10
"_id" : 1,
11
"id_cliente" : 10,
12
"monto" : 200,
13
"modo_de_pago" : "efectivo",
14
"articulos" : "harina"
15
},
16
{
17
"_id" : 1,
18
"id_cliente" : 10,
19
"monto" : 200,
20
"modo_de_pago" : "efectivo",
21
"articulos" : "arroz"
22
},
23
{
24
"_id" : 1,
25
"id_cliente" : 10,
26
"monto" : 200,
27
"modo_de_pago" : "efectivo",
28
"articulos" : "ketchup"
29
},...
30
],
31
"ok" : 1
32 }
Como mencionamos, el arreglo resultante de documentos contiene un documeto para cada valor del arreglo. Veamos cómo es inicialmente ese primer documento como referencia: 1 > db.ordenes.find({ _id : 1 }).pretty() 2 { 3
"_id" : 1,
4
"id_cliente" : 10,
5
"monto" : 200,
6
"modo_de_pago" : "efectivo",
7
"articulos" : [
8
"harina",
9
"arroz",
10
"ketchup"
11
]
12 }
Ordenar, limitar y saltar ($sort, $limit, $skip) Estas etapas son quizás las más intuitivas debido a la facilidad de uso y la similitud de su funcionalidad a lo que hemos aprendido desde el inicio. 1 > db.ordenes.aggregate([ 2
{
3 4
$sort: { monto: -1, _id: 1 } }
5 ])
La etapa de ordenamiento recibirá como parámetro un documento indicando con qué campos se debe ordenar y en que sentido, siendo 1 ascendente y -1 descendente. En este caso se ordenará primero descendentemente por monto y de haber 2 montos iguales se ordenarán los involucrados de manera ascendente por su campo _id. Veamos su comportamiento en la sección inferior de resultados: 1 ...{ 2
"_id" : 10,
3
"id_cliente" : 1,
4
"monto" : 202,
5
"modo_de_pago" : "efectivo",
6
"articulos" : [
7
"harina",
8
"aceite",
9
"papel de baño"
10
]
11
},
12
{
13
"_id" : 1,
14
"id_cliente" : 10,
15
"monto" : 200,
16
"modo_de_pago" : "efectivo",
17
"articulos" : [
18
"harina",
19
"arroz",
20
"ketchup"
21
]
22
},
23
{
24
"_id" : 4,
25
"id_cliente" : 10,
26
"monto" : 200,
27
"modo_de_pago" : "efectivo",
28
"articulos" : [
29
"carne",
30
"aceite",
31
"galletas"
32 33
] },
34
{
35
"_id" : 15,
36
"id_cliente" : 7,
37
"monto" : 183,
38
"modo_de_pago" : "efectivo",
39
"articulos" : [
40
"pasta",
41
"aceite",
42
"papel de baño"
43
]
44
},...
De igual manera podemos lograr algo como lo que conocemos desde antes con la limitación y salto de registros; sin embargo debemos tomar en cuenta que
saltar y/o limitar una serie de documentos que no hemos ordenamos primero tendrá resultados impredecibles. 1 > db.ordenes.aggregate([ 2
{
3
$sort: { monto: -1, _id: 1 }
4
},
5
{
6
$skip: 7
7
},
8
{
9 10
$limit: 2 }
11 ]) 12 13 { 14 15
"result" : [ {
16
"_id" : 9,
17
"id_cliente" : 10,
18
"monto" : 311,
19
"modo_de_pago" : "tarjeta",
20
"articulos" : [
21
"harina",
22
"ketchup",
23
"pollo"
24
]
25
},
26
{
27
"_id" : 6,
28
"id_cliente" : 3,
29
"monto" : 302,
30
"modo_de_pago" : "tarjeta",
31
"articulos" : [
32
"harina",
33
"pasta",
34
"papel de baño"
35
]
36
}
37
],
38
"ok" : 1
39 }
Ejercicio completo Bien, ahora que conocemos como funciona cada una de las etapas podemos proceder a construir nuestra tubería de agregación con todas las etapas a ver si entendimos correctamente de qué se trata. Veamos primero el comando y luego explicaremos paso a paso lo que sucede. 1 > db.ordenes.aggregate([
2
{
3
$match: {
4
monto : { $gt: 200 }
5
}
6
},
7
{
8
$unwind : "$articulos"
9
},
10
{
11
$group: {
12
_id: "$articulos",
13
monto_promedio: { $avg: "$monto" },
14
cantidad_ordenes: { $sum: 1 },
15
compradores: { $addToSet: "$id_cliente" }
16
}
17
},
18
{
19
$sort: { monto_promedio: -1, cantidad_ordenes: -1}
20
},
21
{
22
$skip: 3
23
},
24
{
25 26
$limit: 2 }
27 ])
Antes de adelantarte a la respuesta tratemos de analizar lo que hemos hecho:
Filtrar En la primera etapa de la tubería obtuvimos las ordenes que tuviesen un monto mayor a 200.
Desenvolver Luego desenvolvimos el arreglo de articulos para poder hacer cálculos con ellos.
Agrupar Posteriormente agrupamos los documentos por articulo Sacamos un promedio de su monto. Contamos cuantas ordenes existían para dicho artículo. Y qué clientes habián comprado dichos articulos.
Ordenar Luego de agrupar procedimos a ordenar nuestro conjunto de documentos por monto promedio y por cantidad de ordenes de manera descendente.
Saltar y Limitar Finalmente saltamos los 3 primeros documentos y limitamos el resto del resultado a solo 2 documentos.
Desgloce Ciertamente no fue necesario el uso de la etapa de proyección, esto es común especialmente cuando utilizamos agrupaciones ya que esta última se suele encargar de realizar las tareas que se podrían realizar al proyectar. Ahora que conocemos lo que hicimos paso a paso podemos llegar a la conclusión de cual podría haber sido el enunciado de un ejercicio como este:
Encuentre el 4to y 5to artículo de mayor monto (tomando en cuenta que el monto varía segun el momento de la compra), indicando los compradores involucrados y cantidad de ordenes realizadas. 1 { 2 3
"result" : [ {
4
"_id" : "carne",
5
"monto_promedio" : 555.6666666666666,
6
"cantidad_ordenes" : 3,
7
"compradores" : [
8
3,
9
10
10
]
11
},
12
{
13
"_id" : "galletas",
14
"monto_promedio" : 542,
15
"cantidad_ordenes" : 1,
16
"compradores" : [
17
3
18
]
19
}
20
],
21
"ok" : 1
22 }
Conclusión Con lo que hemos visto en los últimos capítulos de la serie podemos realizar tareas avanzadas de cálculos de datos del lado de la base de datos, esto evitará que tu aplicación tenga que realizar varias búsquedas e implementar la lógica para calculo mediante múltiples ciclos y validaciones. Recuerda que las operaciones de agregación pueden realizarse de manera más rápida si haces uso de los índices, no dudes en comentarnos tus dificultades en este tema ya que suele tornarse un tanto complejo.
MongoDB desde Cero: Producción Luego de pasar por un espectro de temas de esta solución de base de datos NoSQL hemos llegado al final de la serie. Pasamos desde lo más sencillo aprendiendo qué es MongoDB, de qué esta compuesto y como se instala a las tareas más avanzadas de manipulación de datos. Para culminar la serie es vital mencionar varios aspectos que se deben considerar al usar MongoDB en un ambiente de producción.
Seguridad Debemos siempre proteger nuestras instancias de base de datos y la información que estas contienen, por ello es altamente recomendado establecer los usuarios con sus respectivos niveles de acceso a las instancias, esto evitará que cualquier individuo con o sin acceso a las mismas pueda realizar operaciones que no debería estar haciendo. No utilices el puerto estándar de las instancias (27017 para mongod por ejemplo), el conocimiento pordesatar agentesunexternos paso que puede ataque. de donde se alojan tus servicios es el primer Protege los accesos, dentro de lo posible trata de limitar por medio de reglas de firewall el acceso para que solo las aplicaciones que deben comunicarse con la base de datos sean las autorizadas a establecer una conexión con el servidor. Para más detalles puede volver a echar un vistazo a la entrada de Seguridad.
Infraestructura Siempre utiliza sistemas operativos de 64bit. Los paquetes de 32bit de MongoDB solo pueden almacenar 2GB de datos, estos son ideales para ambientes de prueba y aprender pero no para el despliegue de la base de datos final. Si estás buscando maximizar el rendimiento de entrada y salida de la base de datos se recomienda invertir en memoria RAM y discos de estado sólido (SSD), incrementar el poder de procesamiento al agregar más núcleos de CPU o actualizar a uno más potente puede ayudar pero los cambios no son tan significativos. Siempre habilita memoria swap en sistemas Linux, esto evitará errores de escasez de memoria que pueda matar algún proceso de Mongo. Trate de utilizar almacenamiento local en lugar de sistemas de archivos remotos, esto aplica en general para varios sistemas de base de datos, no solo MongoDB. En caso de utilizarlos, opta por servicios de protocolo iSCSI y no NFS, ya que este último puede causar múltiples escenarios de errores, incompatibilidades y degradación en el rendimiento. Algunos ejemplos de esto serían el EBS de Amazon y unidades locales montadas como sistemas de archivos para máquinas virtuales. En ambientes Linux que posean NUMA (Acceso de memoria no uniforme) se debe desactivar este comportamiento para MongoDB para evitar múltiples escenarios de problemas y degradación en el rendimiento. Esto también aplica para otras bases de datos como MySQL.
Disponibilidad y Rendimiento Se recomienda ampliamente utilizar replica sets, esto ayudará a mantener la base de datos siempre disponible sin importar si alguno de sus nodos falla. Siempre debes tener un número impar de miembros, preferiblemente repartidos en datacenters separados ya que cuando un proveedor de servicios falla suele fallar por datacenter completo. Recuerda que puedes utilizar los tipos especiales de miembros secundarios para tareas especiales como reportes y respaldos, de esta manera no se estará generando una carga adicional sobre los miembros principales. Si tu aplicación que se comunica con la base de datos tiene un nivel muy elevado de lecturas puedes habilitar la lectura a miembros secundarios, esto permitirá balancear la carga para que el primario no se someta a tanto estrés.
Para manejo de volúmenes de datos muy grandes considera utilizar fragmentación esto te permitirá escalar tu infraestructura para soportar más datos manteniendo un alto nivel de rendimiento. Nunca te olvides de construir los índices necesarios para los tipos de búsquedas más frecuentes para incrementar la velocidad de las operaciones.
Prevención Siempre respalda con frecuencia la información de tu base de datos. Esto es imperativo para cualquier solución de base de datos y debe ser tomada muy en serio ya que nadie quiere perder información valiosa que pueda comprometer la aplicación que esta soporta. Mantén monitoreado tu sistema, de esta manera puedes detectar degradación en el rendimiento y estar al tanto de fallas que puedan ocurrir. Para ambos casos los compañeros de 10gen (compañía detrás del desarrollo de MongoDB) nos ofrecen una herramienta tipo cloud llamada MMS (Servicio de administración de MongoDB), esta monitoreará de manera gratuita todas tus instancias de base de datos y te avisará si alguna falla, y por una cuota mensual también va respaldando los datos de tu base de datos de manera frecuente. Para monitorear también puede utilizar Munin con el plugin de MongoDB.
Alternativas Si prefieres que alguien más se encargue de la carga pesada administrativa de la base de datos puedes utilizar servicioscloud como MongoHQ o MongoLab, de esta manera se tercerizan las tareas más pesadas como la fragmentación, replicación, respaldos y monitoreo. Incluso tienen planes gratis para que puedas ir desarrollando sobre ellos y determines si es lo que deseas.
Conclusión Bueno, han sido unos meses muy interesantes y productivos desde que empezamos con MongoDB aquí en CODEHERO, espero que lo hayan aprovechado y disfrutado, ciertamente yo lo hice y aprendí junto con ustedes. Hay varios detalles que no tocamos pero creo que hemos cubierto lo suficiente como para enfrentarnos a la mayoría de las situaciones que se nos pueden presentar. Fue un placer guiarlos a través de este camino del NoSQL, y siempre pueden hacernos saber sus dudas, inquietudes y comentarios.