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

  1. Vocabulario: disponibilidad, nueves, MTBF y MTTR
  2. Escalado vertical y horizontal
  3. Redundancia: activo-pasivo y activo-activo
  4. El estado es el problema difícil
  5. Balanceo de carga: capas, algoritmos y comprobaciones de salud
  6. HAProxy en la práctica
  7. Alta disponibilidad del balanceador: keepalived y VRRP
  8. Split-brain y quórum
  9. Replicación de datos
  10. Clústeres, Pacemaker y fencing
  11. Diseño para Tramontana y análisis de coste
  12. 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:
Disponibilidad = MTBF / (MTBF + MTTR)

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.sock

Repetido 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

$ sudo apt install haproxy socat
$ haproxy -v
HAProxy version 2.8.5-1ubuntu3 2024/01/31
# /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,21

Cuatro 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.

$ sudo apt install keepalived
# /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/24

El 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      |             0

Ese 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 nodo1

Un 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=off

En 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:

  1. 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.
  2. 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.
  3. 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 proceso

La 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 con keepalived vivo 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 /salud que 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 3 y slowstart.
  • Olvidar que los timers se ejecutan en todos los nodos. Dos copias de seguridad simultáneas, dos purgas. El flock es 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:

GET /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:

  1. 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.
  2. 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.
  3. 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 }
    
  4. Sin efectos secundarios y sin bloqueo. Un /salud que 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:
    - balanceador

Las 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.

  1. 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.
  2. 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.
  3. 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

Módulo 2: Comandos Básicos de Linux

Módulo 3: Habilidades Avanzadas en la Línea de Comandos

Módulo 4: Scripting en Shell

Módulo 5: Administración del Sistema

Módulo 6: Redes y Seguridad

Módulo 7: Temas Avanzados

Módulo 8: Proyectos Prácticos

© Copyright 2026. Todos los derechos reservados