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:

  • Regular: Es el caso descrito arriba, y será un mongo que podrá actuar como primario o secundario.
  • Árbitro: un nodo de tipo árbitro sólo sirve para participar en la votación (por ejemplo, resultaría útil si queremos desplegar un número par de réplicas, y necesitamos un nodo adicional para deshacer los empates en las decisiones).
  • Delayed: solo puede actuar como nodo secundario (es una mezcla entre árbitro y regular).
  • Hidden: no puede ser primario, y suele emplearse únicamente para almacenar analíticas.
  • 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

  • Los nodos son transparentes para los clientes conectados
  • El driver que estemos usando, necesita conocer al menos a un servidor
  • Write concern: puede pasar un tiempo hasta que la escritura esté disponible, y cuando se produce un fallo, puede que no se admita la escritura durante un breve tiempo.
  • Hay preferencias de lectura que puedes cambiar
  • Los errores pueden seguir ocurriendo, y el entorno distribuido esta preparado para resolver gran parte de ellos
  • 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

  • Todos los documentos deben incluir la shardKey
  • La ShardKey no puede cambiar nunca
  • Necesitas establecer un índice que comience con la ShardKey
  • No puede tener ningún índice único (o clave única) que sea parte de la shardKey
  • 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

  • Tienes que asegurarte de que hay suficiente cardinalidad, es decir, suficiente cantidad de valores distintos para que los chunks se generen correctamente.
  • Puede ser monotamente creciente, pero si no tiene suficiente cardinalidad, al final todos los nuevos elementos irán al mismo shard. Esto puede causar problemas, por ejemplo si la clave de shard fuese una fecha, al final todos los elementos mayores al momento en que los rangos de clave se asignan a cada shard, serán enviados al último.
  • 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