Sunday, October 11, 2026


Laboratorio: Replica Set en MongoDB

🖥 Máquina local con mongod🔧 MongoDB ≥ 6.0⏱ ~2 horas📊 1 primario · 2 secundarios
0

¿Qué es un Replica Set?

Un Replica Set es un grupo de instancias de mongod que mantienen el mismo conjunto de datos. Proporciona redundancia (los datos existen en varias copias) y alta disponibilidad (si el nodo primario falla, uno de los secundarios es elegido automáticamente como nuevo primario).

PRIMARIOrs0 / puerto 27018lectura + escrituraSECUNDARIO 1rs0 / puerto 27019réplica (oplog)SECUNDARIO 2rs0 / puerto 27020réplica (oplog)oplogoplogheartbeat

Oplog

El operation log es una colección especial (local.oplog.rs) donde el primario registra cada operación de escritura. Los secundarios leen este log y aplican las operaciones en orden.

Heartbeat

Cada nodo envía un ping a los demás cada 2 segundos. Si el primario no responde durante 10 s (configurable), los secundarios inician una elección para elegir uno nuevo.

Quórum

Se necesita la mayoría de votos (⌈n/2⌉ + 1) para elegir un primario. Con 3 nodos el quórum es 2, por lo que el clúster sobrevive a la caída de 1 nodo.

0b

Paso previo: copiar la base de datos video al futuro primario

Antes de configurar el Replica Set necesitamos que el futuro primario (puerto 27018) tenga los datos de la base de datos video, que actualmente vive en el mongod standalone del puerto 27017. El orden de pasos es: crear el directorio → arrancar un standalone temporal en 27018 → copiar los datos → detenerlo → relanzarlo ya con la configuración de replica set.

⚠
Requisito previoEl mongod standalone original (puerto 27017) debe estar corriendo. Aún no necesitas los nodos secundarios (27019, 27020).

Paso 0b.1 — Crear el directorio de datos y arrancar el standalone temporal en 27018

bash — crear directorio y arrancar mongod temporal en 27018
# 1. Crear el directorio de datos para el futuro primario mkdir -p /data/rs0-0 # 2. Arrancar mongod como standalone temporal (SIN replSetName) # Usamos --fork para que libere la terminal mongod \ --dbpath /data/rs0-0 \ --port 27018 \ --bind_ip 127.0.0.1 \ --logpath /tmp/mongod-27018-tmp.log \ --logappend \ --fork # Verificar que está corriendo mongosh --port 27018 --eval "db.adminCommand({ ping: 1 })"
✓
Respuesta esperada de ping{ ok: 1 } — el standalone en 27018 está listo para recibir datos.
ℹ
¿Por qué arrancar sin replSetName?Un mongod con replSetName configurado no acepta operaciones normales hasta que el replica set esté inicializado. Al arrancarlo primero como standalone podemos restaurar datos con mongorestore sin complicaciones; después lo relanzamos con la configuración de replica set.

Paso 0b.2 — Exportar la BD video desde el nodo 27017

mongodump crea un volcado BSON de la base de datos en un directorio local. No detiene el servidor.

bash — exportar BD video desde el standalone 27017
# Crear un directorio para el volcado mkdir -p /tmp/dump-video # Exportar SOLO la base de datos "video" del nodo standalone mongodump \ --host 127.0.0.1 \ --port 27017 \ --db video \ --out /tmp/dump-video # Verificar que se crearon los archivos BSON ls /tmp/dump-video/video/
Salida esperada
2026-10-08T12:30:00 writing video.movies to dump 2026-10-08T12:30:00 done dumping video.movies (9525 documents) ... # En /tmp/dump-video/video/ deberías ver: movies.bson movies.metadata.json comments.bson comments.metadata.json ... (una .bson por colección)

Paso 0b.2 — Importar la BD video en el futuro primario 27018

