Hasta la fecha hemos cubierto prácticamente todos los temas principales que pueden interesarnos para comenzar a trabajar con MongoDB, salvo uno: el sharding y replicación.
Anteriormente
Guía 7: Análisis de datos con el framework de agregación
Sharding y Replicación en MongoDB
Replicación
La replicación en MongoDB permite conseguir una altísima disponibilidad y tolerancia a fallos, mediante la creación de múltiples copias de mongods.
Al conjunto de mongods se lo denomina “replica set”, donde uno de los mongods será primario y el resto secundarios. La elección del mongo primario es dinámica, y cambiará en función de la disponibilidad del primario actual (es transparente para los clientes conectados), aunque existen algunas peculiaridades y diferencias entre los procesos de escritura y lectura.
Para que este cambio dinámico pueda llevarse a cabo, debe haber al menos 3 mongos, ya que cuando el primario se cae por cualquier motivo, el conjunto de réplicas restante debe ser mayoría respecto al total de mongods que hubiese inicialmente, a fin de que puedan decidir cual será el nuevo mongo primario.
Elecciones de nodo primario
Puede haber distintos tipos de nodo:
Consistencia de escritura
Las escrituras siempre se realizan contra el nodo primario, a diferencia de las lecturas, que pueden ir tanto contra el primario como contra los secundarios. Sin embargo, si siempre usamos el primario será más consistente, ya que los datos estarán disponibles desde el momento en que se solicita su escritura, y no leeremos datos obsoletos durante el tiempo que tarde la propagación.
MongoDB, frente a otros sistemas, no da consistencia eventual (salvo que leas de los nodos secundarios), sino continua (si se utiliza el nodo primario).
Crear replicaset
A continuación se muestra un breve ejemplo de cómo crear un replicaset de 3 servidores, arrancando tres instancias de mongod, y después vinculandolos entre sí.
Lanzar las instancias (en este caso todas están en la misma máquina)
mkdir -p /data/rs1 /data/rs2 /data/rs3
mongod -replSet rs1 -logpath “1.log”; -dbpath /data/rs1 -port 27017 -fork
mongod -replSet rs1 -logpath “3.log”; -dbpath /data/rs2 -port 27018 -fork
mongod -replSet rs1 -logpath “3.log”; -dbpath /data/rs3 -port 27019 -fork
Ahora los tres servidores estan arrancados cada uno en su puerto, pero son independientes y hay que vincularlos para que se mantengan en contacto entre ellos. Para esto, ejecutamos en el mongo shell:
> config = { _id: “rs1”;, members: [
{ _id : 0, host: “127.0.0.1:27017”;, priority: 0, slaveDelay: 5 },
{ _id : 1, host: “127.0.0.1:27018”; },
{ _id : 2, host: “127.0.0.1:27019”;}
]}> rs.initiate(config)
> rs.status()
Hay que prestar atención al nombre rs1 que le dimos al atributo replSet (será el nombre del conjunto de réplicas).
Además, podríamos haber utilizado los atributos SlaveDelay (que hace que arranque un tiempo después de lanzar el comando) o priority a 0, para evitar que un nodo sea primario.
Ahora que los mongods están arrancados y configurados, podemos conectarnos a cualquiera de ellos (salvo al puerto 27017 ya que hemos forzado que no sea primario)
> mongo -port 27018
> rs.status()
Si quisiéramos permitir leer de los esclavos (no se puede por defecto) hay que activarlo:
> db.slaveOk()
Podemos obtener información acerca del replicaset con varios comandos, por ejemplo:
> rs.status()
> rs.isMaster() //será true para el primario
O podemos apagar el servidor
> db.shutdownServer()
Implicaciones de la replicación
Funcionamiento interno de la replicación
En el nodo primario existe una colección (de tipo capped) llamada oplog.rs. Esta colección contiene todas las operaciones que se realizan en el nodo primario, y se emplea para propagar las mismas operaciones al resto de nodos cada cierto tiempo (casi de inmediato).
Si un servidor se cae antes de que todo su oplog se haya propagado, esas operaciones quedarán pendientes. Cuando más tarde el servidor retorne, se conectará al conjunto de replicas como un nodo secundario (ya que el conjunto habrá escogido a un nuevo nodo primario) y en ese momento detectará su oplog lleno y lo moverá al fichero rollback (donde podremos consultar todas esas operaciones que no llegaron a propagarse), pero no aplicará las operaciones al replicaset, por lo que esos datos podrían no ser consistentes.
Es posible establecer los parametros de operación w=1 y j=1 para configurar el Write Concern, y que de este modo no devuelvan el Ok de la operación de escritura hasta que no se haya propagado tambien a alguno de los secundarios (y de este modo asegurar la consistencia de ciertas operaciones críticas).
MongoDB driver
Conectarse usando un driver
Para conectarnos a un replicaset usando un driver, como podria ser el mongodb driver para nodejs, es posible establecer uno o varios de las instancias con la llamada siguiente:
MongoClient.connect(“mongodb://localhost:30001,”; +
“localhost:30002,”;+
“localhost:30003/course”;, function(err, db) {
...
})
Si no se especifica alguno de los nodos del conjunto no pasa nada, ya que será autodescubierto si al menos hemos especificado un nodo que sí esta disponible.
Cuando se produzca un fallo, el driver de mongo-nodejs acumula un buffer de solicitudes y lo ejecuta en cuanto detecta que la conexión es válida y hay un primario de nuevo disponible.
Write Concern en mongo driver
Es posible especificar ciertos atributos de las operaciones para determinar el grado de consistencia que esperamos durante la escritura, según los siguientes parámetros:
- w:1 -> espera a que la operación haya sido registrada en el principal y retorna una respuesta
- w:0 -> entrega la petición y devuelve un ok inmediato pero sin respuesta
- w:2 -> espera a que el primario y al menos un secundario conozcan el dato
- w:3 -> para dos secundarios y el primario… etc
- w:j -> escribe en el journal, lo que permite estar seguro de que los datos serán persistentes
- w:majority -> espera a que esté en la mayoría de nodos
Para usarlo, hay que añadir lo siguiente en el connection string:
“localhost:30003/course?w=1”;
O bien podemos utilizar en una operación individual concreta mediante el objeto options:
“insert({x:2},{w:3})”;
Preferencias de lectura en Mongo Driver
Puedes establecer las preferencias de lectura como primario, secundario o nearest, bien sea al establecer la conexión o en una operación concreta. Veamos ambos casos:
“localhost:30003/course?readPreference=secondary”;
{ 'readPreference' : ReadPreference.PRIMARY }
Sharding
El sharding, a diferencia de la replicación, tiene como objetivo mejorar la eficiencia y el rendimiento de las operaciones de consulta mediante el particionado de datos y su distribución troceada en distintos nodos denominados Shards.
Cada Shard puede ser una única instancia de mongo o un replicaset indistintamente (lo que mejoraria la disponibilidad de cada uno de los shards). Los conjuntos de shards se ubican detrás de un mongos, que es el router que mantiene el pool de conexiones con cada shard.
Cada Shard guarda ciertos trozos de datos de las colecciones, que se denominan “chunks”. Estos trozos se guardan en base a rangos de valores de ciertos atributos, con concreto de aquellos que forman la ShardKey o clave de Shard.
La ShardKey es un atributo de nuestros documentos que, si presenta una segregación de valores adecuada (elevada), permite al Shard repartir los documentos que comparten su valor en un único shard, de forma que cuando pedimos al router un documento y especificamos su ShardKey, sabe exactamente en qué shard tiene que buscar ese documento. Si por el contrario no hubiésemos especificado una shardKey, el router tendría que lanzar la búsqueda en broadcast a todos los shards existentes, para que todos ellos intenten localizar los resultados coincidentes. Debido a esta razón, cuando se trabaja con sharding es extremadamente importante establecer una clave de shard en nuestras colecciones y especificarla siempre, tanto en las consultas como en las inserciones (para que el servidor pueda ubicar correctamente el nuevo documento).
Además, otro datos interesante es que los mongos carecen de estado (stateless), así que podría haber más de uno y se comportarán como un conjunto de réplicas.
Como desplegar un Sharding
Para desplegar un entorno de sharding, hay que lanzarlo del mismo modo que si fuera un conjunto de réplicas, añadiendo la clave —;shardsvr para indicarle que además de una réplica, va a ser un shard.
Además, necesitaremos unos config-servers, que se lanzan igual pero su clave en este caso será -configsvr
Luego ya podríamos arrancar el mongos:
//ej. init_sharded_env.ae39ee9f8161.sh
mongos -logpath “mongos-1.log”; -configdb
localhost:57040,localhost:57041,localhost:57042 -fork
Finalmente, hay que decirle a los servidores de config que conozcan al conjunto de shards.
> db.adminCommand({ addshard: ... })
Ya podemos consultar el estado de nuestros shards:
> sh.status()
Implicaciones del sharding
Por ejemplo, si la clave de shard es “student_id, class”, entonces puedes tener un índice único sobre “student_id, class, otro_atributo”. Esto se debe a que los índices de cada shard están completos en cada shard, no se accede a ellos de forma conjunta.
Para elegir una clave de shard
Un buen ejemplo es considerar qué accesos pueden ser paralelos. Por ejemplo, en un servidor de fotos, cada usuario podría ser un shardKey para paralelizar su acceso, ya que cada usuario accederá a sus propias fotos (empleará un único shard) y el resto accederán en paralelo al resto de shards, por lo que se evita colapsar un shard innecesariamente.
Ejemplo de creación de Sharding y replica-set completo
A continuación se muestra un fragmento de la documentación de Mongo University donde se describen los pasos para generar un conjunto de replicas y sharding:
# Andrew Erlichson
# 10 gen
# script to start a sharded environment on localhost
# clean everything up
echo "killing mongod and mongos"
killall mongod
killall monogs
echo "removing data files"
rm - rf / data / config
rm - rf / data / shard * #start a replica set and tell it that it will be a shord0
mkdir - p / data / shard0 / rs0 / data / shard0 / rs1 / data / shard0 / rs2
mongod-replSet s0-logpath "s0-r0.log"-dbpath / data / shard0 / rs0-port 37017-fork-shardsvr-smallfiles
mongod-replSet s0-logpath "s0-r1.log"-dbpath / data / shard0 / rs1-port 37018-fork-shardsvr-smallfiles
mongod-replSet s0-logpath "s0-r2.log"-dbpath / data / shard0 / rs2-port 37019-fork-shardsvr-smallfiles
sleep 5
# connect to one server and initiate the set
mongo-port 37017 << 'EOF'
config = {
_id: "s0",
members: [{
_id: 0,
host: "localhost:37017"
}, {
_id: 1,
host: "localhost:37018"
}, {
_id: 2,
host: "localhost:37019"
}]
};
rs.initiate(config)
EOF
# start a replicate set and tell it that it will be a shard1
mkdir - p / data / shard1 / rs0 / data / shard1 / rs1 / data / shard1 / rs2
mongod-replSet s1-logpath "s1-r0.log"-dbpath / data / shard1 / rs0-port 47017-fork-shardsvr-smallfiles
mongod-replSet s1-logpath "s1-r1.log"-dbpath / data / shard1 / rs1-port 47018-fork-shardsvr-smallfiles
mongod-replSet s1-logpath "s1-r2.log"-dbpath / data / shard1 / rs2-port 47019-fork-shardsvr-smallfiles
sleep 5
mongo-port 47017 << 'EOF'
config = {
_id: "s1",
members: [{
_id: 0,
host: "localhost:47017"
}, {
_id: 1,
host: "localhost:47018"
}, {
_id: 2,
host: "localhost:47019"
}]
};
rs.initiate(config)
EOF
# start a replicate set and tell it that it will be a shard2
mkdir - p / data / shard2 / rs0 / data / shard2 / rs1 / data / shard2 / rs2
mongod-replSet s2-logpath "s2-r0.log"-dbpath / data / shard2 / rs0-port 57017-fork-shardsvr-smallfiles
mongod-replSet s2-logpath "s2-r1.log"-dbpath / data / shard2 / rs1-port 57018-fork-shardsvr-smallfiles
mongod-replSet s2-logpath "s2-r2.log"-dbpath / data / shard2 / rs2-port 57019-fork-shardsvr-smallfiles
sleep 5
mongo-port 57017 << 'EOF'
config = {
_id: "s2",
members: [{
_id: 0,
host: "localhost:57017"
}, {
_id: 1,
host: "localhost:57018"
}, {
_id: 2,
host: "localhost:57019"
}]
};
rs.initiate(config)
EOF# now start 3 config servers
mkdir - p / data / config / config - a / data / config / config - b / data / config / config - c
mongod-logpath "cfg-a.log"-dbpath / data / config / config - a-port 57040-fork-configsvr-smallfiles
mongod-logpath "cfg-b.log"-dbpath / data / config / config - b-port 57041-fork-configsvr-smallfiles
mongod-logpath "cfg-c.log"-dbpath / data / config / config - c-port 57042-fork-configsvr-smallfiles# now start the mongos on a standard port
mongos-logpath "mongos-1.log"-configdb localhost: 57040, localhost: 57041, localhost: 57042-fork
echo "Waiting 60 seconds for the replica sets to fully come online"
sleep 60
echo "Connnecting to mongos and enabling sharding"#
add shards and enable sharding on the test db
mongo << 'EOF'
db.adminCommand({
addShard: "s0/" + "localhost:37017"
});
db.adminCommand({
addShard: "s1/" + "localhost:47017"
});
db.adminCommand({
addShard: "s2/" + "localhost:57017"
});
db.adminCommand({
enableSharding: "test"
})
db.adminCommand({
shardCollection: "test.grades",
key: {
student_id: 1
}
});
EOF
Conclusión
A lo largo de toda esta guía, hemos visto todos los aspectos fundamentales de MongoDB de una forma bastante práctica. Evidentemente, si queréis utilizar MongoDB en un entorno de producción, tendréis que formaros más profundamente y consultar la documentación para conocer los pormenores de cada características de las que hemos visto, pero espero que lo aquí expuesto sea suficiente para despertar vuestro interés en esta tecnología, que además de resultar extremadamente útil en algunos casos, es muy divertida y rápida de utilizar.
¡Un saludo!
Enlaces a la guía completa
- Guía 1: Introducción a MongoDB, características clave
- Guía 2: Arrancar MongoDB e importar datos
- Guía 3: Comandos y operaciones esenciales
- Guía 4: Comandos y operaciones avanzadas
- Guía 5: Esquema de datos dirigido por la aplicación
- Guía 6: Crear, manejar y entender los índices
- Guía 7: Análisis de datos con el framework de agregación
- Guía 8: Replicación y Sharding