Pots reconstruir srv-tramontana en cinquanta minuts. Pots provar qualsevol canvi abans d'aplicar-lo, detectar una intrusió, xifrar els secrets, diagnosticar una latència anòmala i recuperar una arrencada trencada. I tot i així, si aquella màquina cau, Tramontana Reserves està caiguda.

Un disc que falla, una font d'alimentació, un tall de xarxa del proveïdor, un nucli que no arrenca després d'una actualització, o simplement el manteniment programat de la infraestructura on viu. En el millor dels casos, cinquanta minuts d'interrupció — i això suposant que algú estigui despert per llançar el playbook. Tota la feina de set mòduls descansa sobre una única màquina, que és la definició exacta d'un punt únic de fallada.

Aquesta lliçó ataca aquell problema, i ho fa amb un advertiment per endavant: l'alta disponibilitat és cara, complexa i, mal feta, empitjora la fiabilitat en lloc de millorar-la. Un sistema amb més peces té més maneres de fallar. La lliçó acaba amb l'anàlisi honesta de si Tramontana necessita això, perquè la resposta professional no sempre és que sí.

Contingut

  1. Vocabulari: disponibilitat, nous, MTBF i MTTR
  2. Escalat vertical i horitzontal
  3. Redundància: actiu-passiu i actiu-actiu
  4. L'estat és el problema difícil
  5. Balanceig de càrrega: capes, algorismes i comprovacions de salut
  6. HAProxy a la pràctica
  7. Alta disponibilitat del balancejador: keepalived i VRRP
  8. Cervell dividit i quòrum
  9. Replicació de dades
  10. Clústers, Pacemaker i delimitació
  11. Disseny per a Tramontana i anàlisi de cost
  12. Provar les fallades provocant-les

Vocabulari: disponibilitat, nous, MTBF i MTTR

L'àrea és plena de termes que es fan servir com a sinònims i no ho són. Fixar-los estalvia malentesos cars.

La disponibilitat és la fracció de temps en què el servei respon correctament. S'expressa en percentatge, i en «nous»:

Disponibilitat Nom Caiguda màxima a l'any Al mes A la setmana
99 % «dos nous» 3 dies 15 h 7 h 18 min 1 h 41 min
99,9 % «tres nous» 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 % «quatre nous» 52 min 34 s 4 min 23 s 1 min
99,999 % «cinc nous» 5 min 15 s 26 s 6 s

Mirar aquella taula a poc a poc corregeix la intuïció de gairebé tothom. 99,99 % significa menys d'un minut de caiguda a la setmana, incloses les actualitzacions, els reinicis per nucli nou i les fallades del proveïdor. Amb un únic servidor, un sol reinici mensual de tres minuts ja et situa per sota de tres nous i mig. I cada nou addicional multiplica el cost aproximadament per tres.

Tres distincions que convé tenir clares:

  • Fiabilitat és la probabilitat de funcionar sense fallada durant un interval. Un sistema pot ser molt disponible i poc fiable: falla sovint, però es recupera en segons.
  • Durabilitat es refereix a les dades: la probabilitat de no perdre-les. És independent de la disponibilitat — un sistema pot estar caigut i tenir les dades perfectament segures.
  • MTBF (temps mitjà entre fallades) i MTTR (temps mitjà de reparació) descomponen la disponibilitat:
Disponibilitat = MTBF / (MTBF + MTTR)

I aquella fórmula conté la decisió estratègica de tota la lliçó: hi ha dues formes de pujar la disponibilitat. Augmentar l'MTBF —fallar menys: millor maquinari, més proves, menys canvis— o reduir l'MTTR —recuperar-se abans—. A la pràctica, reduir l'MTTR és gairebé sempre més barat i més eficaç, perquè l'MTBF té un sostre físic i l'MTTR no.

És exactament el que vas fer a 07-06 sense anomenar-ho així: passar l'MTTR de 8 hores a 50 minuts va multiplicar la disponibilitat sense comprar ni un sol component.

I la relació amb el que ja tens acordat amb la Marta:

Mètrica Què mesura Valor actual
RPO Quantes dades es poden perdre 4 hores
RTO Quant pot durar la interrupció 2 hores (revisat a 07-06)
Disponibilitat Fracció de temps funcionant Sense objectiu formal

Hi ha un forat aquí: ningú no ha fixat un objectiu de disponibilitat. I sense ell, no es pot decidir quant invertir.

Escalat vertical i horitzontal

Vertical (scale up) Horitzontal (scale out)
Què es fa Màquina més gran Més màquines
Límit El maquinari més gran que existeix Pràcticament cap
Cost Creix més que linealment Aproximadament lineal
Complexitat Cap Alta: repartiment, estat, coherència
Efecte en la disponibilitat Cap: continua havent-hi un punt únic de fallada És el que la habilita
Requereix canvis a l'aplicació No Gairebé sempre sí

La fila que importa aquí és la penúltima. Duplicar la memòria de srv-tramontana la faria més ràpida i exactament igual de fràgil. L'escalat horitzontal és el que permet que la fallada d'una màquina no sigui la fallada del servei, i per això aquesta lliçó va d'escalat horitzontal encara que el problema no sigui de capacitat.

Redundància: actiu-passiu i actiu-actiu

Tenir dues màquines no és tenir redundància: cal decidir què fa cadascuna.

Actiu-passiu Actiu-actiu
El segon node Espera, sense atendre trànsit Atén trànsit també
Aprofitament 50 % del maquinari ~100 %
Temps de commutació Segons a minuts Immediat
Complexitat Mitjana Alta
Exigeix que l'aplicació… Arrenqui a l'altre node Sigui capaç d'executar-se en diverses instàncies alhora
Risc característic Que el passiu no funcioni quan calgui Cervell dividit, coherència de dades

El risc de l'actiu-passiu se subestima sempre i mereix nom propi: un node passiu que no es fa servir mai és un node del qual no saps si funciona. Es va instal·lar fa vuit mesos, no ha rebut les últimes actualitzacions, té un certificat caducat o un disc ple. El dia de la fallada, descobreixes que el suplent també falla. L'única defensa és fer-lo servir periòdicament: commutar expressament cada poques setmanes.

L'estat és el problema difícil

Aquí està el nucli conceptual de la lliçó, i el que separa un disseny que funciona d'un que sembla que funciona.

Replicar processos és fàcil: arrenques la mateixa aplicació en dues màquines i ja en tens dues. Replicar estat és difícil, i l'estat és en més llocs dels que sembla.

Inventari de l'estat de Tramontana Reserves:

Estat On viu Problema en duplicar
Dades de reserves PostgreSQL El problema central: dues bases de dades divergeixen
Fitxers pujats /opt/tramontana/shared/uploads Una foto pujada al node A no és al B
Sessions d'usuari A la memòria del procés Una petició al node B no reconeix la sessió iniciada a A
Registres /var/log/tramontana/ Es reparteixen entre nodes: cal centralitzar per diagnosticar
Tasques programades Temporitzadors de systemd S'executarien DUES vegades: dues còpies de seguretat, dues purgues
Certificat TLS /etc/letsencrypt/ Cal sincronitzar-lo o acabar-lo al balancejador

Aquella fila de les tasques programades és la que més problemes dona a la pràctica, perquè ningú no hi pensa en duplicar un servidor. Amb dos nodes idèntics, tramontana-copia.timer es dispara a les 02:30 en tots dos: dues còpies simultànies al mateix destí, competint pel flock —que és local a cada màquina i per tant no protegeix res—. La solució exigeix un blocatge distribuït o executar les tasques només en un node designat.

