1. Introducción

Hola a todos y bienvenido a un nuevo post de nuestro querido blog!. En esta ocasión vamos a ver como montar una replica set en MongoDB. Éste es el mecanismo que nos proporciona MongoDB para garantizar la alta disponibilidad de nuestras bases de datos. La idea consiste en tener corriendo varias instancias de mongo con el fin de que la información se replique entre ellas, de tal forma que si el nodo primario se cae, pueda ser sustituido por otro. Existen diferentes tipos de miembros:

  • Primary: Nodo sobre el que se realizan las escrituras y principalmente las lecturas
  • Secundary: Nodo que mantiene una copia del dataset del primario y puede ser candidato en caso de que éste no se encuentre disponible. Interviene en la elección de un primario.
  • Arbitrer: Nodo que no mantiene una copia del dataset y que tiene como principal objetivo ayudar en la elección del primario en el momento de la votación
  • Hidden: Nodo que mantiene una copia del dataset pero no puede ser elegido como primario. No puede ser visto por las aplicaciones. Su principal objetivo es realizar tareas de workload. Interviene en la elección de un primario.
  • Delayed: Nodo que mantiene una copia del dataset atrasada. Esto puede ser útil para ver un estado previo de la base de datos. Interviene en la elección de un primario.

Para más información sobre los diferentes tipos de miembros visitar la web de mongo

Un aspecto importante a considerar es que sólamente el nodo primario es capaz de recibir escrituras, mientras que tanto el primario como el secundario pueden recibir lecturas. Sin embargo no es recomendable llevar a cabo lecturas sobre los nodos secundarios ya que nos podríamos encontrar con datos obsoletos. Esto es así ya que la copia de datos entre los nodos es asíncrona y solamente se recomienda las lecturas sobre estos nodos bajo ciertos supuestos especiales.

Existen diferentes arquitecturas que se pueden considerar dentro de un replica set. En este post abordaremos la más estándar. Un primario y dos secundarios.

Tecnologías empleadas:

  • MongoDB 3.6

2. Requisitos previos

2.1 Instalación

Es necesario tener los binarios de MongoDB en nuestra máquina. Si no los tiene bájatelos de los web oficial y descomprímelos dentro de un directorio de trabajo.

Preparación entorno

Cada instancia de mongo necesita un directorio de trabajo (dbpath) sobre el cual trabajar. Creamos tres directorios asignándoles permisos de lectura / escritura.

mkdir /data/rs1
mkdir /data/rs2
mkdir /data/rs3

chmod 333 /data/rs1
chmod 333 /data/rs2
chmod 333 /data/rs3

3. Creación de la réplica. Un primario y dos secundarios

Creamos tres instancias de mongo especificando mediante el atributo replSet que van a formar parte de una replica set. Además indicamos el directorio de trabajo así como el puerto sobre el que se va a trabajar.

mongod --replSet m101 --dbpath /data/rs1 --port 27017
mongod --replSet m101 --dbpath /data/rs2 --port 27018
mongod --replSet m101 --dbpath /data/rs3 --port 27019

Por ahora solamente hemos levantado tres instancias de mongodb por lo que todavía no hay sincronización entre las mismas. Para terminar de configuración nuestra réplica, entramos en un nodo

mongo --port 27017

y ejecutamos el siguiente comando

config = { _id: "m101", members:[
          { _id : 0, host : "localhost:27017"},
          { _id : 1, host : "localhost:27018"},
          { _id : 2, host : "localhost:27019"} ]
};
rs.initiate(config);

Se puede forzar que un miembro de la replica sea hidden, delayed o arbitrer. Para ello hay que indicarlo tal y como se especifica en el manual de mongo.

Una vez hecho esto, los nodos votan para elegir a un primario, que será quien reciba las escrituras. El resto de nodos se quedarán como secundarios. El proceso tarda en torno a 10 sg. Para ver el estado de las réplicas hacemos

rs.status(config);

Obtiendo algo como