ℹ
¿El nodo 27018 ya tiene datos?Si el directorio /data/rs0-0 es nuevo y vacío, mongorestore simplemente crea la base de datos. Si ya existieran datos, usa la opción --drop para limpiar antes de importar y evitar duplicados.
bash — importar BD video en el futuro primario 27018
# Restaurar la BD video en el nodo 27018 mongorestore \ --host 127.0.0.1 \ --port 27018 \ --db video \ /tmp/dump-video/video # Opción --drop si quieres limpiar antes de restaurar: # mongorestore --host 127.0.0.1 --port 27018 --db video --drop /tmp/dump-video/video
Salida esperada
2026-10-08T12:30:05 restoring video.movies from /tmp/dump-video/video/movies.bson 2026-10-08T12:30:05 9525 document(s) restored successfully 2026-10-08T12:30:05 restoring video.comments from ... 2026-10-08T12:30:05 X document(s) restored successfully

Paso 0b.3 — Verificar la copia

mongosh :27018 — confirmar que la BD video está disponible
mongosh --port 27018 use video # Listar colecciones db.getCollectionNames() # Comparar número de documentos con el origen db.movies.countDocuments() ← debe coincidir con el 27017
✓
¿Los conteos coinciden con el nodo 27017?Ejecuta mongosh --port 27017 --eval "use video; db.movies.countDocuments()" en otra terminal y compara. Si los números son iguales, la copia fue exitosa y puedes continuar con la configuración del Replica Set.
ℹ
¿Por qué no simplemente copiar los archivos de datos?Copiar el directorio dbPath directamente funciona solo si el mongod origen está detenido (o con db.fsyncLock()). Con mongodump/mongorestore puedes hacerlo con el servidor en producción sin riesgo de corrupción.

✅ Una vez confirmada la copia, detén el mongod del 27018 si ya está corriendo sin replicación, porque en el siguiente paso lo relanzarás con la configuración de replica set.

1

Preparar directorios y archivos de configuración

Cada instancia de mongod necesita su propio directorio de datos y su propio puerto. Crearemos tres nodos en la misma máquina.

NodoRol inicialPuertoDirectorio de datos
rs0-0Primario27018/data/rs0-0
rs0-1Secundario27019/data/rs0-1
rs0-2Secundario27020/data/rs0-2
⚠
Linux / macOSLos comandos de creación de directorios asumen una ruta /data/. En macOS puedes usar ~/data/ si /data requiere permisos de root. En Windows sustituye por C:\data\rs0-0 etc. y abre una terminal como Administrador.
bash — crear directorios
# El directorio /data/rs0-0 ya fue creado en el Paso 0b.1 # Crear directorios de datos para los nodos restantes mkdir -p /data/rs0-1 /data/rs0-2 # Crear directorio para los logs mkdir -p /data/logs

Crea tres archivos de configuración YAML (uno por nodo):

/data/mongod-rs0-0.conf (nodo primario, puerto 27018)
storage: dbPath: /data/rs0-0 net: port: 27018 bindIp: 127.0.0.1 replication: replSetName: "rs0" systemLog: destination: file path: /data/logs/mongod-27018.log logAppend: true
/data/mongod-rs0-1.conf (secundario, puerto 27019)
storage: dbPath: /data/rs0-1 net: port: 27019 bindIp: 127.0.0.1 replication: replSetName: "rs0" systemLog: destination: file path: /data/logs/mongod-27019.log logAppend: true
/data/mongod-rs0-2.conf (secundario, puerto 27020)
storage: dbPath: /data/rs0-2 net: port: 27020 bindIp: 127.0.0.1 replication: replSetName: "rs0" systemLog: destination: file path: /data/logs/mongod-27020.log logAppend: true
2

Iniciar los tres nodos mongod

Abre tres terminales separadas (o usa & / screen / tmux) y lanza cada nodo:

Terminal 1 — nodo rs0-0 (27018)
mongod --config /data/mongod-rs0-0.conf
Terminal 2 — nodo rs0-1 (27019)
mongod --config /data/mongod-rs0-1.conf
Terminal 3 — nodo rs0-2 (27020)
mongod --config /data/mongod-rs0-2.conf
ℹ
Verificar que los tres procesos están corriendoEn una cuarta terminal ejecuta pgrep -a mongod (Linux/macOS) o tasklist | findstr mongod (Windows). Debes ver tres PIDs distintos.
3

Inicializar el Replica Set