Una aplicació sense estat (stateless) és aquella les instàncies de la qual no guarden res entre peticions: tot l'estat és a la base de dades o en un magatzem compartit. És el requisit per a l'actiu-actiu, i gairebé mai no es compleix sense feina de desenvolupament. A Tramontana:

Component Sense estat? Què caldria
Servir pàgines i consultar reserves Sí Res
Sessions d'usuari No Moure-les a Redis o a la base de dades
Pujada de fotos No Emmagatzematge compartit o magatzem d'objectes
Base de dades No, per definició Replicació

Conclusió honesta: Tramontana no és una aplicació sense estat avui. Abans de muntar res, calen dos canvis a l'aplicació: sessions externalitzades i fitxers en emmagatzematge compartit. Això és feina de desenvolupament, i dir-l'hi a la Marta abans de comprar servidors és part de la feina.

Balanceig de càrrega: capes, algorismes i comprovacions de salut

Un balancejador reparteix peticions entre diversos servidors. Opera en una de dues capes:

Capa 4 (transport) Capa 7 (aplicació)
Què inspecciona IP i port Capçaleres HTTP, rutes, galetes
Rendiment Molt alt Alt, però menor
Terminació TLS No pot Sí
Repartiment per ruta o domini No Sí
Reintent d'una petició fallida No Sí
Exemples IPVS, nftables, NLB HAProxy, Nginx, Traefik

Per a HTTP, la capa 7 és gairebé sempre la correcta: permet acabar TLS en un punt, repartir segons la ruta, reintentar una petició que falla i fer comprovacions de salut significatives.

Algorismes

Algorisme Com reparteix Quan
roundrobin Per torns Peticions homogènies, servidors iguals
leastconn Al que menys connexions té Peticions de durada variable
source Hash de la IP d'origen Quan cal afinitat (amb reserves)
uri Hash de la ruta Memòries cau: la mateixa ruta sempre al mateix node
random A l'atzar, amb dos sondejos Molts balancejadors en paral·lel

leastconn és el més adequat per a Tramontana: una consulta de disponibilitat triga 40 ms i un informe de facturació uns quants segons, així que repartir per torns amuntegaria les lentes al mateix node.

Comprovacions de salut

És la peça que fa útil el balancejador: sense ella, continuaria enviant trànsit a un servidor caigut.

Tipus Com funciona Avantatge
Activa El balancejador sondeja cada N segons Detecta la fallada encara que no hi hagi trànsit
Passiva Observa els errors del trànsit real Sense cost afegit; detecta fallades parcials

El correcte és fer servir totes dues. I aquí encaixa una cosa que tens des del Mòdul 4: revisio_salut.sh retorna 0, 1 o 2 — correcte, avís, crític. Aquella gradació és exactament el que un balancejador necessita, si l'aplicació exposa un extrem equivalent:

Estat HTTP Què ha de fer el balancejador
0 correcte 200 Enviar trànsit normalment
1 avís 200 amb capçalera d'avís Enviar, però amb menys pes
2 crític 503 No enviar trànsit

I la distinció que gairebé ningú no fa bé: una comprovació superficial és pitjor que cap. Si l'extrem de salut només respon «estic viu» sense comprovar la base de dades, el balancejador continuarà enviant trànsit a un node que no pot atendre cap petició. Si comprova massa —per exemple, una consulta pesada—, un problema de la base de dades marca tots els nodes com a caiguts i el servei desapareix del tot, quan podria haver servit almenys les pàgines estàtiques.

L'equilibri: comprovar les dependències crítiques amb una operació barata. Una consulta SELECT 1 amb temps límit, no un informe.

Sessions persistents: un antipatró

La sticky session lliga cada client a un node, per galeta o per IP. Resol el problema de les sessions en memòria... i en crea d'altres:

  • El repartiment es desequilibra: un node pot acabar amb el triple de càrrega.
  • Quan un node cau, tots els seus usuaris perden la sessió de cop.
  • Impedeix drenar un node per a manteniment sense afectar ningú.
  • Emmascara el problema real en lloc de resoldre'l.

És una solució transitòria acceptable mentre s'externalitzen les sessions. Com a solució permanent, no.

Drenatge de connexions

Per desplegar sense tallar el servei, un node ha de poder sortir del repartiment ordenadament: deixar de rebre peticions noves i acabar les que ja té en curs.

# 1. Treure el node del repartiment (deixa de rebre peticions noves)
$ echo "set server tramontana/app1 state drain" | \
    sudo socat stdio /run/haproxy/admin.sock

# 2. Esperar que acabin les connexions en curs
$ 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 amb tranquil·litat
$ ssh [email protected] 'sudo ~/scripts/desplegar.sh 3.3.0'

# 4. Tornar-lo al repartiment
$ echo "set server tramontana/app1 state ready" | \
    sudo socat stdio /run/haproxy/admin.sock

Repetit node a node, això és un desplegament sense interrupció. I és la resposta a una cosa que va quedar pendent des de 04-07: desplegar.sh reinicia el servei, i aquell reinici són uns segons de peticions rebutjades. Amb dos nodes i drenatge, cap.

HAProxy a 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
    # Socol d administracio: permet drenar nodes en calent
    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 modern (06-05). Mai no inventis la llista 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 un ALTRE servidor si el primer falla. Nomes es segur per a
    # peticions idempotents: per aixo 'if-none', que exclou POST.
    retry-on all-retryable-errors
    option  redispatch
    retries 3
    timeout connect 5s
    timeout client  30s
    timeout server  30s
    timeout http-request 10s
    # Drenatge: espera que acabin les connexions en treure un node
    default-server init-addr last,libc,none

# ---------- Entrada ----------
frontend tramontana_https
    bind *:443 ssl crt /etc/haproxy/certs/reserves.tramontana.example.pem alpn h2,http/1.1
    bind *:80
    # Redirigeix HTTP a HTTPS, llevat del desafiament 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). Comencar amb max-age curt i pujar-lo despres.
    http-response set-header Strict-Transport-Security "max-age=63072000"

    # Limit de taxa per IP: complementa fail2ban a 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

# ---------- Servidors d aplicacio ----------
backend tramontana_app
    # leastconn: les peticions tenen durada molt variable
    balance leastconn

    # Comprovacio de salut ACTIVA a la capa 7.
    # L extrem /salut comprova les dependencies critiques de forma barata.
    option httpchk
    http-check send meth GET uri /salut ver HTTP/1.1 hdr Host reserves.tramontana.example
    http-check expect status 200

    # Capcaleres perque l aplicacio sapiga qui es el client real
    http-request set-header X-Forwarded-Proto https if { ssl_fc }
    option forwardfor

    # inter: cada quant sondejar. rise/fall: quantes vegades seguides
    # per considerar el node sa o caigut. slowstart: en tornar, puja
    # el pes progressivament en lloc de rebre tota la carrega de cop.
    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

    # Comprovacio PASSIVA: si un servidor dona errors al transit real,
    # es treu temporalment encara que el seu /salut respongui.
    option redispatch

# ---------- Estadistiques, nomes des de la xarxa interna ----------
listen stats
    bind 10.0.2.20:8404
    stats enable
    stats uri /
    stats refresh 5s
    stats admin if TRUE
    acl xarxa_interna src 10.0.2.0/24
    http-request deny unless xarxa_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

Quatre detalls d'aquella configuració que mereixen explicació:

slowstart 30s. Quan un node torna després d'una fallada, les seves memòries cau són fredes i els seus grups de connexions buits. Enviar-li un terç del trànsit de cop el pot tombar un altre cop, i entrar en un cicle de caigudes. slowstart puja el seu pes progressivament durant 30 segons.