{
	"set" : "m101",
	"date" : ISODate("2018-03-24T08:39:39.258Z"),
	"myState" : 1,
	"term" : NumberLong(1),
	"heartbeatIntervalMillis" : NumberLong(2000),
	"optimes" : {
		"lastCommittedOpTime" : {
			"ts" : Timestamp(1521880769, 1),
			"t" : NumberLong(1)
		},
		"readConcernMajorityOpTime" : {
			"ts" : Timestamp(1521880769, 1),
			"t" : NumberLong(1)
		},
		"appliedOpTime" : {
			"ts" : Timestamp(1521880769, 1),
			"t" : NumberLong(1)
		},
		"durableOpTime" : {
			"ts" : Timestamp(1521880769, 1),
			"t" : NumberLong(1)
		}
	},
	"members" : [
		{
			"_id" : 0,
			"name" : "localhost:27017",
			"health" : 1,
			"state" : 1,
			"stateStr" : "PRIMARY",
			"uptime" : 441,
			"optime" : {
				"ts" : Timestamp(1521880769, 1),
				"t" : NumberLong(1)
			},
			"optimeDate" : ISODate("2018-03-24T08:39:29Z"),
			"infoMessage" : "could not find member to sync from",
			"electionTime" : Timestamp(1521880728, 1),
			"electionDate" : ISODate("2018-03-24T08:38:48Z"),
			"configVersion" : 1,
			"self" : true
		},
		{
			"_id" : 1,
			"name" : "localhost:27018",
			"health" : 1,
			"state" : 2,
			"stateStr" : "SECONDARY",
			"uptime" : 61,
			"optime" : {
				"ts" : Timestamp(1521880769, 1),
				"t" : NumberLong(1)
			},
			"optimeDurable" : {
				"ts" : Timestamp(1521880769, 1),
				"t" : NumberLong(1)
			},
			"optimeDate" : ISODate("2018-03-24T08:39:29Z"),
			"optimeDurableDate" : ISODate("2018-03-24T08:39:29Z"),
			"lastHeartbeat" : ISODate("2018-03-24T08:39:38.107Z"),
			"lastHeartbeatRecv" : ISODate("2018-03-24T08:39:39.127Z"),
			"pingMs" : NumberLong(0),
			"syncingTo" : "localhost:27017",
			"configVersion" : 1
		},
		{
			"_id" : 2,
			"name" : "localhost:27019",
			"health" : 1,
			"state" : 2,
			"stateStr" : "SECONDARY",
			"uptime" : 61,
			"optime" : {
				"ts" : Timestamp(1521880769, 1),
				"t" : NumberLong(1)
			},
			"optimeDurable" : {
				"ts" : Timestamp(1521880769, 1),
				"t" : NumberLong(1)
			},
			"optimeDate" : ISODate("2018-03-24T08:39:29Z"),
			"optimeDurableDate" : ISODate("2018-03-24T08:39:29Z"),
			"lastHeartbeat" : ISODate("2018-03-24T08:39:38.107Z"),
			"lastHeartbeatRecv" : ISODate("2018-03-24T08:39:39.093Z"),
			"pingMs" : NumberLong(0),
			"syncingTo" : "localhost:27017",
			"configVersion" : 1
		}
	],
	"ok" : 1,
	"operationTime" : Timestamp(1521880769, 1),
	"$clusterTime" : {
		"clusterTime" : Timestamp(1521880769, 1),
		"signature" : {
			"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
			"keyId" : NumberLong(0)
		}
	}
}

Vemos que la instancia que se encuentra escuchando en el puerto 27017 ha sido elegida como la primaria mientras que las otras dos se han quedado como secundarias.

4. Pruebas de sincronización

Para probar que verdaderamente se está llevando a cabo la sincronización de las colecciones entre el nodo primario y los secundarios nos basta con crear una colección e insertar un documentos en el primario y ver como se sincroniza con el resto de nodos. En nuestro caso el nodo primario es el que corre sobre el puerto 27017

Entramos en la instancia 27017 (el primario)

mongo --port 27017

Creamos una bd, una colección, insertamos un documento y salimos

use mibd;
db.user.insert({name: "Jorge"});
exit;

Entramos en la instancia 27019 (uno de los secundarios)

mongo --port 27019

Y vemos que se ha creado la base de datos midb y la colección users. Sin embargo antes es necesario habilitar las escrituras en los secundarios ya que por defecto se encuentra deshabilitada a través del comando rs.slaveOk()

rs.slaveOk();
use mibd;
db.user.find();

Obteniendo algo como

Hemos de tener en cuenta que durante el proceso de sincronización se copian todas las colecciones excepto las que se encuentran dentro de la base de datos local. No hemos hablado de una colección especial dentro de esta base de datos que es oplog y que básicamente es la responsable de ir anotando todos los cambios en términos de escrituras, borrados y actualizaciones que se acometen sobre los documentos. Esta colección será utilizada por el resto de miembros para acometer la sincronización.

5. Referencias