Con los tres procesos corriendo, conéctate al primario candidato (puerto 27018) e inicializa el replica set:

mongosh — conectar al nodo 27018 e inicializar
# Conectar al nodo 27018 mongosh --port 27018 # Dentro de mongosh: inicializar el Replica Set rs.initiate({ _id: "rs0", members: [ { _id: 0, host: "127.0.0.1:27018", priority: 2 }, { _id: 1, host: "127.0.0.1:27019", priority: 1 }, { _id: 2, host: "127.0.0.1:27020", priority: 1 } ] })
✓
Respuesta esperada{ ok: 1 } — el replica set se está formando. Espera ~10 segundos y verás que el prompt cambia a rs0 [direct: primary]>

Verificar el estado del replica set:

mongosh — rs.status()
rs.status()
Salida esperada (extracto)
set: 'rs0', members: [ { _id: 0, name: '127.0.0.1:27018', health: 1, state: 1, stateStr: 'PRIMARY', ← nodo principal ... }, { _id: 1, name: '127.0.0.1:27019', stateStr: 'SECONDARY', ← réplica lista ... }, { _id: 2, name: '127.0.0.1:27020', stateStr: 'SECONDARY', ... } ]
ℹ
¿Qué significa priority: 2 en el nodo 0?La prioridad determina qué nodo tiene más probabilidad de ser elegido primario en una votación. Con priority: 2, el nodo 27018 siempre intentará ser primario cuando esté disponible. Los demás tienen priority: 1.
4

Usar una base de datos existente y verificar la replicación

La base de datos video ya está en el nodo primario (copiada en el Paso 0b). Al ejecutar rs.initiate(), MongoDB hizo la initial sync y replicó automáticamente todos los datos a los secundarios. Vamos a comprobarlo consultando la colección movieDetails y luego añadiremos un nuevo documento para observar su replicación en tiempo real.

✓
La BD video ya está replicadaCuando el replica set se inicializó, los secundarios copiaron automáticamente todos los datos del primario (incluyendo video.movieDetails). No necesitas hacer nada adicional para los datos históricos.

4.1 — Consultar movieDetails en el primario y contar documentos

mongosh :27018 — consultas sobre movieDetails
# Conectar al primario mongosh --port 27018 use video # Total de películas en la colección db.movieDetails.countDocuments() # Ver un documento completo de ejemplo db.movieDetails.findOne({ title: "Once Upon a Time in the West" }) # Películas con rating IMDb ≥ 8.5, ordenadas por rating descendente db.movieDetails.find( { "imdb.rating": { $gte: 8.5 } }, { title: 1, year: 1, "imdb.rating": 1, _id: 0 } ).sort({ "imdb.rating": -1 }).limit(5)
Salida esperada (películas top IMDb)
[ { title: 'Once Upon a Time in the West', year: 1968, imdb: { rating: 8.6 } }, { title: 'The Godfather', year: 1972, imdb: { rating: 9.2 } }, ... ]

4.2 — Insertar una nueva película en el primario y verificar replicación en Secundario 1 (27019)

mongosh :27018 — insertar una nueva película
# En el primario — insertar una película nueva use video db.movieDetails.insertOne({ title: "El laberinto del fauno", year: 2006, rated: "R", runtime: 118, countries: ["España", "México"], genres: ["Drama", "Fantasy"], director: "Guillermo del Toro", imdb: { id: "tt0457430", rating: 8.2, votes: 680000 }, tomato: { meter: 95, userMeter: 91 }, awards: { wins: 65, nominations: 105, text: "65 wins & 105 nominations." }, type: "movie" })

Abre una nueva terminal, conéctate al Secundario 1 y verifica que el documento apareció:

mongosh :27019 — leer en el Secundario 1
# Conectar al secundario en 27019 mongosh --port 27019 # Habilitar lectura en el secundario db.getMongo().setReadPref("secondaryPreferred") use video db.movieDetails.findOne({ title: "El laberinto del fauno" })
✓
Resultado esperadoEl documento recién insertado en el primario aparece en el secundario. La replicación en redes locales ocurre en milisegundos.

4.3 — Comprobar el oplog

El oplog registra el insert anterior. Inspecciónalo desde el primario:

mongosh :27018 — inspeccionar el oplog
use local db.oplog.rs.find( { ns: "video.movieDetails" }, { op: 1, ns: 1, "o.title": 1, wall: 1 } ).sort({ wall: -1 }).limit(5).pretty()
Salida esperada
{ op: 'i', ← insert ns: 'video.movieDetails', o: { title: 'El laberinto del fauno', ... }, wall: ISODate('2026-10-08T...') }
ℹ
¿Cómo funciona el oplog internamente?El oplog es una capped collection (de tamaño fijo) en la base de datos local. Cada secundario mantiene un cursor abierto sobre el oplog del primario y aplica las operaciones en orden idempotente. Si un secundario se desconecta y se reconecta, retoma desde su último timestamp registrado.
5

Verificar que las modificaciones se replican

Realiza una actualización en el primario y comprueba que se propaga a ambos secundarios.

mongosh :27018 — actualizar un documento en movieDetails
# En el primario (27018) use video # Actualizar el Tomatometer y añadir un campo de reseña editorial db.movieDetails.updateOne( { title: "Once Upon a Time in the West" }, { $set: { "tomato.meter": 99, editorial: "Obra maestra del spaghetti western" }, $inc: { "imdb.votes": 5000 } } )
mongosh :27020 — confirmar en el Secundario 2
# En el secundario 27020 (nueva terminal) mongosh --port 27020 db.getMongo().setReadPref("secondaryPreferred") use video db.movieDetails.findOne( { title: "Once Upon a Time in the West" }, { title: 1, "tomato.meter": 1, "imdb.votes": 1, editorial: 1, _id: 0 } )
Resultado esperado en el secundario
{ title: 'Once Upon a Time in the West', editorial: 'Obra maestra del spaghetti western', ← campo nuevo replicado imdb: { votes: 206283 }, ← 201283 + 5000, replicado tomato: { meter: 99 } ← actualizado desde 98 }

Medir el retraso de replicación

mongosh :27018 — rs.printSecondaryReplicationInfo()
rs.printSecondaryReplicationInfo()
Salida esperada
source: 127.0.0.1:27019 { syncedTo: 'Thu Oct 08 2026 10:35:14', replLag: '0 secs' ← retraso en segundos } source: 127.0.0.1:27020 { syncedTo: 'Thu Oct 08 2026 10:35:14', replLag: '0 secs' }
6

Añadir un nuevo nodo secundario (puerto 27021)

Puedes agregar miembros al replica set en caliente, sin detener el clúster.

  1. Crea el directorio de datos y el archivo de configuración del nuevo nodo.
  2. Inicia el procesomongoddel cuarto nodo.
  3. Desde el primario, usars.add()para incorporarlo al set.
  4. MongoDB realiza lainitial sync: el nuevo nodo copia todos los datos del primario automáticamente.
bash — crear directorio y configuración del nodo 4
mkdir -p /data/rs0-3
/data/mongod-rs0-3.conf
storage: dbPath: /data/rs0-3 net: port: 27021 bindIp: 127.0.0.1 replication: replSetName: "rs0" systemLog: destination: file path: /data/logs/mongod-27021.log logAppend: true
Terminal 4 — iniciar el nuevo nodo
mongod --config /data/mongod-rs0-3.conf
mongosh :27018 — agregar el nodo al Replica Set
# Desde el primario rs.add("127.0.0.1:27021") # Verificar que ya aparece como SECONDARY rs.status()
ℹ
Initial syncEl nuevo nodo aparecerá en estado STARTUP2 mientras copia los datos. Dependiendo del tamaño de la base de datos tardará segundos o minutos. Monitorea el progreso con rs.status() repetidamente hasta ver SECONDARY.
⚠
Número impar de votantesAhora el set tiene 4 miembros. Para mantener un quórum claro se recomienda un número impar. Puedes agregar unárbitro(no almacena datos, solo vota) con rs.addArb("127.0.0.1:27021"), o configurar el nodo 3 como votes: 0 para que no vote.
7

Eliminar el primario y observar la elección automática

Simularemos la caída del nodo primario (27018) para observar cómo el replica set elige automáticamente un nuevo primario.