rise 2 fall 3. Asimètric a propòsit: calen tres fallades seguides per treure un node (evita falsos positius per un pic) però només dos èxits per tornar-lo. Treure de cop per una fallada transitòria és com un balancejador provoca una caiguda.

option redispatch amb retry-on. Si el servidor triat falla, reintenta en un altre. Amb un matís important: reintentar un POST pot duplicar una reserva. HAProxy només reintenta quan la petició no s'ha enviat o el mètode és idempotent.

El stick-table amb límit de taxa. És el fail2ban de 06-03 a la capa 7: compta peticions per IP en finestres de 10 segons i retorna 429 en superar el llindar. Complementari, no substitut: fail2ban bloqueja a nivell de xarxa i aquest a nivell d'aplicació.

Nginx pot fer el mateix amb upstream, i és l'opció natural si ja el tens com a servidor intermediari invers — que és el que muntaràs a 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 diferència pràctica: Nginx només fa comprovacions passives a la seva versió lliure; les actives són de la versió comercial. HAProxy les porta de sèrie, i per això és preferible quan el balanceig és la funció principal.

Alta disponibilitat del balancejador: keepalived i VRRP

I aquí està l'error de disseny més comú de l'àrea, tan freqüent que mereix enunciar-se sol:

Posar un balancejador davant de dos servidors no elimina el punt únic de fallada: el mou al balancejador.

Abans tenies una màquina que podia caure. Ara en tens tres, i si cau la que reparteix, el servei desapareix igual — amb l'agreujant que ara hi ha més peces que poden fallar.

La solució és VRRP (Virtual Router Redundancy Protocol): dos balancejadors comparteixen una IP virtual flotant, la que apunta el DNS. Un la té; si deixa d'anunciar-se, l'altre se la queda en segons.

$ sudo apt install keepalived
# /etc/keepalived/keepalived.conf — NODE PRINCIPAL (lb1, 10.0.2.20)
global_defs {
    router_id lb1
    enable_script_security
    script_user keepalived_script
}

# Comprovacio del servei LOCAL. Si HAProxy mor en aquest node, la
# prioritat baixa i l altre es queda la IP. Sense aixo, un node amb
# keepalived viu i HAProxy mort reté la IP i el servei cau.
vrrp_script comprovar_haproxy {
    script "/usr/bin/killall -0 haproxy"
    interval 2
    weight -40        # baixa la prioritat 40 punts si falla
    fall 2
    rise 2
}

vrrp_instance TRAMONTANA_VIP {
    state MASTER
    interface enp0s3
    virtual_router_id 51        # IDENTIC en tots dos nodes
    priority 100                # mes gran que el suplent
    advert_int 1                # anunci cada segon

    authentication {
        auth_type PASS
        auth_pass {{ vault_vrrp_pass }}
    }

    virtual_ipaddress {
        10.0.2.30/24 dev enp0s3
    }

    track_script {
        comprovar_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"
}
# NODE SUPLENT (lb2, 10.0.2.21): identic llevat de tres linies
    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 es a lb1

# A lb2, la VIP NO apareix: esta en espera
$ ssh [email protected] 'ip -brief addr show enp0s3'
enp0s3  UP  10.0.2.21/24

El mecanisme: el node amb més prioritat anuncia la VIP cada segon. Si el suplent deixa de sentir anuncis durant tres intervals, assumeix que el principal ha caigut, s'assigna la IP i envia un ARP gratuït perquè els commutadors de la xarxa actualitzin les seves taules. La commutació triga entre 3 i 4 segons.

El track_script amb weight -40 és el que evita la fallada més ximple d'aquesta arquitectura: sense ell, un node on keepalived funciona però HAProxy està mort reté la VIP alegrement, i el servei queda caigut encara que l'altre balancejador estigui perfectament. Amb ell, la prioritat d'lb1 baixa de 100 a 60, per sota dels 90 d'lb2, i la VIP es mou.

I enable_script_security amb script_user és una precaució de seguretat real: keepalived corre com a root i executa aquells scripts; sense aquelles directives, un script amb permisos laxos és una escalada de privilegis.

Cervell dividit i quòrum

La fallada característica de qualsevol sistema redundant, i la que causa les pèrdues de dades més greus.

El cervell dividit (split-brain) passa quan els nodes deixen de veure's entre si però tots dos continuen funcionant. Cadascun conclou que l'altre ha caigut, i tots dos es declaren principals:

  • Dos balancejadors anuncien la mateixa IP virtual → trànsit impredictible, connexions tallades.
  • Dues bases de dades accepten escriptures → les dades divergeixen, i reconciliar-les després pot ser impossible.

L'escenari que ho provoca no és que un node caigui —això es gestiona bé—, sinó que la xarxa entre ells es talli mentre tots dos continuen vius i atenent clients. És més freqüent del que sembla: un commutador que falla, una regla de tallafocs mal posada, una saturació momentània.

Les tres defenses:

Defensa Com funciona Limitació
Quòrum Només actua el grup amb majoria de nodes Exigeix 3 o més nodes senars
Delimitació (STONITH) El supervivent apaga l'altre per maquinari Requereix control remot d'energia o de l'hipervisor
Testimoni Un tercer recurs arbitra (un disc, una IP) Menys robust que el quòrum real

I la conclusió que cal retenir:

Amb dos nodes no hi ha quòrum possible. Cap dels dos no pot tenir majoria de dos. Per això un clúster seriós té tres nodes, o dos més un testimoni.

Això té una conseqüència directa en el disseny de Tramontana: un parell de balancejadors amb VRRP és acceptable, perquè el pitjor cas —dos nodes anunciant la mateixa IP uns segons— és molest però recuperable. Un parell de bases de dades amb commutació automàtica no ho és: el pitjor cas és la divergència de dades, i no es recupera. D'aquí la recomanació de l'apartat següent.

Replicació de dades

PostgreSQL replica mitjançant el registre d'escriptura anticipada (WAL): el primari envia els seus canvis a una o més rèpliques.

Mode El primari confirma quan… Risc de pèrdua Cost
Asíncrona Ha escrit localment Les últimes transaccions Cap
Síncrona La rèplica ha confirmat Cap Latència de cada escriptura

I el compromís, que cal entendre abans de triar: amb replicació síncrona, cada escriptura espera el viatge d'anada i tornada a la rèplica. A la mateixa xarxa local, un o dos mil·lisegons. Entre centres de dades, desenes. I si la rèplica cau, el primari es bloqueja esperant confirmació — llevat que es configuri synchronous_standby_names amb la sintaxi adequada, cosa que obre la porta a perdre dades justament quan més importa.

Per a Tramontana, amb un RPO de 4 hores acordat, la replicació asíncrona és més que suficient: perdre els últims segons de transaccions està molt dins del que es tolera.

# postgresql.conf al PRIMARI
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 des de zero
$ 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 retard_bytes FROM pg_stat_replication;"
 client_addr | state     | sync_state | retard_bytes
-------------+-----------+------------+--------------
 10.0.2.22   | streaming | async      |            0

Aquell retard_bytes és la mètrica que cal vigilar: és quantes dades es perdrien si el primari caigués ara. Va directa a la monitoració, al costat de l'antiguitat de l'última còpia de 05-08.

Commutació: manual davant d'automàtica

Manual Automàtica
Temps de recuperació Minuts Segons
Risc de cervell dividit Cap Alt sense quòrum
Requereix personal disponible Sí No
Eines pg_ctl promote Patroni, repmgr, pg_auto_failover

La recomanació professional, i va contra la intuïció:

Amb dos nodes, la commutació de base de dades ha de ser manual. Sense quòrum, una fallada de xarxa entre primari i rèplica pot promoure la rèplica mentre el primari continua acceptant escriptures, i aleshores hi ha dues bases de dades divergents. Recuperar-se d'allà pot significar perdre transaccions o reconciliar a mà.

Eines com Patroni fan commutació automàtica segura, però requereixen un magatzem de configuració distribuït amb quòrum propi (etcd o Consul), que al seu torn necessita tres nodes. És a dir: automatitzar la commutació de forma segura exigeix, com a mínim, cinc màquines. És una decisió d'arquitectura, no una casella per marcar.

Emmagatzematge compartit

Per a /opt/tramontana/shared/uploads:

Opció Avantatge Inconvenient
NFS Simple, funciona sense tocar l'aplicació És un nou punt únic de fallada
GlusterFS / Ceph Distribuït i replicat Complexitat alta
Magatzem d'objectes (S3, MinIO) Escalable, gestionat, replicat Requereix canviar l'aplicació
rsync periòdic Trivial No és temps real: es perden fitxers

NFS té la ironia característica: es munta per donar alta disponibilitat i, si no es fa també redundant, empitjora la fiabilitat, perquè ara dos servidors depenen d'una tercera màquina.

La resposta moderna és el magatzem d'objectes: és el que fan servir les aplicacions dissenyades per escalar horitzontalment. Exigeix feina de desenvolupament, i és la mateixa conclusió de l'apartat de l'estat.

Clústers, Pacemaker i delimitació

Per a escenaris més complexos —bases de dades, sistemes de fitxers compartits, recursos amb dependències— existeix la pila Pacemaker + Corosync:

  • Corosync: comunicació entre nodes, pertinença al clúster i quòrum.
  • Pacemaker: gestió de recursos, amb dependències i restriccions.
$ sudo pcs status
Cluster name: tramontana
Cluster Summary:
  * Stack: corosync
  * Current DC: node1 (version 2.1.6) - partition with quorum
  * 3 nodes configured
  * 4 resource instances configured

Full List of Resources:
  * vip_tramontana  (ocf:heartbeat:IPaddr2):  Started node1
  * postgresql      (ocf:heartbeat:pgsql):    Master node1

Un recurs és qualsevol cosa que el clúster gestiona (una IP, un servei, un sistema de fitxers), i les restriccions expressen regles: «la IP ha de ser on és el primari de PostgreSQL», «aquest servei ha d'arrencar després d'aquell».

I el concepte sense el qual res d'això no funciona de debò:

Delimitació (o STONITH, Shoot The Other Node In The Head): abans de prendre els recursos d'un node que sembla caigut, el supervivent s'assegura que està apagat, tallant-li l'energia o destruint la seva màquina virtual.

Per què és imprescindible: si el node A sembla caigut però en realitat només està congelat o incomunicat, i B pren els seus recursos —munta el sistema de fitxers, promou la base de dades, agafa la IP—, quan A es recuperi tots dos escriuran a les mateixes dades. La corrupció resultant pot ser irrecuperable.

# Delimitacio sobre libvirt: destrueix la maquina virtual de l altre node
$ sudo pcs stonith create fence_node2 fence_virsh \
    ip=10.0.2.10 login=fence identity_file=/etc/pacemaker/id_rsa \
    pcmk_host_list=node2 action=off

En maquinari físic es fa amb IPMI, iLO o unitats de distribució d'energia gestionables. I la regla de l'àrea és contundent: un clúster sense delimitació configurada no és un clúster fiable. Molts projectes la desactiven «temporalment» perquè complica les proves, i aquella és exactament la configuració que provoca la pèrdua de dades el dia de la fallada.

Disseny per a Tramontana i anàlisi de cost

graph TD
    I["Internet"] --> DNS["DNS: reserves.tramontana.example<br/>→ 10.0.2.30 (VIP)"]
    DNS --> VIP{"IP virtual flotant<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 primari<br/>10.0.2.15"]
    APP2 --> BD1
    BD1 -->|WAL asincron| BD2["PostgreSQL replica<br/>10.0.2.16"]
    APP1 --> OBJ["Magatzem d objectes<br/>uploads"]
    APP2 --> OBJ

Què protegeix cada capa, i què no:

Fallada Coberta? Temps de recuperació
Cau app1 o app2 Sí, automàtic 3-9 s (fall 3 × inter 3s)
Cau lb1 Sí, automàtic 3-4 s (VRRP)
Cau el primari de PostgreSQL No: promoció manual deliberada 5-15 min
Falla la xarxa entre lb1 i lb2 Parcial: cervell dividit breu, recuperable Segons
Cau l'amfitrió que allotja tot No RTO complet
Una fallada lògica (esborrat, corrupció) No: es replica a l'instant Restaurar còpia
Un desplegament defectuós No: es desplega en tots dos Reversió

Les tres últimes files són les que cal subratllar davant de la direcció. La redundància no protegeix contra els errors lògics: un DELETE equivocat es replica en mil·lisegons a la rèplica. Per a això hi ha les còpies de 05-08, i aquesta arquitectura no les substitueix en absolut.

L'anàlisi de cost

El que la lliçó ve prometent, i el que la Marta fa tres respostes que demana:

Cost de l'arquitectura completa (5 màquines davant d'1):

