Puedes reconstruir srv-tramontana en cincuenta minutos. Puedes probar cualquier cambio antes de aplicarlo, detectar una intrusión, cifrar los secretos, diagnosticar una latencia anómala y recuperar un arranque roto. Y aun así, si esa máquina se cae, Tramontana Reservas está caída.
Un disco que falla, una fuente de alimentación, un corte de red del proveedor, un kernel que no arranca tras una actualización, o simplemente el mantenimiento programado de la infraestructura donde vive. En el mejor de los casos, cincuenta minutos de interrupción — y eso suponiendo que alguien esté despierto para lanzar el playbook. Todo el trabajo de siete módulos descansa sobre una única máquina, que es la definición exacta de un punto único de fallo.
Esta lección ataca ese problema, y lo hace con una advertencia por delante: la alta disponibilidad es cara, compleja y, mal hecha, empeora la fiabilidad en lugar de mejorarla. Un sistema con más piezas tiene más formas de fallar. La lección termina con el análisis honesto de si Tramontana necesita esto, porque la respuesta profesional no siempre es que sí.
Contenido
- Vocabulario: disponibilidad, nueves, MTBF y MTTR
- Escalado vertical y horizontal
- Redundancia: activo-pasivo y activo-activo
- El estado es el problema difícil
- Balanceo de carga: capas, algoritmos y comprobaciones de salud
- HAProxy en la práctica
- Alta disponibilidad del balanceador: keepalived y VRRP
- Split-brain y quórum
- Replicación de datos
- Clústeres, Pacemaker y fencing
- Diseño para Tramontana y análisis de coste
- Probar los fallos provocándolos
Vocabulario: disponibilidad, nueves, MTBF y MTTR
El área está llena de términos que se usan como sinónimos y no lo son. Fijarlos ahorra malentendidos caros.
La disponibilidad es la fracción de tiempo en que el servicio responde correctamente. Se expresa en porcentaje, y en «nueves»:
| Disponibilidad | Nombre | Caída máxima al año | Al mes | A la semana |
|---|---|---|---|---|
| 99 % | «dos nueves» | 3 días 15 h | 7 h 18 min | 1 h 41 min |
| 99,9 % | «tres nueves» | 8 h 46 min | 43 min 50 s | 10 min 5 s |
| 99,95 % | 4 h 23 min | 21 min 55 s | 5 min 2 s | |
| 99,99 % | «cuatro nueves» | 52 min 34 s | 4 min 23 s | 1 min |
| 99,999 % | «cinco nueves» | 5 min 15 s | 26 s | 6 s |
Mirar esa tabla despacio corrige la intuición de casi todo el mundo. 99,99 % significa menos de un minuto de caída a la semana, incluidas las actualizaciones, los reinicios por kernel nuevo y los fallos del proveedor. Con un único servidor, un solo reinicio mensual de tres minutos ya te sitúa por debajo de tres nueves y medio. Y cada nueve adicional multiplica el coste aproximadamente por tres.
Tres distinciones que conviene tener claras:
- Fiabilidad es la probabilidad de funcionar sin fallo durante un intervalo. Un sistema puede ser muy disponible y poco fiable: falla a menudo, pero se recupera en segundos.
- Durabilidad se refiere a los datos: la probabilidad de no perderlos. Es independiente de la disponibilidad — un sistema puede estar caído y tener los datos perfectamente a salvo.
- MTBF (tiempo medio entre fallos) y MTTR (tiempo medio de reparación) descomponen la disponibilidad:
Y esa fórmula contiene la decisión estratégica de toda la lección: hay dos formas de subir la disponibilidad. Aumentar el MTBF —fallar menos: mejor hardware, más pruebas, menos cambios— o reducir el MTTR —recuperarse antes—. En la práctica, reducir el MTTR es casi siempre más barato y más eficaz, porque el MTBF tiene un techo físico y el MTTR no.
Es exactamente lo que hiciste en 07-06 sin llamarlo así: pasar el MTTR de 8 horas a 50 minutos multiplicó la disponibilidad sin comprar un solo componente.
Y la relación con lo que ya tienes acordado con Marta:
| Métrica | Qué mide | Valor actual |
|---|---|---|
| RPO | Cuántos datos se pueden perder | 4 horas |
| RTO | Cuánto puede durar la interrupción | 2 horas (revisado en 07-06) |
| Disponibilidad | Fracción de tiempo funcionando | Sin objetivo formal |
Hay un hueco ahí: nadie ha fijado un objetivo de disponibilidad. Y sin él, no se puede decidir cuánto invertir.
Escalado vertical y horizontal
| Vertical (scale up) | Horizontal (scale out) | |
|---|---|---|
| Qué se hace | Máquina más grande | Más máquinas |
| Límite | El hardware más grande que existe | Prácticamente ninguno |
| Coste | Crece más que linealmente | Aproximadamente lineal |
| Complejidad | Ninguna | Alta: reparto, estado, coherencia |
| Efecto en disponibilidad | Ninguno: sigue habiendo un punto único de fallo | Es lo que la habilita |
| Requiere cambios en la aplicación | No | Casi siempre sí |
La fila que importa aquí es la penúltima. Duplicar la memoria de srv-tramontana la haría más rápida y exactamente igual de frágil. El escalado horizontal es lo que permite que el fallo de una máquina no sea el fallo del servicio, y por eso esta lección va de escalado horizontal aunque el problema no sea de capacidad.
Redundancia: activo-pasivo y activo-activo
Tener dos máquinas no es tener redundancia: hay que decidir qué hace cada una.
| Activo-pasivo | Activo-activo | |
|---|---|---|
| El segundo nodo | Espera, sin atender tráfico | Atiende tráfico también |
| Aprovechamiento | 50 % del hardware | ~100 % |
| Tiempo de conmutación | Segundos a minutos | Inmediato |
| Complejidad | Media | Alta |
| Exige que la aplicación… | Arranque en el otro nodo | Sea capaz de correr en varias instancias a la vez |
| Riesgo característico | Que el pasivo no funcione cuando haga falta | Split-brain, coherencia de datos |
El riesgo del activo-pasivo se subestima siempre y merece nombre propio: un nodo pasivo que nunca se usa es un nodo del que no sabes si funciona. Se instaló hace ocho meses, no ha recibido las últimas actualizaciones, tiene un certificado caducado o un disco lleno. El día del fallo, descubres que el respaldo también falla. La única defensa es usarlo periódicamente: conmutar a propósito cada pocas semanas.
El estado es el problema difícil
Aquí está el núcleo conceptual de la lección, y lo que separa un diseño que funciona de uno que parece funcionar.
Replicar procesos es fácil: arrancas la misma aplicación en dos máquinas y ya tienes dos. Replicar estado es difícil, y el estado está en más sitios de los que parece.
Inventario del estado de Tramontana Reservas:
| Estado | Dónde vive | Problema al duplicar |
|---|---|---|
| Datos de reservas | PostgreSQL | El problema central: dos bases de datos divergen |
| Ficheros subidos | /opt/tramontana/shared/uploads |
Una foto subida al nodo A no está en el B |
| Sesiones de usuario | En memoria del proceso | Una petición al nodo B no reconoce la sesión iniciada en A |
| Registros | /var/log/tramontana/ |
Se reparten entre nodos: hay que centralizar para diagnosticar |
| Tareas programadas | Timers de systemd | Se ejecutarían DOS veces: dos copias de seguridad, dos purgas |
| Certificado TLS | /etc/letsencrypt/ |
Hay que sincronizarlo o terminarlo en el balanceador |
Esa fila de las tareas programadas es la que más problemas da en la práctica, porque nadie piensa en ella al duplicar un servidor. Con dos nodos idénticos, tramontana-respaldo.timer se dispara a las 02:30 en ambos: dos copias simultáneas al mismo destino, compitiendo por el flock —que es local a cada máquina y por tanto no protege nada—. La solución exige un bloqueo distribuido o ejecutar las tareas solo en un nodo designado.
Una aplicación sin estado (stateless) es aquella cuyas instancias no guardan nada entre peticiones: todo el estado está en la base de datos o en un almacén compartido. Es el requisito para el activo-activo, y casi nunca se cumple sin trabajo de desarrollo. En Tramontana:
| Componente | ¿Sin estado? | Qué haría falta |
|---|---|---|
| Servir páginas y consultar reservas | Sí | Nada |
| Sesiones de usuario | No | Moverlas a Redis o a la base de datos |
| Subida de fotos | No | Almacenamiento compartido o almacén de objetos |
| Base de datos | No, por definición | Replicación |
Conclusión honesta: Tramontana no es una aplicación sin estado hoy. Antes de montar nada, hacen falta dos cambios en la aplicación: sesiones externalizadas y ficheros en almacenamiento compartido. Eso es trabajo de desarrollo, y decírselo a Marta antes de comprar servidores es parte del trabajo.
Balanceo de carga: capas, algoritmos y comprobaciones de salud
Un balanceador reparte peticiones entre varios servidores. Opera en una de dos capas:
| Capa 4 (transporte) | Capa 7 (aplicación) | |
|---|---|---|
| Qué inspecciona | IP y puerto | Cabeceras HTTP, rutas, cookies |
| Rendimiento | Muy alto | Alto, pero menor |
| Terminación TLS | No puede | Sí |
| Reparto por ruta o dominio | No | Sí |
| Reintento de una petición fallida | No | Sí |
| Ejemplos | IPVS, nftables, NLB |
HAProxy, Nginx, Traefik |
Para HTTP, la capa 7 es casi siempre la correcta: permite terminar TLS en un punto, repartir según la ruta, reintentar una petición que falla y hacer comprobaciones de salud significativas.
Algoritmos
| Algoritmo | Cómo reparte | Cuándo |
|---|---|---|
roundrobin |
Por turnos | Peticiones homogéneas, servidores iguales |
leastconn |
Al que menos conexiones tiene | Peticiones de duración variable |
source |
Hash de la IP de origen | Cuando hace falta afinidad (con reservas) |
uri |
Hash de la ruta | Cachés: la misma ruta siempre al mismo nodo |
random |
Al azar, con dos sondeos | Muchos balanceadores en paralelo |
leastconn es el más adecuado para Tramontana: una consulta de disponibilidad tarda 40 ms y un informe de facturación varios segundos, así que repartir por turnos amontonaría las lentas en el mismo nodo.
Comprobaciones de salud
Es la pieza que hace útil el balanceador: sin ella, seguiría enviando tráfico a un servidor caído.
| Tipo | Cómo funciona | Ventaja |
|---|---|---|
| Activa | El balanceador sondea cada N segundos | Detecta el fallo aunque no haya tráfico |
| Pasiva | Observa los errores del tráfico real | Sin coste añadido; detecta fallos parciales |
Lo correcto es usar ambas. Y aquí encaja algo que tienes desde el Módulo 4: revision_salud.sh devuelve 0, 1 o 2 — correcto, aviso, crítico. Esa gradación es exactamente lo que un balanceador necesita, si la aplicación expone un extremo equivalente:
| Estado | HTTP | Qué debe hacer el balanceador |
|---|---|---|
| 0 correcto | 200 | Enviar tráfico normalmente |
| 1 aviso | 200 con cabecera de aviso | Enviar, pero con menos peso |
| 2 crítico | 503 | No enviar tráfico |
Y la distinción que casi nadie hace bien: una comprobación superficial es peor que ninguna. Si el extremo de salud solo responde «estoy vivo» sin comprobar la base de datos, el balanceador seguirá enviando tráfico a un nodo que no puede atender ninguna petición. Si comprueba demasiado —por ejemplo, una consulta pesada—, un problema de la base de datos marca todos los nodos como caídos y el servicio desaparece por completo, cuando podría haber servido al menos las páginas estáticas.
El equilibrio: comprobar las dependencias críticas con una operación barata. Una consulta SELECT 1 con tiempo límite, no un informe.
Sesiones persistentes: un antipatrón
La sticky session ata a cada cliente a un nodo, por cookie o por IP. Resuelve el problema de las sesiones en memoria... y crea otros:
- El reparto se desequilibra: un nodo puede acabar con el triple de carga.
- Cuando un nodo cae, todos sus usuarios pierden la sesión de golpe.
- Impide drenar un nodo para mantenimiento sin afectar a nadie.
- Enmascara el problema real en lugar de resolverlo.
Es una solución transitoria aceptable mientras se externalizan las sesiones. Como solución permanente, no.
Drenaje de conexiones
Para desplegar sin cortar el servicio, un nodo tiene que poder salir del reparto ordenadamente: dejar de recibir peticiones nuevas y terminar las que ya tiene en curso.
# 1. Sacar el nodo del reparto (deja de recibir peticiones nuevas)
$ echo "set server tramontana/app1 state drain" | \
sudo socat stdio /run/haproxy/admin.sock
# 2. Esperar a que terminen las conexiones en curso
$ while [[ "$(echo 'show stat' | sudo socat stdio /run/haproxy/admin.sock \
| awk -F, '$1=="tramontana" && $2=="app1" {print $5}')" != "0" ]]; do
sleep 1
done
# 3. Desplegar con tranquilidad
$ ssh [email protected] 'sudo ~/scripts/desplegar.sh 3.3.0'
# 4. Devolverlo al reparto
$ echo "set server tramontana/app1 state ready" | \
sudo socat stdio /run/haproxy/admin.sockRepetido nodo a nodo, eso es un despliegue sin interrupción. Y es la respuesta a algo que quedó pendiente desde 04-07: desplegar.sh reinicia el servicio, y ese reinicio son unos segundos de peticiones rechazadas. Con dos nodos y drenaje, cero.
HAProxy en la práctica
# /etc/haproxy/haproxy.cfg
global
log /dev/log local0
chroot /var/lib/haproxy
# Socket de administracion: permite drenar nodos en caliente
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
maxconn 4000
# Perfil TLS moderno (06-05). Nunca inventes la lista de suites.
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
defaults
log global
mode http
option httplog
option dontlognull
# Reintenta en OTRO servidor si el primero falla. Solo seguro para
# peticiones idempotentes: por eso 'if-none', que excluye POST.
retry-on all-retryable-errors
option redispatch
retries 3
timeout connect 5s
timeout client 30s
timeout server 30s
timeout http-request 10s
# Drenaje: espera a que terminen las conexiones al sacar un nodo
default-server init-addr last,libc,none
# ---------- Entrada ----------
frontend tramontana_https
bind *:443 ssl crt /etc/haproxy/certs/reservas.tramontana.example.pem alpn h2,http/1.1
bind *:80
# Redirige HTTP a HTTPS, salvo el desafio de Let's Encrypt
acl acme_challenge path_beg /.well-known/acme-challenge/
http-request redirect scheme https code 301 unless { ssl_fc } || acme_challenge
# HSTS (06-05). Empezar con max-age corto y subirlo despues.
http-response set-header Strict-Transport-Security "max-age=63072000"
# Limite de tasa por IP: complementa a fail2ban en la capa 7
stick-table type ip size 100k expire 60s store http_req_rate(10s)
http-request track-sc0 src
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }
default_backend tramontana_app
# ---------- Servidores de aplicacion ----------
backend tramontana_app
# leastconn: las peticiones tienen duracion muy variable
balance leastconn
# Comprobacion de salud ACTIVA en la capa 7.
# El extremo /salud comprueba las dependencias criticas de forma barata.
option httpchk
http-check send meth GET uri /salud ver HTTP/1.1 hdr Host reservas.tramontana.example
http-check expect status 200
# Cabeceras para que la aplicacion sepa quien es el cliente real
http-request set-header X-Forwarded-Proto https if { ssl_fc }
option forwardfor
# inter: cada cuanto sondear. rise/fall: cuantas veces seguidas
# para considerar el nodo sano o caido. slowstart: al volver, sube
# el peso progresivamente en vez de recibir toda la carga de golpe.
server app1 10.0.2.21:8080 check inter 3s rise 2 fall 3 slowstart 30s maxconn 200
server app2 10.0.2.22:8080 check inter 3s rise 2 fall 3 slowstart 30s maxconn 200
# Comprobacion PASIVA: si un servidor da errores en el trafico real,
# se saca temporalmente aunque su /salud responda.
option redispatch
# ---------- Estadisticas, solo desde la red interna ----------
listen stats
bind 10.0.2.20:8404
stats enable
stats uri /
stats refresh 5s
stats admin if TRUE
acl red_interna src 10.0.2.0/24
http-request deny unless red_interna$ sudo haproxy -c -f /etc/haproxy/haproxy.cfg
Configuration file is valid
$ sudo systemctl reload haproxy
$ echo "show stat" | sudo socat stdio /run/haproxy/admin.sock | \
awk -F, 'NR==1 || $1=="tramontana_app" {print $1","$2","$18","$5}'
# pxname,svname,status,scur
tramontana_app,app1,UP,12
tramontana_app,app2,UP,9
tramontana_app,BACKEND,UP,21Cuatro detalles de esa configuración que merecen explicación:
slowstart 30s. Cuando un nodo vuelve tras un fallo, sus cachés están frías y sus grupos de conexiones vacíos. Enviarle un tercio del tráfico de golpe puede tumbarlo otra vez, y entrar en un ciclo de caídas. slowstart sube su peso progresivamente durante 30 segundos.
rise 2 fall 3. Asimétrico a propósito: se necesitan tres fallos seguidos para sacar un nodo (evita falsos positivos por un pico) pero solo dos éxitos para devolverlo. Sacar de golpe por un fallo transitorio es cómo un balanceador provoca una caída.
option redispatch con retry-on. Si el servidor elegido falla, reintenta en otro. Con un matiz importante: reintentar un POST puede duplicar una reserva. HAProxy solo reintenta cuando la petición no se ha enviado o el método es idempotente.
El stick-table con límite de tasa. Es fail2ban de 06-03 en la capa 7: cuenta peticiones por IP en ventanas de 10 segundos y devuelve 429 al superar el umbral. Complementario, no sustituto: fail2ban bloquea a nivel de red y este a nivel de aplicación.
Nginx puede hacer lo mismo con upstream, y es la opción natural si ya lo tienes como proxy inverso — que es lo que montarás en 08-01:
upstream tramontana {
least_conn;
server 10.0.2.21:8080 max_fails=3 fail_timeout=30s;
server 10.0.2.22:8080 max_fails=3 fail_timeout=30s;
}La diferencia práctica: Nginx solo hace comprobaciones pasivas en su versión libre; las activas son de la versión comercial. HAProxy las trae de serie, y por eso es preferible cuando el balanceo es la función principal.
Alta disponibilidad del balanceador: keepalived y VRRP
Y aquí está el error de diseño más común del área, tan frecuente que merece enunciarse solo:
Poner un balanceador delante de dos servidores no elimina el punto único de fallo: lo mueve al balanceador.
Antes tenías una máquina que podía caer. Ahora tienes tres, y si cae la que reparte, el servicio desaparece igual — con la agravante de que ahora hay más piezas que pueden fallar.
La solución es VRRP (Virtual Router Redundancy Protocol): dos balanceadores comparten una IP virtual flotante, la que apunta el DNS. Uno la tiene; si deja de anunciarse, el otro se la queda en segundos.
# /etc/keepalived/keepalived.conf — NODO PRINCIPAL (lb1, 10.0.2.20)
global_defs {
router_id lb1
enable_script_security
script_user keepalived_script
}
# Comprobacion del servicio LOCAL. Si HAProxy muere en este nodo, la
# prioridad baja y el otro se queda la IP. Sin esto, un nodo con
# keepalived vivo y HAProxy muerto retiene la IP y el servicio cae.
vrrp_script comprobar_haproxy {
script "/usr/bin/killall -0 haproxy"
interval 2
weight -40 # baja la prioridad 40 puntos si falla
fall 2
rise 2
}
vrrp_instance TRAMONTANA_VIP {
state MASTER
interface enp0s3
virtual_router_id 51 # IDENTICO en ambos nodos
priority 100 # mayor que el respaldo
advert_int 1 # anuncio cada segundo
authentication {
auth_type PASS
auth_pass {{ vault_vrrp_pass }}
}
virtual_ipaddress {
10.0.2.30/24 dev enp0s3
}
track_script {
comprobar_haproxy
}
notify_master "/usr/local/sbin/vrrp-notificar master"
notify_backup "/usr/local/sbin/vrrp-notificar backup"
notify_fault "/usr/local/sbin/vrrp-notificar fault"
}# NODO DE RESPALDO (lb2, 10.0.2.21): identico salvo tres lineas
state BACKUP
priority 90
router_id lb2$ sudo systemctl enable --now keepalived
$ ip -brief addr show enp0s3
enp0s3 UP 10.0.2.20/24 10.0.2.30/24 # <- la VIP esta en lb1
# En lb2, la VIP NO aparece: esta en espera
$ ssh [email protected] 'ip -brief addr show enp0s3'
enp0s3 UP 10.0.2.21/24El mecanismo: el nodo con más prioridad anuncia la VIP cada segundo. Si el respaldo deja de oír anuncios durante tres intervalos, asume que el principal ha caído, se asigna la IP y envía un ARP gratuito para que los conmutadores de la red actualicen sus tablas. La conmutación tarda entre 3 y 4 segundos.
El track_script con weight -40 es lo que evita el fallo más tonto de esta arquitectura: sin él, un nodo donde keepalived funciona pero HAProxy está muerto retiene la VIP alegremente, y el servicio queda caído aunque el otro balanceador esté perfectamente. Con él, la prioridad de lb1 baja de 100 a 60, por debajo de los 90 de lb2, y la VIP se mueve.
Y enable_script_security con script_user es una precaución de seguridad real: keepalived corre como root y ejecuta esos scripts; sin esas directivas, un script con permisos laxos es una escalada de privilegios.
Split-brain y quórum
El fallo característico de cualquier sistema redundante, y el que causa las pérdidas de datos más graves.
Split-brain ocurre cuando los nodos dejan de verse entre sí pero ambos siguen funcionando. Cada uno concluye que el otro ha caído, y ambos se declaran principales:
- Dos balanceadores anuncian la misma IP virtual → tráfico impredecible, conexiones cortadas.
- Dos bases de datos aceptan escrituras → los datos divergen, y reconciliarlos después puede ser imposible.
El escenario que lo provoca no es que un nodo caiga —eso se maneja bien—, sino que la red entre ellos se corte mientras ambos siguen vivos y atendiendo clientes. Es más frecuente de lo que parece: un conmutador que falla, una regla de firewall mal puesta, una saturación momentánea.
Las tres defensas:
| Defensa | Cómo funciona | Limitación |
|---|---|---|
| Quórum | Solo actúa el grupo con mayoría de nodos | Exige 3 o más nodos impares |
| Fencing (STONITH) | El superviviente apaga al otro por hardware | Requiere control remoto de energía o del hipervisor |
| Testigo | Un tercer recurso arbitra (un disco, una IP) | Menos robusto que el quórum real |
Y la conclusión que hay que retener:
Con dos nodos no hay quórum posible. Ninguno puede tener mayoría de dos. Por eso un clúster serio tiene tres nodos, o dos más un testigo.
Esto tiene una consecuencia directa en el diseño de Tramontana: un par de balanceadores con VRRP es aceptable, porque el peor caso —dos nodos anunciando la misma IP unos segundos— es molesto pero recuperable. Un par de bases de datos con conmutación automática no lo es: el peor caso es la divergencia de datos, y no se recupera. De ahí la recomendación del apartado siguiente.
Replicación de datos
PostgreSQL replica mediante el registro de escritura anticipada (WAL): el primario envía sus cambios a una o más réplicas.
| Modo | El primario confirma cuando… | Riesgo de pérdida | Coste |
|---|---|---|---|
| Asíncrona | Ha escrito localmente | Las últimas transacciones | Ninguno |
| Síncrona | La réplica ha confirmado | Ninguno | Latencia de cada escritura |
Y el compromiso, que hay que entender antes de elegir: con replicación síncrona, cada escritura espera al viaje de ida y vuelta a la réplica. En la misma red local, uno o dos milisegundos. Entre centros de datos, decenas. Y si la réplica cae, el primario se bloquea esperando confirmación — a menos que se configure synchronous_standby_names con la sintaxis adecuada, lo que abre la puerta a perder datos justo cuando más importa.
Para Tramontana, con un RPO de 4 horas acordado, la replicación asíncrona es más que suficiente: perder los últimos segundos de transacciones está muy dentro de lo tolerado.
# postgresql.conf en el PRIMARIO
wal_level = replica
max_wal_senders = 3
wal_keep_size = 1GB
archive_mode = on
archive_command = 'test ! -f /srv/wal/%f && cp %p /srv/wal/%f'
hot_standby = on# Crear la replica desde cero
$ sudo -u postgres pg_basebackup -h 10.0.2.15 -U replicador \
-D /var/lib/postgresql/16/main -R -P -X stream -c fast
$ sudo -u postgres psql -c "SELECT client_addr, state, sync_state,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS retraso_bytes FROM pg_stat_replication;"
client_addr | state | sync_state | retraso_bytes
-------------+-----------+------------+---------------
10.0.2.22 | streaming | async | 0Ese retraso_bytes es la métrica que hay que vigilar: es cuántos datos se perderían si el primario cayera ahora. Va directa a la monitorización, junto a la edad de la última copia de 05-08.
Conmutación: manual frente a automática
| Manual | Automática | |
|---|---|---|
| Tiempo de recuperación | Minutos | Segundos |
| Riesgo de split-brain | Ninguno | Alto sin quórum |
| Requiere personal disponible | Sí | No |
| Herramientas | pg_ctl promote |
Patroni, repmgr, pg_auto_failover |
La recomendación profesional, y va en contra de la intuición:
Con dos nodos, la conmutación de base de datos debe ser manual. Sin quórum, un fallo de red entre primario y réplica puede promover la réplica mientras el primario sigue aceptando escrituras, y entonces hay dos bases de datos divergentes. Recuperar de ahí puede significar perder transacciones o reconciliar a mano.
Herramientas como Patroni hacen conmutación automática segura, pero requieren un almacén de configuración distribuido con quórum propio (etcd o Consul), que a su vez necesita tres nodos. Es decir: automatizar la conmutación de forma segura exige, como mínimo, cinco máquinas. Es una decisión de arquitectura, no una casilla que marcar.
Almacenamiento compartido
Para /opt/tramontana/shared/uploads:
| Opción | Ventaja | Inconveniente |
|---|---|---|
| NFS | Simple, funciona sin tocar la aplicación | Es un nuevo punto único de fallo |
| GlusterFS / Ceph | Distribuido y replicado | Complejidad alta |
| Almacén de objetos (S3, MinIO) | Escalable, gestionado, replicado | Requiere cambiar la aplicación |
rsync periódico |
Trivial | No es tiempo real: se pierden ficheros |
NFS tiene la ironía característica: se monta para dar alta disponibilidad y, si no se hace también redundante, empeora la fiabilidad, porque ahora dos servidores dependen de una tercera máquina.
La respuesta moderna es el almacén de objetos: es lo que usan las aplicaciones diseñadas para escalar horizontalmente. Exige trabajo de desarrollo, y es la misma conclusión del apartado del estado.
Clústeres, Pacemaker y fencing
Para escenarios más complejos —bases de datos, sistemas de ficheros compartidos, recursos con dependencias— existe la pila Pacemaker + Corosync:
- Corosync: comunicación entre nodos, pertenencia al clúster y quórum.
- Pacemaker: gestión de recursos, con dependencias y restricciones.
$ sudo pcs status
Cluster name: tramontana
Cluster Summary:
* Stack: corosync
* Current DC: nodo1 (version 2.1.6) - partition with quorum
* 3 nodes configured
* 4 resource instances configured
Full List of Resources:
* vip_tramontana (ocf:heartbeat:IPaddr2): Started nodo1
* postgresql (ocf:heartbeat:pgsql): Master nodo1Un recurso es cualquier cosa que el clúster gestiona (una IP, un servicio, un sistema de ficheros), y las restricciones expresan reglas: «la IP debe estar donde está el primario de PostgreSQL», «este servicio debe arrancar después de aquel».
Y el concepto sin el cual nada de esto funciona de verdad:
Fencing (o STONITH, Shoot The Other Node In The Head): antes de tomar los recursos de un nodo que parece caído, el superviviente se asegura de que está apagado, cortándole la energía o destruyendo su máquina virtual.
Por qué es imprescindible: si el nodo A parece caído pero en realidad solo está congelado o incomunicado, y B toma sus recursos —monta el sistema de ficheros, promueve la base de datos, coge la IP—, cuando A se recupere ambos escribirán en los mismos datos. La corrupción resultante puede ser irrecuperable.
# Fencing sobre libvirt: destruye la maquina virtual del otro nodo
$ sudo pcs stonith create fence_nodo2 fence_virsh \
ip=10.0.2.10 login=fence identity_file=/etc/pacemaker/id_rsa \
pcmk_host_list=nodo2 action=offEn hardware físico se hace con IPMI, iLO o unidades de distribución de energía gestionables. Y la regla del área es contundente: un clúster sin fencing configurado no es un clúster fiable. Muchos proyectos lo desactivan «temporalmente» porque complica las pruebas, y esa es exactamente la configuración que provoca la pérdida de datos el día del fallo.
Diseño para Tramontana y análisis de coste
graph TD
I["Internet"] --> DNS["DNS: reservas.tramontana.example<br/>→ 10.0.2.30 (VIP)"]
DNS --> VIP{"IP virtual flotante<br/>10.0.2.30"}
VIP -.->|VRRP| LB1["lb1 · HAProxy<br/>MASTER prio 100"]
VIP -.->|VRRP| LB2["lb2 · HAProxy<br/>BACKUP prio 90"]
LB1 --> APP1["app1 · Tramontana<br/>10.0.2.21:8080"]
LB1 --> APP2["app2 · Tramontana<br/>10.0.2.22:8080"]
LB2 -.-> APP1
LB2 -.-> APP2
APP1 --> BD1["PostgreSQL primario<br/>10.0.2.15"]
APP2 --> BD1
BD1 -->|WAL asincrono| BD2["PostgreSQL replica<br/>10.0.2.16"]
APP1 --> OBJ["Almacen de objetos<br/>uploads"]
APP2 --> OBJ
Qué protege cada capa, y qué no:
| Fallo | ¿Cubierto? | Tiempo de recuperación |
|---|---|---|
Cae app1 o app2 |
Sí, automático | 3-9 s (fall 3 × inter 3s) |
Cae lb1 |
Sí, automático | 3-4 s (VRRP) |
| Cae el primario de PostgreSQL | No: promoción manual deliberada | 5-15 min |
| Falla la red entre lb1 y lb2 | Parcial: split-brain breve, recuperable | Segundos |
| Cae el anfitrión que aloja todo | No | RTO completo |
| Un fallo lógico (borrado, corrupción) | No: se replica al instante | Restaurar copia |
| Un despliegue defectuoso | No: se despliega en ambos | Rollback |
Las tres últimas filas son las que hay que subrayar ante la dirección. La redundancia no protege contra los errores lógicos: un DELETE equivocado se replica en milisegundos a la réplica. Para eso están las copias de 05-08, y esta arquitectura no las sustituye en absoluto.
El análisis de coste
Lo que la lección viene prometiendo, y lo que Marta lleva tres respuestas pidiendo:
Coste de la arquitectura completa (5 máquinas frente a 1):
| Concepto | Estimación anual |
|---|---|
| 4 máquinas adicionales | Coste del VPS actual × 4 |
| Trabajo de desarrollo (sesiones + almacén de objetos) | 3-4 semanas, una vez |
| Montaje y automatización con Ansible | 2 semanas, una vez |
| Mantenimiento adicional | ~4 h/mes de forma indefinida |
| Almacén de objetos | Bajo, por volumen |
| Complejidad: más modos de fallo | Difícil de cuantificar, real |
Coste de la indisponibilidad. La pregunta que hay que responder antes:
Reservas al mes: ~500 Importe medio: 313,70 € Ingreso medio por hora (24×30): 500 × 313,70 / 720 ≈ 218 €/h
Pero ese cálculo bruto es engañoso por tres motivos, y decirlo es parte del análisis:
- Las reservas no son uniformes. Se concentran de tarde y en fin de semana. Una caída de dos horas un martes de madrugada puede costar cero; la misma caída un viernes por la tarde, mucho más que la media.
- Una reserva perdida no siempre se pierde. Muchos clientes vuelven a intentarlo. El coste real está entre el 20 % y el 50 % de la cifra bruta.
- Hay coste reputacional, no lineal y difícil de cuantificar, especialmente si la caída coincide con una campaña.
Estimación razonable del coste anual de indisponibilidad, con distintos objetivos:
| Disponibilidad | Caída anual | Coste estimado (a 218 €/h × 35 %) |
|---|---|---|
| 99,5 % (situación actual estimada) | 43,8 h | ~3.340 € |
| 99,9 % | 8,8 h | ~670 € |
| 99,95 % | 4,4 h | ~335 € |
El ahorro de pasar de 99,5 % a 99,9 % es de unos 2.700 € al año. Eso hay que compararlo con el coste de la arquitectura completa —cuatro máquinas más, seis semanas de trabajo inicial y cuatro horas mensuales indefinidas—, que muy probablemente lo supera.
Y aquí está el análisis que casi nadie hace, y que cambia la recomendación:
| Medida | Coste | Disponibilidad estimada |
|---|---|---|
| Situación actual | — | ~99,5 % |
| Solo dos nodos de aplicación + un balanceador | 2 máquinas, 1 semana | ~99,8 % |
| Arquitectura completa con BD replicada | 4 máquinas, 6 semanas | ~99,9 % |
El primer paso captura la mayor parte del beneficio por una fracción del coste, y esto es lo habitual en disponibilidad: los primeros nueves son baratos y los siguientes se encarecen exponencialmente. La razón es que la mayoría de las interrupciones no vienen de un fallo de hardware, sino de despliegues, reinicios y mantenimiento — y todo eso se cubre con dos nodos de aplicación y drenaje de conexiones, sin tocar la base de datos.
Recomendación
Fase 1 (ahora): fijar un objetivo formal de disponibilidad con Marta. Sin objetivo no se puede decidir cuánto invertir. Y medirla, porque el 99,5 % actual es una estimación, no un dato.
Fase 2 (1-2 meses): dos nodos de aplicación y un balanceador. Sesiones externalizadas y ficheros en almacén de objetos, que es trabajo de desarrollo. Elimina las interrupciones por despliegue y por fallo de un nodo, que son la mayoría.
Fase 3 (evaluar después): segundo balanceador con VRRP, y réplica de PostgreSQL con promoción manual.
Fase 4 (probablemente nunca): conmutación automática de base de datos. Exige quórum, es decir, cinco máquinas y un almacén distribuido. Para una empresa de este tamaño, la complejidad supera al beneficio.
Probar los fallos provocándolos
Un sistema de alta disponibilidad no probado no es un sistema de alta disponibilidad: es una arquitectura que crees que funciona. Y la historia del área está llena de casos en que el respaldo falló el día que hizo falta.
# --- Prueba 1: cae un nodo de aplicacion ---
$ ssh [email protected] 'sudo systemctl stop tramontana'
# Esperado: HAProxy lo saca en ~9 s (fall 3 x inter 3s), sin errores visibles
$ for i in {1..60}; do
curl -s -o /dev/null -w '%{http_code} ' https://reservas.tramontana.example/casas
sleep 1
done; echo
# Criterio: cero respuestas != 200
# --- Prueba 2: cae el balanceador principal ---
$ ssh [email protected] 'sudo systemctl stop keepalived'
# Esperado: la VIP se mueve a lb2 en 3-4 s
$ ssh [email protected] 'ip -brief addr show enp0s3 | grep 10.0.2.30'
# --- Prueba 3: HAProxy muere pero keepalived vive ---
# Es la prueba que valida el track_script. Sin el, la VIP NO se mueve
# y el servicio cae con ambos balanceadores "vivos".
$ ssh [email protected] 'sudo systemctl stop haproxy'
$ sleep 6 && ssh [email protected] 'ip -brief addr show enp0s3 | grep 10.0.2.30'
# --- Prueba 4: particion de red entre balanceadores (split-brain) ---
$ ssh [email protected] 'sudo nft add rule inet filter input ip saddr 10.0.2.21 drop'
# Esperado: AMBOS toman la VIP. Documentar el comportamiento observado.
$ ssh [email protected] 'sudo nft flush chain inet filter input'
# --- Prueba 5: despliegue sin interrupcion ---
$ echo "set server tramontana_app/app1 state drain" | sudo socat stdio /run/haproxy/admin.sock
$ ssh [email protected] 'sudo ~/scripts/desplegar.sh 3.3.0'
$ echo "set server tramontana_app/app1 state ready" | sudo socat stdio /run/haproxy/admin.sock
# Criterio: cero errores durante todo el procesoLa prueba 4 es la más incómoda y la más importante: no se puede «aprobar» con dos nodos, porque sin quórum el split-brain es inevitable. Lo que se hace es documentar el comportamiento observado y el procedimiento de recuperación, para que el día que ocurra nadie improvise.
La ingeniería del caos lleva esto más lejos: inyectar fallos de forma continua y automatizada en producción, con la lógica de que si los fallos ocurren todos los días, el sistema está obligado a tolerarlos y el equipo obligado a saber responder. Para Tramontana es desproporcionado; el principio —provoca los fallos tú, en horario laboral, en lugar de esperarlos— es aplicable a cualquier escala, y encaja con el simulacro semestral que propusiste en 07-06.
Errores Comunes y Consejos
- Poner un balanceador y creer que ya hay alta disponibilidad. Solo has movido el punto único de fallo. El balanceador necesita su propia redundancia.
- Configurar VRRP sin
track_script. Un nodo conkeepalivedvivo y HAProxy muerto retiene la IP virtual, y el servicio queda caído con ambos balanceadores «funcionando». - Conmutación automática de base de datos con dos nodos. Sin quórum, una partición de red produce dos primarios y datos divergentes. Manual, hasta que haya tres nodos.
- Desactivar el fencing «temporalmente». Es la configuración que provoca la corrupción de datos el día del fallo. Un clúster sin fencing no es fiable.
- Comprobaciones de salud superficiales. Un
/saludque solo dice «estoy vivo» hace que el balanceador envíe tráfico a un nodo que no puede atender nada. - Comprobaciones de salud demasiado profundas. Un fallo de la base de datos marca todos los nodos como caídos, cuando podrían haber servido algo.
- Sacar un nodo al primer fallo. Un pico transitorio saca el nodo, la carga se concentra en los demás, y caen en cascada.
fall 3yslowstart. - Olvidar que los timers se ejecutan en todos los nodos. Dos copias de seguridad simultáneas, dos purgas. El
flockes local y no protege. - Sesiones persistentes como solución permanente. Enmascara el problema real, desequilibra la carga, y cuando un nodo cae pierde todas sus sesiones.
- Creer que la replicación sustituye a las copias. Un borrado accidental se replica en milisegundos. La redundancia protege del fallo de hardware, no del error humano.
- NFS sin redundancia para dar alta disponibilidad. Añades un punto único de fallo del que ahora dependen dos servidores.
- No probar los fallos. Un sistema de alta disponibilidad no probado es una arquitectura que crees que funciona.
- Consejo de método. Antes de diseñar nada, fija el objetivo de disponibilidad y mide el actual. Sin esos dos números, cualquier inversión es una intuición disfrazada de arquitectura.
Ejercicios
Ejercicio 1
Diseña el extremo /salud que debe exponer la aplicación para que HAProxy tome buenas decisiones. Especifica qué comprueba, qué no, qué códigos devuelve y con qué latencia, y explica cómo se relaciona con revision_salud.sh.
Ejercicio 2
Escribe el rol de Ansible que despliega la configuración de HAProxy y keepalived en los dos balanceadores, teniendo en cuenta que la configuración difiere entre ellos y que aplicarla mal deja el servicio sin IP.
Ejercicio 3
Marta pregunta directamente: «¿Necesitamos alta disponibilidad?». Redacta la respuesta con el análisis completo y una recomendación clara.
Soluciones
Solución 1
Diseño del extremo /salud:
| Comprueba | Cómo | Por qué |
|---|---|---|
| El proceso responde | Que la petición llegue | Trivial pero necesario |
| Conexión a PostgreSQL | SELECT 1 con timeout de 1 s |
Sin base de datos no puede atender nada |
| Grupo de conexiones | Que haya al menos una libre | Con el grupo agotado, las peticiones encolan |
| Almacén de ficheros | Escritura de un byte cada 30 s (en caché) | Sin él fallan las subidas, pero no las consultas |
Espacio en /var/log |
Umbral del 95 % (en caché) | Sin sitio para logs, se pierde la trazabilidad |
| NO comprueba | Por qué |
|---|---|
| Consultas de negocio reales | Caras; un problema de datos tumbaría todos los nodos |
| Servicios externos (correo, pagos) | Un fallo ajeno sacaría todo el servicio del reparto |
| Estado de otros nodos | Cada nodo informa solo de sí mismo |
| Métricas de rendimiento | Eso es monitorización, no salud |
Los tres estados y su respuesta:
// 200 OK — estado 0, correcto
{
"estado": "ok",
"version": "3.2.1",
"comprobaciones": {
"bd": {"ok": true, "latencia_ms": 2},
"pool": {"ok": true, "libres": 68, "total": 80},
"almacen": {"ok": true},
"disco_logs": {"ok": true, "uso_pct": 34}
}
}// 200 OK con cabecera X-Salud-Aviso: degradado — estado 1
// SIGUE recibiendo trafico: puede atender consultas
{
"estado": "degradado",
"avisos": ["almacen_no_disponible"],
"comprobaciones": {
"bd": {"ok": true, "latencia_ms": 3},
"pool": {"ok": true, "libres": 12, "total": 80},
"almacen": {"ok": false, "error": "timeout"},
"disco_logs": {"ok": true, "uso_pct": 34}
}
}// 503 Service Unavailable — estado 2, critico
// NO recibe trafico
{
"estado": "critico",
"errores": ["bd_inaccesible"],
"comprobaciones": {
"bd": {"ok": false, "error": "connection refused"}
}
}La relación con revision_salud.sh es la parte conceptual del ejercicio. Ambos responden a la misma pregunta desde lados distintos, y esa complementariedad hay que aprovecharla en lugar de duplicar trabajo:
revision_salud.sh |
/salud |
|
|---|---|---|
| Quién pregunta | El administrador o un timer | HAProxy, cada 3 s |
| Desde dónde | Fuera del proceso, en la máquina | Desde dentro del proceso |
| Ve el estado interno | No | Sí: grupo de conexiones, cachés |
| Frecuencia | Minutos | Segundos |
| Coste aceptable | Alto | Muy bajo |
| Códigos | 0 / 1 / 2 | 200 / 200+cabecera / 503 |
Lo correcto es que revision_salud.sh consuma /salud en lugar de reimplementar las comprobaciones:
# Fragmento de revision_salud.sh, adaptado
comprobar_aplicacion() {
local respuesta codigo estado
respuesta="$(curl -s -m 5 -w '\n%{http_code}' \
"http://127.0.0.1:${TRAMONTANA_PUERTO}/salud")" || {
error "la aplicacion no responde"
return 2
}
codigo="$(tail -1 <<<"$respuesta")"
estado="$(sed '$d' <<<"$respuesta" | jq -r '.estado')"
case "$estado" in
ok) log "aplicacion correcta"; return 0 ;;
degradado) error "degradada: $(sed '$d' <<<"$respuesta" | jq -r '.avisos|join(", ")')"
return 1 ;;
critico) error "critica: $(sed '$d' <<<"$respuesta" | jq -r '.errores|join(", ")')"
return 2 ;;
*) error "estado desconocido (HTTP $codigo)"; return 2 ;;
esac
}Cuatro decisiones de diseño que hay que justificar:
- El estado degradado devuelve 200, no 503. Es contraintuitivo y es correcto: si el almacén de ficheros falla, el nodo puede seguir atendiendo consultas de disponibilidad, que son la mayoría del tráfico. Sacarlo del reparto reduciría la capacidad sin ganar nada. La cabecera permite que la monitorización lo detecte sin que el balanceador actúe.
- Latencia acotada por diseño. Con 3 segundos de intervalo y dos nodos, son 40 peticiones por minuto. El objetivo es menos de 50 ms, y de ahí que las comprobaciones caras estén cacheadas 30 segundos.
- Sin autenticación, pero restringido por red. Debe ser accesible sin credenciales para que HAProxy lo consulte, y no debe estar expuesto a Internet: el detalle del JSON es información útil para un atacante. Se bloquea en el frontend:
http-request deny if { path_beg /salud } !{ src 10.0.2.0/24 } - Sin efectos secundarios y sin bloqueo. Un
/saludque escribe en la base de datos o toma un bloqueo puede provocar el fallo que pretende detectar. La comprobación de escritura del almacén va cacheada y en segundo plano.
Y la advertencia final, que es el error más común: /salud no debe comprobar dependencias que no son propias de este nodo. Si comprobara el estado de la base de datos con una consulta pesada, un problema de PostgreSQL sacaría todos los nodos a la vez y el servicio desaparecería por completo, cuando podría haber devuelto al menos una página de error decente.
Solución 2
# roles/balanceador/defaults/main.yml
haproxy_vip: 10.0.2.30
haproxy_vip_cidr: 24
haproxy_interfaz: enp0s3
haproxy_vrrp_id: 51
haproxy_backends:
- { nombre: app1, direccion: 10.0.2.21, puerto: 8080 }
- { nombre: app2, direccion: 10.0.2.22, puerto: 8080 }
haproxy_check_inter: 3s
haproxy_check_rise: 2
haproxy_check_fall: 3
haproxy_slowstart: 30s# inventario/produccion.yml (fragmento)
balanceadores:
hosts:
lb1:
ansible_host: 10.0.2.20
vrrp_estado: MASTER
vrrp_prioridad: 100
lb2:
ansible_host: 10.0.2.21
vrrp_estado: BACKUP
vrrp_prioridad: 90# roles/balanceador/tasks/main.yml
---
# =====================================================================
# PRECAUCION: una configuracion incorrecta de keepalived puede dejar el
# servicio SIN IP VIRTUAL, es decir, completamente caido. Este rol
# despliega los nodos DE UNO EN UNO y verifica entre ellos.
# =====================================================================
- name: "Verificar que no se ejecuta en paralelo sobre ambos balanceadores"
ansible.builtin.assert:
that: ansible_play_hosts | length == 1
fail_msg: >-
Este rol debe ejecutarse con --limit sobre UN balanceador cada vez,
o con serial: 1 en el play. Configurar ambos a la vez puede dejar
el servicio sin IP virtual.
- name: Instalar HAProxy y keepalived
ansible.builtin.apt:
name: [haproxy, keepalived, socat]
state: present
tags: [paquetes]
# ---------- HAProxy ----------
- name: Crear el directorio de certificados
ansible.builtin.file:
path: /etc/haproxy/certs
state: directory
owner: root
group: haproxy
mode: '0750'
tags: [tls]
- name: Instalar el certificado combinado para HAProxy
ansible.builtin.copy:
content: "{{ vault_certificado_pem }}"
dest: /etc/haproxy/certs/reservas.tramontana.example.pem
owner: root
group: haproxy
mode: '0640'
no_log: true # contiene la clave privada
notify: Recargar haproxy
tags: [tls]
- name: Desplegar la configuracion de HAProxy
ansible.builtin.template:
src: haproxy.cfg.j2
dest: /etc/haproxy/haproxy.cfg
owner: root
group: root
mode: '0644'
backup: true
# VALIDA antes de instalar: una config rota impide arrancar
validate: 'haproxy -c -f %s'
notify: Recargar haproxy
tags: [haproxy]
- name: Activar HAProxy
ansible.builtin.systemd_service:
name: haproxy
enabled: true
state: started
tags: [haproxy]
# ---------- keepalived ----------
# El sysctl que permite a HAProxy escuchar en una IP que este nodo
# todavia no tiene (la VIP cuando esta en el otro). Sin esto, HAProxy
# no arranca en el nodo BACKUP.
- name: Permitir bind sobre IP no locales
ansible.posix.sysctl:
name: net.ipv4.ip_nonlocal_bind
value: '1'
sysctl_file: /etc/sysctl.d/71-balanceador.conf
sysctl_set: true
reload: true
tags: [keepalived]
- name: Crear el usuario para los scripts de keepalived
ansible.builtin.user:
name: keepalived_script
system: true
shell: /usr/sbin/nologin
create_home: false
tags: [keepalived]
- name: Instalar el script de notificacion de VRRP
ansible.builtin.template:
src: vrrp-notificar.j2
dest: /usr/local/sbin/vrrp-notificar
owner: root
group: root
mode: '0755' # NO escribible por el usuario del script
tags: [keepalived]
- name: Desplegar la configuracion de keepalived
ansible.builtin.template:
src: keepalived.conf.j2
dest: /etc/keepalived/keepalived.conf
owner: root
group: root
mode: '0600' # contiene la contrasena de VRRP
backup: true
no_log: true
notify: Reiniciar keepalived
tags: [keepalived]
- name: Activar keepalived
ansible.builtin.systemd_service:
name: keepalived
enabled: true
state: started
tags: [keepalived]
# ---------- Verificacion ----------
- name: Forzar los handlers antes de verificar
ansible.builtin.meta: flush_handlers
- name: "Verificar | HAProxy responde en el socket de administracion"
ansible.builtin.shell:
cmd: >-
set -o pipefail;
echo "show stat" | socat stdio /run/haproxy/admin.sock
| awk -F, '$1=="tramontana_app" && $2=="BACKEND" {print $18}'
register: estado_backend
changed_when: false
failed_when: "'UP' not in estado_backend.stdout"
tags: [verificacion]
- name: "Verificar | Los backends estan sanos"
ansible.builtin.shell:
cmd: >-
set -o pipefail;
echo "show stat" | socat stdio /run/haproxy/admin.sock
| awk -F, '$1=="tramontana_app" && $2 ~ /^app/ && $18=="UP"' | wc -l
register: backends_up
changed_when: false
failed_when: backends_up.stdout | int < 1
tags: [verificacion]
- name: "Verificar | La VIP esta en el MASTER y solo en el"
ansible.builtin.shell:
cmd: "ip -brief addr show {{ haproxy_interfaz }} | grep -c {{ haproxy_vip }} || true"
register: tiene_vip
changed_when: false
tags: [verificacion]
- name: "Verificar | Coherencia del estado de la VIP"
ansible.builtin.assert:
that:
- (vrrp_estado == 'MASTER' and tiene_vip.stdout | int == 1) or
(vrrp_estado == 'BACKUP' and tiene_vip.stdout | int == 0)
fail_msg: >-
Estado de VIP incoherente en {{ inventory_hostname }}
({{ vrrp_estado }}, VIP presente: {{ tiene_vip.stdout }}).
Revisar antes de continuar con el otro balanceador.
tags: [verificacion]
- name: "Verificar | El servicio responde a traves de la VIP"
ansible.builtin.uri:
url: "https://reservas.tramontana.example/salud"
validate_certs: true
status_code: 200
timeout: 10
delegate_to: localhost
become: false
run_once: true
tags: [verificacion]# roles/balanceador/handlers/main.yml
- name: Recargar haproxy
ansible.builtin.systemd_service:
name: haproxy
state: reloaded # reload, no restart: no corta las conexiones
- name: Reiniciar keepalived
ansible.builtin.systemd_service:
name: keepalived
state: restarted{# roles/balanceador/templates/keepalived.conf.j2 #}
# GENERADO POR ANSIBLE — NO EDITAR A MANO
global_defs {
router_id {{ inventory_hostname }}
enable_script_security
script_user keepalived_script
}
vrrp_script comprobar_haproxy {
script "/usr/bin/killall -0 haproxy"
interval 2
weight -40
fall 2
rise 2
}
vrrp_instance TRAMONTANA_VIP {
state {{ vrrp_estado }}
interface {{ haproxy_interfaz }}
virtual_router_id {{ haproxy_vrrp_id }}
priority {{ vrrp_prioridad }}
advert_int 1
authentication {
auth_type PASS
auth_pass {{ vault_vrrp_pass }}
}
virtual_ipaddress {
{{ haproxy_vip }}/{{ haproxy_vip_cidr }} dev {{ haproxy_interfaz }}
}
track_script {
comprobar_haproxy
}
notify_master "/usr/local/sbin/vrrp-notificar master"
notify_backup "/usr/local/sbin/vrrp-notificar backup"
notify_fault "/usr/local/sbin/vrrp-notificar fault"
}Y el play que garantiza el despliegue de uno en uno:
# balanceadores.yml
- name: Configurar los balanceadores
hosts: balanceadores
become: true
# serial: 1 -> un nodo cada vez. Si el primero falla, el segundo NO se toca.
serial: 1
# max_fail_percentage: 0 -> aborta ante el primer fallo
max_fail_percentage: 0
# ORDEN DELIBERADO: primero el BACKUP. Si algo sale mal, el MASTER
# sigue con la VIP y el servicio no se interrumpe.
order: reverse_inventory
roles:
- balanceadorLas decisiones que evitan dejar el servicio sin IP:
| Decisión | Contra qué protege |
|---|---|
serial: 1 + assert de un solo host |
Configurar ambos a la vez y quedarse sin ningún nodo con VIP |
order: reverse_inventory (BACKUP primero) |
Si el rol falla, el MASTER conserva la VIP y el servicio sigue |
max_fail_percentage: 0 |
Que un fallo en lb2 no impida detectarse antes de tocar lb1 |
validate: haproxy -c -f %s |
Instalar una configuración que impide arrancar HAProxy |
meta: flush_handlers antes de verificar |
Verificar el estado anterior a los cambios |
state: reloaded en HAProxy |
restart cortaría todas las conexiones en curso |
assert de coherencia de la VIP |
Detectar que ambos o ninguno tienen la VIP |
uri contra la VIP con run_once |
Comprobar el servicio de extremo a extremo, no solo las piezas |
net.ipv4.ip_nonlocal_bind |
HAProxy no arranca en el BACKUP si no puede escuchar en la VIP ausente |
no_log: true en certificado y VRRP |
La clave privada y la contraseña saldrían en la salida |
Script 0755 propiedad de root |
enable_script_security rechaza scripts escribibles por el usuario que los ejecuta |
Y lo que hay que añadir al runbook: este rol se prueba primero en el entorno de pruebas con dos balanceadores virtuales, y en producción se ejecuta con --check --diff antes que en serio. La virtual_router_id debe ser única en el segmento de red: dos clústeres VRRP con el mismo ID en la misma red interfieren entre sí, y es un fallo desconcertante de diagnosticar.
Solución 3
¿Necesita Tramontana Reservas alta disponibilidad? Para: Marta Vidal · De: Operaciones de sistemas · 18 de agosto de 2026
Respuesta breve: alta disponibilidad completa, no. Un primer paso, sí, y con una relación coste-beneficio muy favorable. Recomiendo dos servidores de aplicación con un balanceador, que captura la mayor parte del beneficio por una fracción del coste.
Primero, un dato que nos falta. No tenemos un objetivo de disponibilidad acordado ni la medimos. Estimo que estamos alrededor del 99,5 %, unas 44 horas de caída al año, contando reinicios por actualización, despliegues y algún incidente. Es una estimación, no un dato, y lo primero que propongo es empezar a medirlo — cuesta muy poco y sin ese número cualquier inversión es una intuición.
Lo que ya hemos conseguido sin gastar nada. La disponibilidad depende de dos cosas: con qué frecuencia falla el sistema y cuánto tarda en recuperarse. En las últimas semanas hemos reducido drásticamente lo segundo: reconstruir el servidor por completo ha pasado de ocho horas a cincuenta minutos, automatizando su configuración. Eso ya ha mejorado la disponibilidad sin comprar una sola máquina, y es la clase de mejora que hay que agotar antes de duplicar infraestructura.
Qué costaría estar caídos. Con unas 500 reservas mensuales de 313,70 € de media, el ingreso medio es de unos 218 € por hora. Ese número hay que matizarlo en tres sentidos:
- Las reservas se concentran por la tarde y en fin de semana. Una caída de madrugada un martes puede no costar nada; la misma un viernes por la tarde cuesta bastante más que la media.
- Muchos clientes que encuentran la web caída vuelven a intentarlo. El coste real está probablemente entre el 20 % y el 50 % de la cifra bruta.
- Hay un coste de imagen, no lineal y difícil de cuantificar, sobre todo si coincide con una campaña.
Con una estimación prudente (35 %):
Disponibilidad Caída al año Coste estimado 99,5 % (situación actual) 44 h ~3.340 € 99,8 % 17,5 h ~1.330 € 99,9 % 8,8 h ~670 € Las tres opciones, con su coste real.
Qué incluye Inversión Disponibilidad Ahorro anual A. No hacer nada Lo actual 0 ~99,5 % — B. Dos servidores + balanceador 2 máquinas, 1 semana de trabajo Baja ~99,8 % ~2.000 € C. Arquitectura completa 4 máquinas, 6 semanas, +4 h/mes Alta ~99,9 % ~2.670 € Lo importante está en la comparación entre B y C: la opción C cuesta varias veces más que la B y aporta apenas 670 € adicionales de ahorro anual. La razón es que los primeros nueves son baratos y los siguientes se encarecen mucho — y, sobre todo, que la mayoría de nuestras interrupciones no vienen de fallos de hardware, sino de despliegues, reinicios y mantenimiento, y eso se resuelve entero con la opción B.
Qué nos daría exactamente la opción B:
- Despliegues sin interrupción. Hoy cada despliegue son unos segundos de servicio cortado. Con dos servidores se actualiza uno mientras el otro atiende, y el cliente no nota nada.
- Mantenimiento sin ventana. Reiniciar por un kernel nuevo deja de ser una interrupción programada.
- Tolerancia al fallo de un servidor de aplicación. El sistema lo detecta en menos de diez segundos y deja de enviarle tráfico solo.
- Capacidad para picos, como beneficio secundario.
Qué NO nos daría, y hay que decirlo con claridad:
- Si cae la base de datos, el servicio cae. La promoción de la réplica sería manual, de 5 a 15 minutos.
- Si cae el balanceador, el servicio cae. Es la opción C.
- Si cae la infraestructura del proveedor, todo cae.
- Y nada de esto protege contra un borrado accidental o una corrupción de datos: eso se replicaría al instante a todos los nodos. Para eso están las copias, que seguirían siendo igual de necesarias.
Hay una condición previa que no es técnica de sistemas. Tramontana Reservas guarda hoy dos cosas dentro de cada servidor: las sesiones de las personas que navegan, y las fotos que se suben. Con dos servidores, quien iniciara sesión en uno aparecería desconectado al ser atendido por el otro, y una foto subida a uno no se vería desde el otro. Antes de montar nada hace falta trabajo de desarrollo —externalizar las sesiones y guardar los ficheros en un almacén compartido—, unas tres o cuatro semanas. Es la parte del proyecto que hay que planificar con Luis, y sin ella la opción B no funciona.
Y una advertencia profesional. La redundancia añade piezas, y cada pieza puede fallar de formas nuevas. Un sistema mal montado es menos fiable que uno simple bien mantenido. Por eso no recomiendo la opción C: la complejidad que añade —replicación, promoción, riesgo de que dos servidores escriban a la vez datos incompatibles— exige un nivel de vigilancia y de pruebas que hoy no podemos sostener con la plantilla actual. Es mejor una arquitectura sencilla que entendemos y probamos que una sofisticada que nadie ha ensayado.
Recomendación, en tres pasos.
- Este mes, coste cero: fijar un objetivo formal de disponibilidad —propongo 99,8 %— y empezar a medirla. Con eso podremos decidir con datos en lugar de con estimaciones.
- En dos o tres meses: desarrollo de las dos condiciones previas, y despliegue de la opción B. Empezaría con un solo balanceador, asumiendo que es un punto único de fallo pero mucho menos propenso a caer que la aplicación completa.
- Revisar en un año, o antes si cambia algo: si el volumen de reservas crece de forma significativa, si una caída nos cuesta un cliente importante, o si medimos una disponibilidad peor de lo estimado.
Y una última cosa. Aparte de todo lo anterior, quiero programar un simulacro semestral: provocar los fallos a propósito, en horario laboral, y comprobar que la recuperación funciona. Un sistema de respaldo que nunca se ha probado es un sistema del que no sabemos nada, y esto vale tanto para la arquitectura futura como para el procedimiento de reconstrucción que ya tenemos. El primero, en septiembre.
Conclusión
Sabes de qué se habla cuando se habla de alta disponibilidad, y con la precisión que el tema exige. Los nueves tienen números concretos —99,99 % es menos de un minuto de caída a la semana—, la disponibilidad se descompone en MTBF y MTTR, y reducir el MTTR suele ser mucho más barato que aumentar el MTBF: es exactamente lo que hiciste en 07-06 al bajar la reconstrucción de ocho horas a cincuenta minutos, sin comprar nada. Sabes que el escalado vertical no aporta disponibilidad, que un nodo pasivo que nunca se usa es un nodo del que no sabes nada, y sobre todo que el estado es el problema difícil: las sesiones, los ficheros subidos y las tareas programadas son lo que impide duplicar un servidor sin más.
Has montado un balanceador con HAProxy que reparte por leastconn, sondea la salud cada tres segundos con fall 3 y rise 2 asimétricos, devuelve los nodos con slowstart para no tumbarlos de nuevo, y permite drenar conexiones para desplegar sin cortar — resolviendo lo que quedó pendiente desde 04-07. Le has dado redundancia con keepalived y una IP virtual flotante, con el track_script que evita el fallo más tonto de esta arquitectura: un balanceador con keepalived vivo y HAProxy muerto reteniendo la IP. Y conoces los dos conceptos sin los cuales todo esto es teatro: el split-brain, que con dos nodos no se puede evitar porque no hay quórum posible, y el fencing, sin el cual un clúster no es fiable por mucho que arranque.
Y has hecho el análisis que Marta llevaba tres lecciones pidiendo, con la conclusión que no siempre gusta: la arquitectura completa no compensa. Dos servidores de aplicación con un balanceador capturan la mayor parte del beneficio por una fracción del coste, porque la mayoría de las interrupciones no vienen de fallos de hardware sino de despliegues y mantenimiento. Que la respuesta profesional a «¿necesitamos alta disponibilidad?» pueda ser «una parte, y por este orden» es tan importante como saber montarla. Junto con la advertencia que cierra el asunto: la redundancia mal hecha empeora la fiabilidad, y un sistema sencillo que se entiende y se prueba vale más que uno sofisticado que nadie ha ensayado.
Con esto se cierra el Módulo 7, y con él la parte más avanzada del curso. Has aprendido a mirar dentro del sistema: la cadena de arranque completa y cómo intervenir cuando se rompe, el diagnóstico con strace, perf y eBPF cuando los contadores no bastan, y el ajuste del kernel con la regla de no tocar lo que no se ha medido. Y has aprendido a multiplicarlo: virtualización con KVM y el entorno de pruebas que el curso echaba en falta desde el Módulo 4, contenedores entendidos como lo que son —procesos aislados, no máquinas pequeñas—, automatización con Ansible que convirtió la configuración en código y el RTO en cincuenta minutos, y por fin la redundancia que quita a srv-tramontana su condición de punto único de fallo.
En el Módulo 8: Proyectos Prácticos todo eso se aplica a construcciones completas, de principio a fin. Montarás un servidor web con Nginx como proxy inverso y TLS delante de la aplicación — y ahí se cierra por fin el cifrado en tránsito que quedó preparado en 06-05 y lleva desde entonces esperando. Configurarás un servidor de base de datos PostgreSQL en condiciones, con su ajuste, sus copias y su replicación. Construirás un servidor de medios doméstico, que es el proyecto donde comprobar que todo lo aprendido se aplica igual fuera del trabajo. Levantarás un servidor VPN con WireGuard para acceder a la red interna sin exponer nada. Desplegarás un clúster de Kubernetes, que es donde los contenedores de 07-05 y la orquestación se juntan. Y cerrarás con la puesta en producción: la checklist completa, la monitorización con Prometheus y Grafana que se mencionó en 05-07, y el procedimiento de operación diaria de un sistema que ya no es un laboratorio. Actualiza el snapshot de tu VM y nos vemos allí.
Curso de Linux: De Principiante a Administrador de Sistemas
Módulo 1: Introducción a Linux
- ¿Qué es Linux?
- Historia de Linux
- Distribuciones de Linux
- Instalando Linux
- Primer Contacto con el Sistema
- Estructura del Sistema de Archivos de Linux
Módulo 2: Comandos Básicos de Linux
- Introducción a la Línea de Comandos
- Obtener Ayuda y Documentación del Sistema
- Navegando el Sistema de Archivos
- Operaciones con Archivos y Directorios
- Visualización y Edición de Archivos
- Enlaces Duros y Simbólicos
- Permisos y Propiedad de Archivos
Módulo 3: Habilidades Avanzadas en la Línea de Comandos
- El Entorno del Shell: Variables, Alias e Historial
- Uso de Comodines y Expresiones Regulares
- Búsqueda de Archivos y Contenido: find, locate y grep
- Tuberías y Redirección
- Procesamiento de Texto: cut, sort, uniq, sed y awk
- Gestión de Procesos
- Programación de Tareas con Cron
- Comandos de Redes
Módulo 4: Scripting en Shell
- Introducción al Scripting en Shell
- Variables y Tipos de Datos
- Entrada, Salida y Argumentos de un Script
- Estructuras de Control
- Funciones y Librerías
- Depuración y Manejo de Errores
- Scripts de Producción: Buenas Prácticas
Módulo 5: Administración del Sistema
- Gestión de Usuarios y Grupos
- sudo y Permisos Especiales
- Gestión de Paquetes
- Gestión de Discos
- systemd y la Gestión de Servicios
- Registros del Sistema: journald y syslog
- Monitoreo del Sistema y Optimización del Rendimiento
- Respaldo y Restauración
Módulo 6: Redes y Seguridad
- Configuración de Redes
- SSH y Acceso Remoto
- Firewall y Seguridad Perimetral
- Sistemas de Detección de Intrusos
- Gestión de Secretos y Certificados TLS
- Asegurando Sistemas Linux
Módulo 7: Temas Avanzados
- El Proceso de Arranque y la Recuperación del Sistema
- Diagnóstico Avanzado: strace, perf y eBPF
- Optimización del Kernel de Linux
- Virtualización con Linux
- Contenedores de Linux y Docker
- Automatización con Ansible
- Alta Disponibilidad y Balanceo de Carga