🔴
AtenciónMatar el proceso primario genera ~10–30 segundos de indisponibilidad de escritura mientras dura la elección. Es exactamente lo que queremos observar.

7.1 — Apagar el primario

Desde el primario, usa el comando stepDown (forma controlada) o simplemente mata el proceso:

Opción A — stepDown controlado (desde mongosh :27018)
# El primario cede su rol voluntariamente durante 60 s rs.stepDown(60)
Opción B — matar el proceso (bash)
# Encontrar el PID del mongod en 27018 lsof -i :27018 | grep LISTEN # Matar el proceso (reemplaza 12345 con el PID real) kill -9 12345

7.2 — Observar la elección desde un secundario

mongosh :27019 — monitorear el estado cada 2 s
# Conectar a un secundario sobreviviente mongosh --port 27019 # Monitorear repetidamente (Ctrl+C para detener) while(true) { let status = rs.status() status.members.forEach(m => { print(`${m.name.padEnd(22)} ${m.stateStr}`) }) print("─────────────────────────────") sleep(2000) }
Evolución del estado durante la elección
── t=0: caída del primario ────────────── 127.0.0.1:27018 DOWN 127.0.0.1:27019 SECONDARY 127.0.0.1:27020 SECONDARY 127.0.0.1:27021 SECONDARY ── t=2s: detección de falla ───────────── 127.0.0.1:27018 UNKNOWN 127.0.0.1:27019 RECOVERING 127.0.0.1:27020 SECONDARY 127.0.0.1:27021 SECONDARY ── t=12s: nueva elección completada ───── 127.0.0.1:27018 DOWN 127.0.0.1:27019 PRIMARY ← nuevo primario elegido 127.0.0.1:27020 SECONDARY 127.0.0.1:27021 SECONDARY
ℹ
¿Por qué 27019 fue elegido y no 27020?Ambos tienen priority: 1. La elección la gana el nodo con eloplog más actualizado(mayor timestamp). En condiciones normales, ambos están sincronizados y cualquiera puede ganar; la votación es aleatoria con ese desempate.

7.3 — Reiniciar el nodo original y observar su regreso como secundario

bash — reiniciar el nodo 27018
mongod --config /data/mongod-rs0-0.conf
✓
¿Qué ocurre al reconectarse?El nodo 27018 detecta que existe un primario (27019) y entra en modo SECONDARY. Como tiene priority: 2, después de un breve periodo MongoDB ejecutará una nueva elección y 27018 recuperará automáticamente el rol de primario si así lo configuramos.
8

Controlar lectura y escritura en nodos secundarios

Por defecto, los secundarios no aceptan lecturas ni escrituras directas. Las escrituras siempre van al primario. Para leer de un secundario hay que indicarlo explícitamente. En esta sección se explica cómo controlar este comportamiento a nivel de conexión y a nivel del replica set.

8.1 — Comportamiento por defecto: intentar leer un secundario sin configuración

mongosh :27019 — sin setReadPref (falla intencionalmente)
mongosh --port 27019 use universidad db.estudiantes.find() ← ejecutar sin setReadPref
Error esperado
MongoServerError: not primary and secondaryOk=false - consider using db.getMongo().setReadPref() or connecting to a secondary
✓
Este error es correcto y esperado.MongoDB protege la coherencia de lectura por defecto. Para leer de un secundario hay que ser explícito.

8.2 — Preferencias de lectura (Read Preferences)

MongoDB ofrece cinco modos. Elige según tus necesidades de coherencia y latencia:

ModoDescripciónCuándo usarlo
primarySolo el primario (defecto)Lecturas con coherencia fuerte
primaryPreferredPrimario si disponible, si no secundarioAlta disponibilidad con coherencia preferida
secondarySolo secundarios (round-robin)Reportes, analítica, sin impacto al primario
secondaryPreferredSecundario si disponible, si no primarioDistribución de carga de lectura
nearestNodo con menor latencia de redAplicaciones geográficamente distribuidas
mongosh — configurar preferencias de lectura
── Leer SOLO del primario (coherencia fuerte) ── db.getMongo().setReadPref("primary") ── Permitir leer de cualquier secundario ────── db.getMongo().setReadPref("secondary") ── Secundario preferido, primario si no hay ── db.getMongo().setReadPref("secondaryPreferred") ── Con tag para elegir un secundario específico db.getMongo().setReadPref("secondary", [{ dc: "mexico" }])