Concepte Estimació anual
4 màquines addicionals Cost del VPS actual × 4
Feina de desenvolupament (sessions + magatzem d'objectes) 3-4 setmanes, una vegada
Muntatge i automatització amb Ansible 2 setmanes, una vegada
Manteniment addicional ~4 h/mes de forma indefinida
Magatzem d'objectes Baix, per volum
Complexitat: més modes de fallada Difícil de quantificar, real

Cost de la indisponibilitat. La pregunta que cal respondre abans:

Reserves al mes: ~500
Import mitja:    313,70 €
Ingres mitja per hora (24×30):  500 × 313,70 / 720 ≈ 218 €/h

Però aquell càlcul brut és enganyós per tres motius, i dir-ho és part de l'anàlisi:

  1. Les reserves no són uniformes. Es concentren a la tarda i en cap de setmana. Una caiguda de dues hores un dimarts de matinada pot costar zero; la mateixa caiguda un divendres a la tarda, molt més que la mitjana.
  2. Una reserva perduda no sempre es perd. Molts clients ho tornen a intentar. El cost real està entre el 20 % i el 50 % de la xifra bruta.
  3. Hi ha cost reputacional, no lineal i difícil de quantificar, especialment si la caiguda coincideix amb una campanya.

Estimació raonable del cost anual d'indisponibilitat, amb diferents objectius:

Disponibilitat Caiguda anual Cost estimat (a 218 €/h × 35 %)
99,5 % (situació actual estimada) 43,8 h ~3.340 €
99,9 % 8,8 h ~670 €
99,95 % 4,4 h ~335 €

L'estalvi de passar de 99,5 % a 99,9 % és d'uns 2.700 € l'any. Això cal comparar-ho amb el cost de l'arquitectura completa —quatre màquines més, sis setmanes de feina inicial i quatre hores mensuals indefinides—, que molt probablement el supera.

I aquí està l'anàlisi que gairebé ningú no fa, i que canvia la recomanació:

Mesura Cost Disponibilitat estimada
Situació actual — ~99,5 %
Només dos nodes d'aplicació + un balancejador 2 màquines, 1 setmana ~99,8 %
Arquitectura completa amb BD replicada 4 màquines, 6 setmanes ~99,9 %

El primer pas captura la major part del benefici per una fracció del cost, i això és l'habitual en disponibilitat: els primers nous són barats i els següents s'encareixen exponencialment. La raó és que la majoria de les interrupcions no vénen d'una fallada de maquinari, sinó de desplegaments, reinicis i manteniment — i tot això es cobreix amb dos nodes d'aplicació i drenatge de connexions, sense tocar la base de dades.

Recomanació

Fase 1 (ara): fixar un objectiu formal de disponibilitat amb la Marta. Sense objectiu no es pot decidir quant invertir. I mesurar-la, perquè el 99,5 % actual és una estimació, no una dada.

Fase 2 (1-2 mesos): dos nodes d'aplicació i un balancejador. Sessions externalitzades i fitxers en magatzem d'objectes, que és feina de desenvolupament. Elimina les interrupcions per desplegament i per fallada d'un node, que són la majoria.

Fase 3 (avaluar després): segon balancejador amb VRRP, i rèplica de PostgreSQL amb promoció manual.

Fase 4 (probablement mai): commutació automàtica de base de dades. Exigeix quòrum, és a dir, cinc màquines i un magatzem distribuït. Per a una empresa d'aquesta mida, la complexitat supera el benefici.

Provar les fallades provocant-les

Un sistema d'alta disponibilitat no provat no és un sistema d'alta disponibilitat: és una arquitectura que et penses que funciona. I la història de l'àrea és plena de casos en què el suplent va fallar el dia que va caldre.

# --- Prova 1: cau un node d aplicacio ---
$ ssh [email protected] 'sudo systemctl stop tramontana'
# Esperat: HAProxy el treu en ~9 s (fall 3 x inter 3s), sense errors visibles
$ for i in {1..60}; do
      curl -s -o /dev/null -w '%{http_code} ' https://reserves.tramontana.example/cases
      sleep 1
  done; echo
# Criteri: cap resposta != 200

# --- Prova 2: cau el balancejador principal ---
$ ssh [email protected] 'sudo systemctl stop keepalived'
# Esperat: la VIP es mou a lb2 en 3-4 s
$ ssh [email protected] 'ip -brief addr show enp0s3 | grep 10.0.2.30'

# --- Prova 3: HAProxy mor pero keepalived viu ---
# Es la prova que valida el track_script. Sense ell, la VIP NO es mou
# i el servei cau amb tots dos balancejadors "vius".
$ ssh [email protected] 'sudo systemctl stop haproxy'
$ sleep 6 && ssh [email protected] 'ip -brief addr show enp0s3 | grep 10.0.2.30'

# --- Prova 4: particio de xarxa entre balancejadors (cervell dividit) ---
$ ssh [email protected] 'sudo nft add rule inet filter input ip saddr 10.0.2.21 drop'
# Esperat: TOTS DOS prenen la VIP. Documentar el comportament observat.
$ ssh [email protected] 'sudo nft flush chain inet filter input'

# --- Prova 5: desplegament sense interrupcio ---
$ 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
# Criteri: cap error durant tot el proces

La prova 4 és la més incòmoda i la més important: no es pot «aprovar» amb dos nodes, perquè sense quòrum el cervell dividit és inevitable. El que es fa és documentar el comportament observat i el procediment de recuperació, perquè el dia que passi ningú no improvisi.

L'enginyeria del caos porta això més lluny: injectar fallades de forma contínua i automatitzada en producció, amb la lògica que si les fallades passen cada dia, el sistema està obligat a tolerar-les i l'equip obligat a saber respondre. Per a Tramontana és desproporcionat; el principi —provoca les fallades tu, en horari laboral, en lloc d'esperar-les— és aplicable a qualsevol escala, i encaixa amb el simulacre semestral que vas proposar a 07-06.

Errors Comuns i Consells

  • Posar un balancejador i creure que ja hi ha alta disponibilitat. Només has mogut el punt únic de fallada. El balancejador necessita la seva pròpia redundància.
  • Configurar VRRP sense track_script. Un node amb keepalived viu i HAProxy mort reté la IP virtual, i el servei queda caigut amb tots dos balancejadors «funcionant».
  • Commutació automàtica de base de dades amb dos nodes. Sense quòrum, una partició de xarxa produeix dos primaris i dades divergents. Manual, fins que hi hagi tres nodes.
  • Desactivar la delimitació «temporalment». És la configuració que provoca la corrupció de dades el dia de la fallada. Un clúster sense delimitació no és fiable.
  • Comprovacions de salut superficials. Un /salut que només diu «estic viu» fa que el balancejador enviï trànsit a un node que no pot atendre res.
  • Comprovacions de salut massa profundes. Una fallada de la base de dades marca tots els nodes com a caiguts, quan haurien pogut servir alguna cosa.
  • Treure un node a la primera fallada. Un pic transitori treu el node, la càrrega es concentra en els altres, i cauen en cascada. fall 3 i slowstart.
  • Oblidar que els temporitzadors s'executen a tots els nodes. Dues còpies de seguretat simultànies, dues purgues. El flock és local i no protegeix.
  • Sessions persistents com a solució permanent. Emmascara el problema real, desequilibra la càrrega, i quan un node cau perd totes les seves sessions.
  • Creure que la replicació substitueix les còpies. Un esborrat accidental es replica en mil·lisegons. La redundància protegeix de la fallada de maquinari, no de l'error humà.
  • NFS sense redundància per donar alta disponibilitat. Afegeixes un punt únic de fallada del qual ara depenen dos servidors.
  • No provar les fallades. Un sistema d'alta disponibilitat no provat és una arquitectura que et penses que funciona.
  • Consell de mètode. Abans de dissenyar res, fixa l'objectiu de disponibilitat i mesura l'actual. Sense aquells dos números, qualsevol inversió és una intuïció disfressada d'arquitectura.

Exercicis

Exercici 1

Dissenya l'extrem /salut que ha d'exposar l'aplicació perquè HAProxy prengui bones decisions. Especifica què comprova, què no, quins codis retorna i amb quina latència, i explica com es relaciona amb revisio_salut.sh.

Exercici 2

Escriu el rol d'Ansible que desplega la configuració d'HAProxy i keepalived als dos balancejadors, tenint en compte que la configuració difereix entre ells i que aplicar-la malament deixa el servei sense IP.

Exercici 3

La Marta pregunta directament: «Necessitem alta disponibilitat?». Redacta la resposta amb l'anàlisi completa i una recomanació clara.

Solucions

Solució 1

Disseny de l'extrem /salut:

GET /salut
Comprova Com Per què
El procés respon Que la petició arribi Trivial però necessari
Connexió a PostgreSQL SELECT 1 amb temps límit d'1 s Sense base de dades no pot atendre res
Grup de connexions Que n'hi hagi almenys una de lliure Amb el grup exhaurit, les peticions s'encuen
Magatzem de fitxers Escriptura d'un byte cada 30 s (en memòria cau) Sense ell fallen les pujades, però no les consultes
Espai a /var/log Llindar del 95 % (en memòria cau) Sense lloc per als registres, es perd la traçabilitat
NO comprova Per què
Consultes de negoci reals Cares; un problema de dades tombaria tots els nodes
Serveis externs (correu, pagaments) Una fallada aliena trauria tot el servei del repartiment
Estat d'altres nodes Cada node informa només de si mateix
Mètriques de rendiment Això és monitoració, no salut

Els tres estats i la seva resposta:

// 200 OK — estat 0, correcte
{
  "estat": "ok",
  "versio": "3.2.1",
  "comprovacions": {
    "bd": {"ok": true, "latencia_ms": 2},
    "pool": {"ok": true, "lliures": 68, "total": 80},
    "magatzem": {"ok": true},
    "disc_logs": {"ok": true, "us_pct": 34}
  }
}
// 200 OK amb capcalera X-Salut-Avis: degradat — estat 1
// CONTINUA rebent transit: pot atendre consultes
{
  "estat": "degradat",
  "avisos": ["magatzem_no_disponible"],
  "comprovacions": {
    "bd": {"ok": true, "latencia_ms": 3},
    "pool": {"ok": true, "lliures": 12, "total": 80},
    "magatzem": {"ok": false, "error": "timeout"},
    "disc_logs": {"ok": true, "us_pct": 34}
  }
}
// 503 Service Unavailable — estat 2, critic
// NO rep transit
{
  "estat": "critic",
  "errors": ["bd_inaccessible"],
  "comprovacions": {
    "bd": {"ok": false, "error": "connection refused"}
  }
}

La relació amb revisio_salut.sh és la part conceptual de l'exercici. Tots dos responen a la mateixa pregunta des de costats diferents, i aquella complementarietat cal aprofitar-la en lloc de duplicar feina:

revisio_salut.sh /salut
Qui pregunta L'administrador o un temporitzador HAProxy, cada 3 s
Des d'on Fora del procés, a la màquina Des de dins del procés
Veu l'estat intern No Sí: grup de connexions, memòries cau
Freqüència Minuts Segons
Cost acceptable Alt Molt baix
Codis 0 / 1 / 2 200 / 200+capçalera / 503

El correcte és que revisio_salut.sh consumeixi /salut en lloc de reimplementar les comprovacions:

# Fragment de revisio_salut.sh, adaptat
comprovar_aplicacio() {
    local resposta codi estat
    resposta="$(curl -s -m 5 -w '\n%{http_code}' \
        "http://127.0.0.1:${TRAMONTANA_PORT}/salut")" || {
        error "l aplicacio no respon"
        return 2
    }
    codi="$(tail -1 <<<"$resposta")"
    estat="$(sed '$d' <<<"$resposta" | jq -r '.estat')"

    case "$estat" in
        ok)       log "aplicacio correcta"; return 0 ;;
        degradat) error "degradada: $(sed '$d' <<<"$resposta" | jq -r '.avisos|join(", ")')"
                  return 1 ;;
        critic)   error "critica: $(sed '$d' <<<"$resposta" | jq -r '.errors|join(", ")')"
                  return 2 ;;
        *)        error "estat desconegut (HTTP $codi)"; return 2 ;;
    esac
}

Quatre decisions de disseny que cal justificar:

  1. L'estat degradat retorna 200, no 503. És contraintuïtiu i és correcte: si el magatzem de fitxers falla, el node pot continuar atenent consultes de disponibilitat, que són la majoria del trànsit. Treure'l del repartiment reduiria la capacitat sense guanyar res. La capçalera permet que la monitoració ho detecti sense que el balancejador actuï.
  2. Latència acotada per disseny. Amb 3 segons d'interval i dos nodes, són 40 peticions per minut. L'objectiu és menys de 50 ms, i d'aquí que les comprovacions cares estiguin a la memòria cau 30 segons.
  3. Sense autenticació, però restringit per xarxa. Ha de ser accessible sense credencials perquè HAProxy el consulti, i no ha d'estar exposat a Internet: el detall del JSON és informació útil per a un atacant. Es bloqueja al frontend:
    http-request deny if { path_beg /salut } !{ src 10.0.2.0/24 }
    
  4. Sense efectes secundaris i sense blocatge. Un /salut que escriu a la base de dades o pren un blocatge pot provocar la fallada que pretén detectar. La comprovació d'escriptura del magatzem va a la memòria cau i en segon pla.

I l'advertiment final, que és l'error més comú: /salut no ha de comprovar dependències que no són pròpies d'aquest node. Si comprovés l'estat de la base de dades amb una consulta pesada, un problema de PostgreSQL trauria tots els nodes alhora i el servei desapareixeria del tot, quan podria haver retornat almenys una pàgina d'error decent.