8.3 — Bloquear totalmente la lectura en un secundario específico

Puedes deshabilitar que un nodo vote y que sea elegible como primario, convirtiéndolo en un nodo oculto (hidden member). Un nodo oculto no aparece en la topología de clientes y sólo puede ser leído con la preferencia secondary y tag explícito. Es útil para backups o reportes sin afectar el clúster principal.

mongosh :27018 — convertir el nodo 27021 en nodo oculto
# Obtener la configuración actual let cfg = rs.conf() # El nodo 27021 está en el índice 3 del array members cfg.members[3].hidden = true cfg.members[3].priority = 0 ← obligatorio cuando hidden: true rs.reconfig(cfg)
Verificar: el nodo oculto no aparece en isMaster / hello
# Desde cualquier nodo db.hello() # El campo "hosts" NO incluirá 127.0.0.1:27021 # pero rs.status() sí lo mostrará como SECONDARY

8.4 — Impedir escrituras accidentales en un secundario

Incluso si alguien se conecta directamente a un secundario, MongoDB impide la escritura por defecto. Pero si se usa --directConnection en la cadena de conexión, es posible intentarlo. Esto siempre falla con un error claro:

mongosh — intento de escritura directa en secundario
mongosh "mongodb://127.0.0.1:27019/?directConnection=true" use video db.movieDetails.insertOne({ title: "Test desde secundario", year: 2026 })
Error esperado
MongoServerError: not primary Writes to secondaries are not supported.
✓
MongoDB garantiza esto a nivel del servidor.No se puede escribir en un secundario sin antes convertirlo en primario. Esta restricción no se puede desactivar; es parte del protocolo del replica set.

8.5 — Write Concern: controlar cuántos nodos confirman una escritura

Puedes hacer que MongoDB espere a que un número determinado de nodos confirme la escritura antes de responder al cliente:

mongosh :27018 — escritura con confirmación de la mayoría
use video # w:"majority" — esperar a que la mayoría de nodos confirme db.movieDetails.insertOne( { title: "Roma", year: 2018, director: "Alfonso Cuarón", genres: ["Drama"], imdb: { id: "tt6155172", rating: 7.7, votes: 220000 }, awards: { wins: 204, nominations: 297, text: "Won 3 Oscars." }, type: "movie" }, { writeConcern: { w: "majority", j: true, wtimeout: 5000 } } ) # w:3 — esperar a que exactamente 3 nodos confirmen db.movieDetails.insertOne( { title: "Amores perros", year: 2000, director: "Alejandro González Iñárritu", genres: ["Drama", "Thriller"], imdb: { id: "tt0245712", rating: 8.0, votes: 135000 }, type: "movie" }, { writeConcern: { w: 3 } } )
ℹ
Write Concern vs Read Preference
Write Concerncontrola cuántos nodos deben confirmar una escritura (durabilidad).Read Preferencecontrola desde qué nodo se lee (disponibilidad/latencia). Son conceptos ortogonales y se combinan para ajustar la consistencia de tu aplicación.
✓

Resumen y puntos clave

Inicialización

rs.initiate() une a los nodos en un conjunto. El nodo con mayor priority se convierte en primario.

Replicación automática

Todo lo que se escribe en el primario queda en el oplog y se aplica en los secundarios en milisegundos en redes locales.

Elección automática

Si el primario falla, los nodos restantes votan y eligen uno nuevo en ~10–30 s, sin intervención manual.

Nodos ocultos

hidden: true + priority: 0 crea un nodo invisible para los clientes: ideal para backups o analítica sin afectar al clúster de producción.

Escritura solo en primario

MongoDB rechaza escrituras en secundarios con error not primary. Esta restricción es del servidor y no puede desactivarse.

Write Concern

w: "majority" garantiza que la escritura sobrevive a la caída del primario porque ya se replicó en la mayoría de nodos.