Solució 2

# roles/balanceador/defaults/main.yml
haproxy_vip: 10.0.2.30
haproxy_vip_cidr: 24
haproxy_interficie: enp0s3
haproxy_vrrp_id: 51
haproxy_backends:
  - { nom: app1, adreca: 10.0.2.21, port: 8080 }
  - { nom: app2, adreca: 10.0.2.22, port: 8080 }
haproxy_check_inter: 3s
haproxy_check_rise: 2
haproxy_check_fall: 3
haproxy_slowstart: 30s
# inventari/produccio.yml (fragment)
balancejadors:
  hosts:
    lb1:
      ansible_host: 10.0.2.20
      vrrp_estat: MASTER
      vrrp_prioritat: 100
    lb2:
      ansible_host: 10.0.2.21
      vrrp_estat: BACKUP
      vrrp_prioritat: 90
# roles/balanceador/tasks/main.yml
---
# =====================================================================
# PRECAUCIO: una configuracio incorrecta de keepalived pot deixar el
# servei SENSE IP VIRTUAL, es a dir, completament caigut. Aquest rol
# desplega els nodes D UN EN UN i verifica entre ells.
# =====================================================================

- name: "Verificar que no s executa en paral·lel sobre tots dos balancejadors"
  ansible.builtin.assert:
    that: ansible_play_hosts | length == 1
    fail_msg: >-
      Aquest rol s ha d executar amb --limit sobre UN balancejador cada
      vegada, o amb serial: 1 al play. Configurar tots dos alhora pot
      deixar el servei sense IP virtual.

- name: Instal·lar HAProxy i keepalived
  ansible.builtin.apt:
    name: [haproxy, keepalived, socat]
    state: present
  tags: [paquets]

# ---------- HAProxy ----------
- name: Crear el directori de certificats
  ansible.builtin.file:
    path: /etc/haproxy/certs
    state: directory
    owner: root
    group: haproxy
    mode: '0750'
  tags: [tls]

- name: Instal·lar el certificat combinat per a HAProxy
  ansible.builtin.copy:
    content: "{{ vault_certificat_pem }}"
    dest: /etc/haproxy/certs/reserves.tramontana.example.pem
    owner: root
    group: haproxy
    mode: '0640'
  no_log: true                    # conte la clau privada
  notify: Recarregar haproxy
  tags: [tls]

- name: Desplegar la configuracio d HAProxy
  ansible.builtin.template:
    src: haproxy.cfg.j2
    dest: /etc/haproxy/haproxy.cfg
    owner: root
    group: root
    mode: '0644'
    backup: true
    # VALIDA abans d instal·lar: una config trencada impedeix arrencar
    validate: 'haproxy -c -f %s'
  notify: Recarregar haproxy
  tags: [haproxy]

- name: Activar HAProxy
  ansible.builtin.systemd_service:
    name: haproxy
    enabled: true
    state: started
  tags: [haproxy]

# ---------- keepalived ----------
# El sysctl que permet a HAProxy escoltar en una IP que aquest node
# encara no te (la VIP quan es a l altre). Sense aixo, HAProxy no
# arrenca al node BACKUP.
- name: Permetre bind sobre IP no locals
  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 l usuari per als scripts de keepalived
  ansible.builtin.user:
    name: keepalived_script
    system: true
    shell: /usr/sbin/nologin
    create_home: false
  tags: [keepalived]

- name: Instal·lar l script de notificacio de VRRP
  ansible.builtin.template:
    src: vrrp-notificar.j2
    dest: /usr/local/sbin/vrrp-notificar
    owner: root
    group: root
    mode: '0755'      # NO escrivible per l usuari de l script
  tags: [keepalived]

- name: Desplegar la configuracio de keepalived
  ansible.builtin.template:
    src: keepalived.conf.j2
    dest: /etc/keepalived/keepalived.conf
    owner: root
    group: root
    mode: '0600'      # conte la contrasenya 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]

# ---------- Verificacio ----------
- name: Forcar els manejadors abans de verificar
  ansible.builtin.meta: flush_handlers

- name: "Verificar | HAProxy respon al socol d administracio"
  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: estat_backend
  changed_when: false
  failed_when: "'UP' not in estat_backend.stdout"
  tags: [verificacio]

- name: "Verificar | Els backends estan sans"
  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: [verificacio]

- name: "Verificar | La VIP es al MASTER i nomes alla"
  ansible.builtin.shell:
    cmd: "ip -brief addr show {{ haproxy_interficie }} | grep -c {{ haproxy_vip }} || true"
  register: te_vip
  changed_when: false
  tags: [verificacio]

- name: "Verificar | Coherencia de l estat de la VIP"
  ansible.builtin.assert:
    that:
      - (vrrp_estat == 'MASTER' and te_vip.stdout | int == 1) or
        (vrrp_estat == 'BACKUP' and te_vip.stdout | int == 0)
    fail_msg: >-
      Estat de VIP incoherent a {{ inventory_hostname }}
      ({{ vrrp_estat }}, VIP present: {{ te_vip.stdout }}).
      Revisar abans de continuar amb l altre balancejador.
  tags: [verificacio]

- name: "Verificar | El servei respon a traves de la VIP"
  ansible.builtin.uri:
    url: "https://reserves.tramontana.example/salut"
    validate_certs: true
    status_code: 200
    timeout: 10
  delegate_to: localhost
  become: false
  run_once: true
  tags: [verificacio]
# roles/balanceador/handlers/main.yml
- name: Recarregar haproxy
  ansible.builtin.systemd_service:
    name: haproxy
    state: reloaded      # reload, no restart: no talla les connexions

- name: Reiniciar keepalived
  ansible.builtin.systemd_service:
    name: keepalived
    state: restarted
{# roles/balanceador/templates/keepalived.conf.j2 #}
# GENERAT PER ANSIBLE — NO EDITAR A MA
global_defs {
    router_id {{ inventory_hostname }}
    enable_script_security
    script_user keepalived_script
}

vrrp_script comprovar_haproxy {
    script "/usr/bin/killall -0 haproxy"
    interval 2
    weight -40
    fall 2
    rise 2
}

vrrp_instance TRAMONTANA_VIP {
    state {{ vrrp_estat }}
    interface {{ haproxy_interficie }}
    virtual_router_id {{ haproxy_vrrp_id }}
    priority {{ vrrp_prioritat }}
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass {{ vault_vrrp_pass }}
    }

    virtual_ipaddress {
        {{ haproxy_vip }}/{{ haproxy_vip_cidr }} dev {{ haproxy_interficie }}
    }

    track_script {
        comprovar_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"
}

I el play que garanteix el desplegament d'un en un:

# balancejadors.yml
- name: Configurar els balancejadors
  hosts: balancejadors
  become: true
  # serial: 1 -> un node cada vegada. Si el primer falla, el segon NO es toca.
  serial: 1
  # max_fail_percentage: 0 -> avorta davant de la primera fallada
  max_fail_percentage: 0
  # ORDRE DELIBERAT: primer el BACKUP. Si alguna cosa surt malament, el
  # MASTER continua amb la VIP i el servei no s interromp.
  order: reverse_inventory
  roles:
    - balanceador

Les decisions que eviten deixar el servei sense IP:

Decisió Contra què protegeix
serial: 1 + assert d'un sol host Configurar tots dos alhora i quedar-se sense cap node amb VIP
order: reverse_inventory (BACKUP primer) Si el rol falla, el MASTER conserva la VIP i el servei continua
max_fail_percentage: 0 Que una fallada a lb2 no impedeixi detectar-se abans de tocar lb1
validate: haproxy -c -f %s Instal·lar una configuració que impedeix arrencar HAProxy
meta: flush_handlers abans de verificar Verificar l'estat anterior als canvis
state: reloaded a HAProxy restart tallaria totes les connexions en curs
assert de coherència de la VIP Detectar que tots dos o cap tenen la VIP
uri contra la VIP amb run_once Comprovar el servei d'extrem a extrem, no només les peces
net.ipv4.ip_nonlocal_bind HAProxy no arrenca al BACKUP si no pot escoltar a la VIP absent
no_log: true a certificat i VRRP La clau privada i la contrasenya sortirien a la sortida
Script 0755 propietat de root enable_script_security rebutja scripts escrivibles per l'usuari que els executa

I el que cal afegir al manual d'operació: aquest rol es prova primer a l'entorn de proves amb dos balancejadors virtuals, i en producció s'executa amb --check --diff abans que de debò. La virtual_router_id ha de ser única al segment de xarxa: dos clústers VRRP amb el mateix ID a la mateixa xarxa interfereixen entre si, i és una fallada desconcertant de diagnosticar.

Solució 3

Necessita Tramontana Reserves alta disponibilitat? Per a: Marta Vidal · De: Operacions de sistemes · 18 d'agost de 2026

Resposta breu: alta disponibilitat completa, no. Un primer pas, sí, i amb una relació cost-benefici molt favorable. Recomano dos servidors d'aplicació amb un balancejador, que captura la major part del benefici per una fracció del cost.


Primer, una dada que ens falta. No tenim un objectiu de disponibilitat acordat ni la mesurem. Estimo que som al voltant del 99,5 %, unes 44 hores de caiguda a l'any, comptant reinicis per actualització, desplegaments i algun incident. És una estimació, no una dada, i el primer que proposo és començar a mesurar-ho — costa molt poc i sense aquell número qualsevol inversió és una intuïció.

El que ja hem aconseguit sense gastar res. La disponibilitat depèn de dues coses: amb quina freqüència falla el sistema i quant triga a recuperar-se. En les últimes setmanes hem reduït dràsticament la segona: reconstruir el servidor del tot ha passat de vuit hores a cinquanta minuts, automatitzant-ne la configuració. Això ja ha millorat la disponibilitat sense comprar ni una sola màquina, i és la mena de millora que cal esgotar abans de duplicar infraestructura.

Què costaria estar caiguts. Amb unes 500 reserves mensuals de 313,70 € de mitjana, l'ingrés mitjà és d'uns 218 € per hora. Aquell número cal matisar-lo en tres sentits:

  • Les reserves es concentren a la tarda i en cap de setmana. Una caiguda de matinada un dimarts pot no costar res; la mateixa un divendres a la tarda costa bastant més que la mitjana.
  • Molts clients que troben el web caigut ho tornen a intentar. El cost real està probablement entre el 20 % i el 50 % de la xifra bruta.
  • Hi ha un cost d'imatge, no lineal i difícil de quantificar, sobretot si coincideix amb una campanya.

Amb una estimació prudent (35 %):

Disponibilitat Caiguda a l'any Cost estimat
99,5 % (situació actual) 44 h ~3.340 €
99,8 % 17,5 h ~1.330 €
99,9 % 8,8 h ~670 €

Les tres opcions, amb el seu cost real.

Què inclou Inversió Disponibilitat Estalvi anual
A. No fer res L'actual 0 ~99,5 % —
B. Dos servidors + balancejador 2 màquines, 1 setmana de feina Baixa ~99,8 % ~2.000 €
C. Arquitectura completa 4 màquines, 6 setmanes, +4 h/mes Alta ~99,9 % ~2.670 €

L'important està a la comparació entre B i C: l'opció C costa diverses vegades més que la B i aporta tot just 670 € addicionals d'estalvi anual. La raó és que els primers nous són barats i els següents s'encareixen molt — i, sobretot, que la majoria de les nostres interrupcions no vénen de fallades de maquinari, sinó de desplegaments, reinicis i manteniment, i això es resol sencer amb l'opció B.

Què ens donaria exactament l'opció B:

  • Desplegaments sense interrupció. Avui cada desplegament són uns segons de servei tallat. Amb dos servidors s'actualitza l'un mentre l'altre atén, i el client no nota res.
  • Manteniment sense finestra. Reiniciar per un nucli nou deixa de ser una interrupció programada.
  • Tolerància a la fallada d'un servidor d'aplicació. El sistema ho detecta en menys de deu segons i deixa d'enviar-li trànsit tot sol.
  • Capacitat per a pics, com a benefici secundari.

Què NO ens donaria, i cal dir-ho amb claredat:

  • Si cau la base de dades, el servei cau. La promoció de la rèplica seria manual, de 5 a 15 minuts.
  • Si cau el balancejador, el servei cau. És l'opció C.
  • Si cau la infraestructura del proveïdor, tot cau.
  • I res d'això no protegeix contra un esborrat accidental o una corrupció de dades: allò es replicaria a l'instant a tots els nodes. Per a això hi ha les còpies, que continuarien sent igual de necessàries.

Hi ha una condició prèvia que no és tècnica de sistemes. Tramontana Reserves guarda avui dues coses dins de cada servidor: les sessions de les persones que naveguen, i les fotos que es pugen. Amb dos servidors, qui iniciés sessió en un apareixeria desconnectat en ser atès per l'altre, i una foto pujada a l'un no es veuria des de l'altre. Abans de muntar res cal feina de desenvolupament —externalitzar les sessions i desar els fitxers en un magatzem compartit—, unes tres o quatre setmanes. És la part del projecte que cal planificar amb el Luis, i sense ella l'opció B no funciona.

I un advertiment professional. La redundància afegeix peces, i cada peça pot fallar de maneres noves. Un sistema mal muntat és menys fiable que un de simple ben mantingut. Per això no recomano l'opció C: la complexitat que afegeix —replicació, promoció, risc que dos servidors escriguin alhora dades incompatibles— exigeix un nivell de vigilància i de proves que avui no podem sostenir amb la plantilla actual. És millor una arquitectura senzilla que entenem i provem que una de sofisticada que ningú no ha assajat.

Recomanació, en tres passos.

  1. Aquest mes, cost zero: fixar un objectiu formal de disponibilitat —proposo 99,8 %— i començar a mesurar-la. Amb això podrem decidir amb dades en lloc d'estimacions.
  2. D'aquí a dos o tres mesos: desenvolupament de les dues condicions prèvies, i desplegament de l'opció B. Començaria amb un sol balancejador, assumint que és un punt únic de fallada però molt menys propens a caure que l'aplicació completa.
  3. Revisar d'aquí a un any, o abans si canvia alguna cosa: si el volum de reserves creix de manera significativa, si una caiguda ens costa un client important, o si mesurem una disponibilitat pitjor de l'estimada.

I una última cosa. A part de tot l'anterior, vull programar un simulacre semestral: provocar les fallades expressament, en horari laboral, i comprovar que la recuperació funciona. Un sistema de suport que no s'ha provat mai és un sistema del qual no sabem res, i això val tant per a l'arquitectura futura com per al procediment de reconstrucció que ja tenim. El primer, al setembre.

Conclusió

Saps de què es parla quan es parla d'alta disponibilitat, i amb la precisió que el tema exigeix. Els nous tenen números concrets —99,99 % és menys d'un minut de caiguda a la setmana—, la disponibilitat es descompon en MTBF i MTTR, i reduir l'MTTR sol ser molt més barat que augmentar l'MTBF: és exactament el que vas fer a 07-06 en baixar la reconstrucció de vuit hores a cinquanta minuts, sense comprar res. Saps que l'escalat vertical no aporta disponibilitat, que un node passiu que no es fa servir mai és un node del qual no saps res, i sobretot que l'estat és el problema difícil: les sessions, els fitxers pujats i les tasques programades són el que impedeix duplicar un servidor sense més.

Has muntat un balancejador amb HAProxy que reparteix per leastconn, sondeja la salut cada tres segons amb fall 3 i rise 2 asimètrics, torna els nodes amb slowstart per no tombar-los de nou, i permet drenar connexions per desplegar sense tallar — resolent el que va quedar pendent des de 04-07. Li has donat redundància amb keepalived i una IP virtual flotant, amb el track_script que evita la fallada més ximple d'aquesta arquitectura: un balancejador amb keepalived viu i HAProxy mort retenint la IP. I coneixes els dos conceptes sense els quals tot això és teatre: el cervell dividit, que amb dos nodes no es pot evitar perquè no hi ha quòrum possible, i la delimitació, sense la qual un clúster no és fiable per molt que arrenqui.

I has fet l'anàlisi que la Marta feia tres lliçons que demanava, amb la conclusió que no sempre agrada: l'arquitectura completa no compensa. Dos servidors d'aplicació amb un balancejador capturen la major part del benefici per una fracció del cost, perquè la majoria de les interrupcions no vénen de fallades de maquinari sinó de desplegaments i manteniment. Que la resposta professional a «necessitem alta disponibilitat?» pugui ser «una part, i per aquest ordre» és tan important com saber muntar-la. Juntament amb l'advertiment que tanca l'assumpte: la redundància mal feta empitjora la fiabilitat, i un sistema senzill que s'entén i es prova val més que un de sofisticat que ningú no ha assajat.


Amb això es tanca el Mòdul 7, i amb ell la part més avançada del curs. Has après a mirar dins del sistema: la cadena d'arrencada completa i com intervenir-hi quan es trenca, el diagnòstic amb strace, perf i eBPF quan els comptadors no basten, i l'ajust del nucli amb la regla de no tocar el que no s'ha mesurat. I has après a multiplicar-lo: virtualització amb KVM i l'entorn de proves que el curs trobava a faltar des del Mòdul 4, contenidors entesos com el que són —processos aïllats, no màquines petites—, automatització amb Ansible que va convertir la configuració en codi i l'RTO en cinquanta minuts, i per fi la redundància que treu a srv-tramontana la seva condició de punt únic de fallada.

Al Mòdul 8: Projectes Pràctics tot allò s'aplica a construccions completes, de principi a fi. Muntaràs un servidor web amb Nginx com a servidor intermediari invers i TLS davant de l'aplicació — i allà es tanca per fi el xifratge en trànsit que va quedar preparat a 06-05 i que fa des d'aleshores que espera. Configuraràs un servidor de base de dades PostgreSQL en condicions, amb el seu ajust, les seves còpies i la seva replicació. Construiràs un servidor de mitjans domèstic, que és el projecte on comprovar que tot l'après s'aplica igual fora de la feina. Aixecaràs un servidor VPN amb WireGuard per accedir a la xarxa interna sense exposar res. Desplegaràs un clúster de Kubernetes, que és on els contenidors de 07-05 i l'orquestració es troben. I tancaràs amb la posada en producció: la llista de verificació completa, la monitoració amb Prometheus i Grafana que es va esmentar a 05-07, i el procediment d'operació diària d'un sistema que ja no és un laboratori. Actualitza la instantània de la teva VM i ens hi veiem.

Curs de Linux: De Principiant a Administrador de Sistemes

Mòdul 1: Introducció a Linux

Mòdul 2: Comandes Bàsiques de Linux

Mòdul 3: Habilitats Avançades en la Línia de Comandes

Mòdul 4: Scripting en Shell

Mòdul 5: Administració del Sistema

Mòdul 6: Xarxes i Seguretat

Mòdul 7: Temes Avançats

Mòdul 8: Projectes Pràctics

© Copyright 2026. Tots els drets reservats