Tot el mòdul s'ha ocupat fins ara de créixer: més pods, de la mida correcta, sobre més nodes, i abans que arribi el trànsit. Però hi ha una pregunta que no hem fet: què passa quan alguna cosa es trenca, o quan algú ha de tocar el clúster?

El proper dimarts toca actualitzar la versió de Kubernetes dels nodes de Rutas Norte. Algú executarà kubectl drain sobre el primer node per buidar-lo. Si en aquest node hi ha tres de les quatre rèpliques d'api-reserves —perquè el planificador les hi va posar i ningú li va dir que no ho fes—, aquest drenatge s'endurà el 75 % de la capacitat de l'API en un instant, i la quarta rèplica rebrà quatre vegades la seva càrrega habitual fins a caure també.

I hi ha una variant pitjor: que el node no el dreni ningú, sinó que s'apagui sol per una fallada de maquinari a la zona de disponibilitat B, a les tres de la matinada, sense previ avís.

Els dos escenaris produeixen el mateix dany, però Kubernetes només et pot protegir d'un d'ells. Entendre per què, i què fer amb l'altre, és el contingut d'aquesta lliçó.

Veurem la distinció entre interrupcions voluntàries i involuntàries, el PodDisruptionBudget i l'API de desallotjament que el respecta, els PDB impossibles que bloquegen el manteniment per sempre, la distribució topològica amb topologySpreadConstraints davant de la podAntiAffinity de 06-05, què significa l'alta disponibilitat a les altres capes del sistema, el procediment complet de manteniment d'un node, i una prova de caos per comprovar que tot funciona de debò.

Contingut

  1. Interrupcions voluntàries i involuntàries
  2. El PodDisruptionBudget: què és i què protegeix
  3. minAvailable davant de maxUnavailable
  4. L'API de desallotjament i kubectl drain
  5. Demostració: un drenatge que es queda esperant
  6. El PDB impossible que bloqueja el manteniment
  7. unhealthyPodEvictionPolicy
  8. Els PDB de Rutas Norte, component a component
  9. topologySpreadConstraints a fons
  10. topologySpreadConstraints davant de podAntiAffinity
  11. Repartir Rutas Norte entre zones i nodes
  12. Alta disponibilitat a les altres capes
  13. El procediment de manteniment d'un node
  14. Interacció amb el Cluster Autoscaler i els desplegaments
  15. Una prova de caos senzilla
  16. Errors comuns i consells
  17. Exercicis
  18. Conclusió

  1. Interrupcions voluntàries i involuntàries

Kubernetes distingeix formalment dues classes d'interrupció, i la distinció no és acadèmica: determina quins mecanismes de protecció existeixen.

Interrupcions involuntàries

Passen sense que ningú les hagi demanat. Són fallades:

Causa Exemple a Rutas Norte
Fallada de maquinari del node La font d'alimentació del servidor que allotja tres rèpliques
Caiguda de la zona de disponibilitat Un tall elèctric al centre de dades de la zona B
El kernel mata un procés per falta de memòria OOMKilled d'api-reserves (mòdul 3)
Desallotjament del kubelet per pressió de recursos El node es queda sense disc i desallotja pods BestEffort
Partició de xarxa El node perd connectivitat amb el pla de control
Esborrat accidental d'una màquina virtual Algú s'equivoca a la consola del proveïdor

Kubernetes no pot impedir cap d'aquestes. Quan el maquinari falla, falla. L'única cosa que es pot fer és limitar el dany: si les rèpliques estan repartides, la caiguda d'un node se'n porta una fracció; si estan amuntegades, se'n porta tot.

La protecció contra interrupcions involuntàries és arquitectònica: rèpliques suficients, repartides entre dominis de fallada independents. Això és l'apartat 9 endavant.

Interrupcions voluntàries

Les provoca algú deliberadament, a través de l'API:

Causa Exemple a Rutas Norte
Drenar un node per manteniment Actualitzar el kernel o la versió de Kubernetes
Reducció del clúster El Cluster Autoscaler retira un node infrautilitzat (09-03)
Redesplegar una aplicació Una actualització progressiva d'api-reserves (02-04)
El VPA ajusta recursos L'actualitzador desallotja un pod per recrear-lo (09-02)
Esborrar un pod a mà kubectl delete pod
Reprogramació per un descheduler Una eina que reequilibra pods entre nodes

Aquí sí que pot intervenir Kubernetes, perquè aquestes accions passen pel servidor d'API i poden ser avaluades abans d'executar-se. El mecanisme s'anomena PodDisruptionBudget, i és l'objecte que diu: «pots fer manteniment, però no a costa de deixar-me sense servei».

flowchart TD
    subgraph INVOL["Interrupcions INVOLUNTARIES"]
        I1[Fallada de maquinari]
        I2[Caiguda de zona]
        I3[OOMKilled]
        I4[Particio de xarxa]
    end

    subgraph VOL["Interrupcions VOLUNTARIES"]
        V1[kubectl drain]
        V2[Reduccio del Cluster Autoscaler]
        V3[Actualitzacio progressiva]
        V4[Desallotjament del VPA]
    end

    INVOL -->|Kubernetes NO les pot impedir| ARQ["Proteccio ARQUITECTONICA:<br/>repliques repartides entre<br/>dominis de fallada<br/>topologySpreadConstraints"]
    VOL -->|Passen per l'API de desallotjament| PDB["Proteccio per POLITICA:<br/>PodDisruptionBudget"]

    style INVOL fill:#fdd,stroke:#c00
    style VOL fill:#ffd,stroke:#c90
    style ARQ fill:#cde,stroke:#369
    style PDB fill:#cfc,stroke:#393

Les dues proteccions són complementàries i calen totes dues. Un PDB perfecte no et salva que caigui la zona on tens totes les rèpliques. I una distribució topològica perfecta no impedeix que un kubectl drain descurat s'endugui tres rèpliques de cop.

Un matís important sobre el maxUnavailable del Deployment

Convé aclarir una confusió freqüent. El Deployment té el seu propi control d'interrupció durant les actualitzacions:

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 2

Això governa només el desplegament progressiu (02-04). No protegeix d'un kubectl drain, ni de la reducció del Cluster Autoscaler, ni d'un desallotjament del VPA. Són mecanismes diferents amb àmbits diferents, i necessites tots dos.

strategy.rollingUpdate.maxUnavailable PodDisruptionBudget
Àmbit Només actualitzacions del Deployment Qualsevol desallotjament per l'API
L'aplica El controlador de Deployments El servidor d'API, en la petició de desallotjament
Protegeix de Que la mateixa actualització deixi sense servei drain, CA, VPA, descheduler
Es defineix a El Deployment Un objecte PodDisruptionBudget a part

  1. El PodDisruptionBudget: què és i què protegeix

Un PodDisruptionBudget (PDB) és un objecte que declara: «d'aquest conjunt de pods, sempre n'hi ha d'haver almenys N de disponibles» (o «com a molt N de no disponibles»).

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
spec:
  maxUnavailable: 25%
  selector:
    matchLabels:
      app: api-reserves

Tres camps i res més:

  • selector: quins pods cobreix. Igual que el selector d'un Service o un Deployment (02-07).
  • minAvailable o maxUnavailable: la restricció. Mútuament excloents: un dels dos, mai tots dos.
  • (Opcional) unhealthyPodEvictionPolicy: apartat 7.

Què fa exactament

Quan algú demana desallotjar un pod a través de l'API de desallotjament, el servidor d'API:

  1. Busca els PDB el selector dels quals coincideixi amb aquest pod.
  2. Per a cadascun, calcula quants pods estan actualment disponibles (Ready).
  3. Comprova si desallotjar aquest pod violaria la restricció.
  4. Si la violaria, rebutja la petició amb un error HTTP 429 (TooManyRequests).
  5. Si no, l'accepta i el pod s'elimina.

És una comprovació en el moment de la petició, no una reserva. El client que rep el 429 normalment reintenta, i així el desallotjament espera fins que sigui segur.

Què NO fa un PDB

Aquesta llista és tan important com l'anterior:

El PDB no Explicació
Impedeix que un node caigui És una interrupció involuntària: ningú demana permís
Impedeix kubectl delete pod L'esborrat directe no passa per l'API de desallotjament
Garanteix que hi hagi N rèpliques Això ho fa el Deployment/ReplicaSet
Crea pods nous No és un controlador de rèpliques
Protegeix d'un OOMKilled Involuntari
Protegeix que el kubelet desallotgi per pressió de recursos Involuntari

El segon punt sorprèn molta gent i mereix èmfasi: kubectl delete pod ignora els PDB per complet. L'esborrat directe és una operació diferent del desallotjament. Si vols respectar els PDB des de la línia d'ordres, fes servir kubectl drain (que sí que fa servir l'API de desallotjament) o la mateixa API:

# Aixo IGNORA el PDB
kubectl delete pod api-reserves-7c9d4f8b6d-2mk8p -n rutas-norte-pro

# Aixo RESPECTA el PDB
kubectl drain <node> --pod-selector=app=api-reserves

  1. minAvailable davant de maxUnavailable

Els dos expressen el mateix des de costats oposats, però es comporten de manera molt diferent quan el nombre de rèpliques canvia, que és exactament el que passa quan hi ha un HPA.

minAvailable

«Sempre hi ha d'haver almenys N pods disponibles.»

spec:
  minAvailable: 3
  selector:
    matchLabels:
      app: api-reserves

Amb 4 rèpliques: se'n pot desallotjar 1 (en queden 3). Amb 10 rèpliques: se'n poden desallotjar 7.

maxUnavailable

«Com a molt hi pot haver N pods no disponibles.»

spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: api-reserves

Amb 4 rèpliques: se'n pot desallotjar 1. Amb 10 rèpliques: també només 1.

La diferència crítica amb els percentatges

Aquí hi ha el detall que gairebé ningú coneix i que produeix comportaments sorprenents: l'arrodoniment és diferent en cada cas.

Camp Arrodoniment del percentatge Motiu
minAvailable: 50% Cap amunt Més conservador: garanteix més pods
maxUnavailable: 50% Cap avall Més conservador: permet menys desallotjaments

En tots dos casos l'arrodoniment afavoreix la disponibilitat. Vegem-ho amb números:

Amb 5 rèpliques:

PDB Càlcul Resultat Desallotjaments permesos
minAvailable: 50% sostre(5 × 0,5) = 3 Mínim 3 disponibles 2
maxUnavailable: 50% terra(5 × 0,5) = 2 Màxim 2 no disponibles 2

Aquí coincideixen. Però amb 7 rèpliques:

PDB Càlcul Resultat Desallotjaments permesos
minAvailable: 50% sostre(7 × 0,5) = 4 Mínim 4 disponibles 3
maxUnavailable: 50% terra(7 × 0,5) = 3 Màxim 3 no disponibles 3

Coincideixen un altre cop. Amb 3 rèpliques:

PDB Càlcul Resultat Desallotjaments permesos
minAvailable: 50% sostre(3 × 0,5) = 2 Mínim 2 disponibles 1
maxUnavailable: 50% terra(3 × 0,5) = 1 Màxim 1 no disponible 1

Per al 50 % el resultat és equivalent. La diferència apareix amb altres percentatges. Amb 10 rèpliques i un 20 %:

PDB Càlcul Resultat Desallotjaments permesos
minAvailable: 20% sostre(10 × 0,2) = 2 Mínim 2 disponibles 8
maxUnavailable: 20% terra(10 × 0,2) = 2 Màxim 2 no disponibles 2

Radicalment diferent. minAvailable: 20% permet desallotjar el 80 % dels pods; maxUnavailable: 20% permet desallotjar el 20 %.

Regla mental: minAvailable parla del que queda; maxUnavailable parla del que se'n va.

Quin fer servir: la regla de l'HPA

I ara la raó per la qual això importa tant en aquest mòdul. Amb un HPA, el nombre de rèpliques canvia sol. Un PDB en número absolut que era raonable amb 30 rèpliques es torna impossible amb 4.

Recordem el cas de l'exercici 2 de 09-03:

Durant el pont: 30 repliques. Algu posa minAvailable: 25.
  Desallotjaments permesos: 5. Raonable.

Despres del pont: l'HPA baixa a 4 repliques. El PDB continua dient minAvailable: 25.
  Pods disponibles: 4. Minim exigit: 25.
  JA s'esta per sota del minim.
  Desallotjaments permesos: ZERO. Per sempre.

El node que allotgi qualsevol replica d'api-reserves queda ANCORAT.
El Cluster Autoscaler no el pot retirar. El manteniment es bloqueja.

Per això:

Situació Recomanació
Càrrega amb HPA o KEDA maxUnavailable en percentatge
Nombre de rèpliques fix i conegut Qualsevol; minAvailable és més explícit
Càrrega amb estat i quòrum (etcd, bases de dades) maxUnavailable: 1 sempre
Una sola rèplica Cap dels dos funciona bé (apartat 6)

maxUnavailable: 25% és l'elecció per defecte assenyada per a una càrrega amb HPA. S'adapta sola: amb 4 rèpliques permet desallotjar-ne 1; amb 30, en permet desallotjar 7. La proporció de servei protegida és constant.

Una nota sobre el denominador: el percentatge es calcula sobre el nombre de rèpliques desitjades pel controlador (el spec.replicas del Deployment), no sobre els pods Ready en aquell moment. Això importa quan hi ha pods arrencant: durant un escalat de l'HPA de 4 a 12, el denominador ja és 12 encara que només 5 estiguin llestos.

  1. L'API de desallotjament i kubectl drain

El PDB només funciona si qui vol treure un pod demana permís. Aquest mecanisme és l'API de desallotjament (Eviction API).

Com funciona

És un subrecurs del pod:

POST /api/v1/namespaces/rutas-norte-pro/pods/api-reserves-7c9d4f8b6d-2mk8p/eviction

Amb aquest cos:

{
  "apiVersion": "policy/v1",
  "kind": "Eviction",
  "metadata": {
    "name": "api-reserves-7c9d4f8b6d-2mk8p",
    "namespace": "rutas-norte-pro"
  }
}

Respostes possibles:

Codi Significat
200 OK Desallotjament acceptat; el pod s'elimina amb el seu període de gràcia
429 TooManyRequests Un PDB ho impedeix. Reintentar més tard
500 Internal Server Error Configuració incoherent (un PDB mal format)

El 429 inclou un missatge explicatiu:

{
  "kind": "Status",
  "status": "Failure",
  "message": "Cannot evict pod as it would violate the pod's disruption budget.",
  "reason": "TooManyRequests",
  "details": {
    "causes": [
      {
        "reason": "DisruptionBudget",
        "message": "The disruption budget api-reserves needs 3 healthy pods and has 3 currently"
      }
    ]
  }
}

Aquest missatge —«necessita 3 pods sans i en té 3 actualment»— és el que veuràs a la pràctica i el que cal saber llegir.

Qui fa servir l'API de desallotjament

Client Respecta els PDB?
kubectl drain
Cluster Autoscaler (09-03)
Actualitzador del VPA (09-02)
Descheduler
Karpenter (consolidació)
kubectl delete pod No. Esborrat directe
Controlador de Deployments (actualització progressiva) No. Fa servir maxUnavailable propi
El kubelet desallotjant per pressió de recursos No. És involuntari
Un node que s'apaga No. És involuntari

Les quatre primeres files són les que importen: tots els mecanismes automàtics que hem anat afegint en aquest mòdul respecten els PDB. És la garantia que l'autoescalat no s'endugui per davant la disponibilitat.

El camp disruptionsAllowed

El PDB publica al seu estat quants desallotjaments permet en aquest moment:

kubectl get pdb -n rutas-norte-pro
NAME                   MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
api-reserves           N/A             25%               1                     12d
botiga-web             N/A             1                 1                     12d
postgres-reserves      1               N/A               0                     12d
worker-notificacions   N/A             50%               3                     12d

ALLOWED DISRUPTIONS és el número més útil de tota la lliçó. Si és 0, cap pod d'aquest conjunt no es pot desallotjar ara mateix, i qualsevol drenatge que els afecti es quedarà esperant.

Un 0 pot ser correcte (postgres-reserves amb una rèplica i minAvailable: 1) o símptoma d'un problema (un PDB impossible, o pods que no estan Ready).

Vista detallada:

kubectl describe pdb api-reserves -n rutas-norte-pro
Name:             api-reserves
Namespace:        rutas-norte-pro
Max unavailable:  25%
Selector:         app=api-reserves
Status:
    Allowed disruptions:  2
    Current:              8
    Desired:              6
    Total:                8
Camp Significat
Current Pods Ready ara mateix
Desired Mínim que exigeix el PDB (calculat)
Total Pods que cobreix el selector
Allowed disruptions Current - Desired

  1. Demostració: un drenatge que es queda esperant

Res no ensenya millor que veure-ho. Preparem l'escenari a minikube.

Preparació

minikube start -p rutas-norte --nodes=3 --cpus=2 --memory=4096

kubectl create namespace rutas-norte-dev

# Un Deployment amb 3 repliques
kubectl -n rutas-norte-dev create deployment demo-pdb \
  --image=registry.k8s.io/pause:3.9 --replicas=3

kubectl -n rutas-norte-dev set resources deployment/demo-pdb \
  --requests=cpu=100m,memory=64Mi

Un PDB deliberadament restrictiu:

# /tmp/pdb-demo.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: demo-pdb
  namespace: rutas-norte-dev
spec:
  # Amb 3 repliques, exigir-ne 3 de disponibles significa ZERO desallotjaments permesos.
  minAvailable: 3
  selector:
    matchLabels:
      app: demo-pdb
kubectl apply -f /tmp/pdb-demo.yaml
kubectl get pdb -n rutas-norte-dev
NAME       MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
demo-pdb   3               N/A               0                     8s

ALLOWED DISRUPTIONS: 0. Ja sabem què passarà.

El drenatge

kubectl get pods -n rutas-norte-dev -o wide
NAME                        READY   STATUS    NODE
demo-pdb-6c9f8d7b5-2mkjp    1/1     Running   rutas-norte
demo-pdb-6c9f8d7b5-7wqzx    1/1     Running   rutas-norte-m02
demo-pdb-6c9f8d7b5-9nvtr    1/1     Running   rutas-norte-m03
kubectl drain rutas-norte-m02 --ignore-daemonsets --delete-emptydir-data
node/rutas-norte-m02 cordoned
evicting pod rutas-norte-dev/demo-pdb-6c9f8d7b5-7wqzx
error when evicting pods/"demo-pdb-6c9f8d7b5-7wqzx" -n "rutas-norte-dev"
(will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod rutas-norte-dev/demo-pdb-6c9f8d7b5-7wqzx
error when evicting pods/"demo-pdb-6c9f8d7b5-7wqzx" -n "rutas-norte-dev"
(will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod rutas-norte-dev/demo-pdb-6c9f8d7b5-7wqzx
error when evicting pods/"demo-pdb-6c9f8d7b5-7wqzx" -n "rutas-norte-dev"
(will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
...

El drenatge es queda en bucle infinit, reintentant cada 5 segons.

Observacions importants:

  1. El node JA està acordonat (cordoned). Encara que el drenatge no avanci, el node no accepta pods nous. És el primer que fa drain.
  2. El missatge és explícit: Cannot evict pod as it would violate the pod's disruption budget.
  3. drain no es rendeix. Reintenta indefinidament fins que l'hi permetin o fins que l'interrompis.

Aquest és exactament el comportament desitjat: el PDB ha impedit que el manteniment trenqui el servei. Però també és exactament el comportament que bloqueja el manteniment per sempre si el PDB està malament, i aquesta és la lliçó de l'apartat 6.

Desbloquejar-ho

Tres formes:

# Opcio A: mes repliques. Amb 4 repliques i minAvailable: 3, es permet 1 desallotjament.
kubectl -n rutas-norte-dev scale deployment demo-pdb --replicas=4

Tan bon punt la quarta rèplica estigui Ready, el drenatge que estava reintentant avança sol. És molt satisfactori de veure.

# Opcio B: corregir el PDB
kubectl -n rutas-norte-dev patch pdb demo-pdb --type merge \
  -p '{"spec":{"minAvailable":null,"maxUnavailable":"25%"}}'

Compte: minAvailable i maxUnavailable són mútuament excloents, així que cal anul·lar-ne un en posar l'altre.

# Opcio C (ULTIM RECURS, perillos): forcar el drenatge
kubectl drain rutas-norte-m02 --ignore-daemonsets --delete-emptydir-data \
  --disable-eviction

--disable-eviction fa servir esborrat directe en lloc de l'API de desallotjament, saltant-se els PDB. És l'opció d'emergència, i cal saber que pot deixar el servei sense cap rèplica disponible. Fes-la servir només quan entenguis exactament què caurà i hagis decidit que és acceptable.

Neteja

kubectl uncordon rutas-norte-m02
kubectl delete -f /tmp/pdb-demo.yaml
kubectl delete deployment demo-pdb -n rutas-norte-dev

No oblidis mai l'uncordon. Un node acordonat que ningú descordona és capacitat pagada i inutilitzable, i el símptoma (pods Pending mentre hi ha nodes aparentment lliures) desconcerta moltíssim.

  1. El PDB impossible que bloqueja el manteniment

Formalitzem el problema, perquè és la trampa més perillosa d'aquesta lliçó.

La definició

Un PDB impossible és aquell la restricció del qual no es pot satisfer desallotjant ni un sol pod. El seu ALLOWED DISRUPTIONS és 0 de forma permanent.

Els dos casos:

Cas 1: minAvailable igual (o major) al nombre de rèpliques.

spec:
  replicas: 3        # al Deployment
---
spec:
  minAvailable: 3    # al PDB

Desallotjar qualsevol pod deixaria 2 disponibles, per sota del mínim de 3. Zero desallotjaments, sempre.

I la variant insidiosa: minAvailable: 25 sobre un Deployment amb HPA que ha baixat a 4 rèpliques. El PDB era raonable quan es va escriure; l'HPA l'ha tornat impossible sense que ningú el toqués.

Cas 2: una sola rèplica amb qualsevol PDB restrictiu.

spec:
  replicas: 1
---
spec:
  minAvailable: 1        # Equivalent a maxUnavailable: 0

Amb una rèplica, desallotjar-la deixa zero disponibles. Zero desallotjaments, sempre.

Les conseqüències

Conseqüència Detall
El manteniment de nodes es bloqueja kubectl drain reintenta per sempre
El Cluster Autoscaler no pot reduir Nodes infrautilitzats que ningú retira, pagant-se (09-03)
El VPA no pot aplicar recomanacions L'actualitzador falla en desallotjar (09-02)
Les actualitzacions del clúster s'encallen Un proveïdor gestionat pot avortar l'actualització
El diagnòstic és difícil El símptoma (nodes zombis) és lluny de la causa (un PDB)

Aquest últim punt és el que fa perillós el problema: ningú relaciona un node que no es retira amb un PDB escrit fa tres mesos. Els logs del Cluster Autoscaler ho diuen (pdb-blocked), però cal saber mirar-hi.

Detectar-los

# Tots els PDB del cluster amb els seus desallotjaments permesos
kubectl get pdb --all-namespaces
NAMESPACE          NAME                   MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS
rutas-norte-pro    api-reserves           N/A             25%               2
rutas-norte-pro    botiga-web             N/A             1                 1
rutas-norte-pro    postgres-reserves      1               N/A               0
rutas-norte-pro    redis-cache            1               N/A               0
rutas-norte-pro    worker-notificacions   N/A             50%               4
monitoratge        prometheus             1               N/A               0

Els tres zeros cal analitzar-los un per un:

  • postgres-reserves: una rèplica primària. ALLOWED DISRUPTIONS: 0 és deliberat i correcte: no volem que cap mecanisme automàtic mogui la base de dades. El preu és que el seu node requereix intervenció manual per al manteniment.
  • redis-cache: mateix cas, encara que aquí és més discutible: perdre la memòria cau uns minuts és molest però no catastròfic. Hi hauria arguments per permetre el desallotjament.
  • prometheus: si Prometheus té una sola rèplica (habitual), és el mateix patró.

Una alerta útil per detectar els impossibles no deliberats:

# PDB que porten mes d'una hora sense permetre cap desallotjament
kube_poddisruptionbudget_status_pod_disruptions_allowed == 0

Amb una anotació als PDB deliberats per excloure'ls de l'alerta.

Què fer amb les càrregues d'una sola rèplica

És una pregunta que cal respondre de forma explícita, i hi ha tres respostes legítimes:

Opció A: no posar PDB. Sense PDB, el desallotjament es permet sempre. El servei tindrà un tall durant el manteniment (els segons que trigui a arrencar en un altre node), però el manteniment flueix. És l'opció correcta per a serveis no crítics.

Opció B: posar maxUnavailable: 1 (equivalent a minAvailable: 0). Permet sempre el desallotjament. Sembla inútil, però documenta explícitament que s'hi ha pensat i s'accepta la interrupció. Millor que l'absència de PDB, que és ambigua.

Opció C: posar dues rèpliques. Si el servei de debò no es pot interrompre, la solució no és un PDB: és tenir més d'una rèplica. Un PDB no crea disponibilitat, només la protegeix.

Per a postgres-reserves, la resposta correcta no és cap de les tres: és un operador (06-07) que gestioni un clúster de PostgreSQL amb primari i rèpliques, i que sàpiga promocionar una rèplica quan el primari s'hagi de moure. Ho desenvolupem a l'apartat 12.

  1. unhealthyPodEvictionPolicy

Un camp relativament recent que resol un problema molt concret i molt molest.

El problema

Imagina que api-reserves té 6 rèpliques i un PDB de maxUnavailable: 1. Un desplegament defectuós fa que 4 de les 6 rèpliques no passin la sonda de readiness (07-01): estan Running però no Ready.

Repliques totals: 6
Repliques Ready: 2
Repliques no Ready: 4

PDB: maxUnavailable: 1 -> minim 5 disponibles.
Disponibles actuals: 2. Ja estem PER SOTA del minim.
ALLOWED DISRUPTIONS: 0.

Ara vols desallotjar un dels pods trencats —precisament perquè està trencat i vols que es recreï en un altre node— i el PDB no t'ho deixa. El PDB està protegint pods que no estan servint trànsit.

És una situació circular i absurda: el sistema està trencat, i el mecanisme de protecció impedeix arreglar-lo.

La solució

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
spec:
  maxUnavailable: 25%
  selector:
    matchLabels:
      app: api-reserves
  # Permet desallotjar SEMPRE els pods que no estan Ready, sense consumir
  # pressupost d'interrupcio.
  unhealthyPodEvictionPolicy: AlwaysAllow

Els dos valors:

Valor Comportament
IfHealthyBudget (defecte) Els pods no sans només es poden desallotjar si el PDB té pressupost disponible
AlwaysAllow Els pods no sans (que no estan Ready) es poden desallotjar sempre

Amb AlwaysAllow, el raonament és directe: un pod que no està Ready no està servint trànsit, així que desallotjar-lo no redueix la disponibilitat. Al contrari: permet que es recreï, possiblement en un node sa.

Quan fer-lo servir

Situació Recomanació
Aplicacions sense estat (api-reserves, botiga-web, workers) AlwaysAllow. Sempre
Aplicacions amb quòrum (etcd, Kafka, Zookeeper) IfHealthyBudget (el defecte). Un pod no Ready pot estar recuperant-se i continuar comptant per al quòrum
Bases de dades amb rèpliques Depèn del sistema; generalment IfHealthyBudget

La distinció és important: en un sistema de quòrum, un membre que no respon a la sonda pot continuar participant en el consens. Desallotjar-lo alegrement pot perdre el quòrum. En una aplicació sense estat, un pod no Ready és pur llast.

Per a Rutas Norte, AlwaysAllow en tots els components sense estat. Evita l'encallament circular i no té contrapartides.

  1. Els PDB de Rutas Norte, component a component

Escrivim els manifests definitius, amb la justificació de cada decisió.

api-reserves

# k8s/entorns/pro/pdb-api-reserves.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
  labels:
    app: api-reserves
    app.kubernetes.io/part-of: rutas-norte
spec:
  # PERCENTATGE, no numero absolut. api-reserves te KEDA/HPA (09-01, 09-04)
  # i oscil·la entre 4 i 30 repliques al llarg del dia. Un minAvailable
  # absolut quedaria obsolet tan bon punt l'HPA es mogues, i amb la vall
  # nocturna es convertiria en un PDB IMPOSSIBLE que bloquejaria el
  # manteniment i la reduccio de nodes.
  #
  # Amb 25 %:
  #    4 repliques -> permet desallotjar-ne 1  (terra(4 x 0,25) = 1)
  #   12 repliques -> permet desallotjar-ne 3
  #   30 repliques -> permet desallotjar-ne 7
  # La PROPORCIO de servei protegida es constant: sempre el 75 %.
  maxUnavailable: 25%

  selector:
    matchLabels:
      app: api-reserves

  # Els pods que no passen la readiness no estan servint transit: desallotjar-los
  # no redueix la disponibilitat i evita l'encallament circular d'un desplegament
  # defectuos que impedeix la seva propia reparacio.
  unhealthyPodEvictionPolicy: AlwaysAllow

botiga-web

# k8s/entorns/pro/pdb-botiga-web.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: botiga-web
  namespace: rutas-norte-pro
  labels:
    app: botiga-web
    app.kubernetes.io/part-of: rutas-norte
spec:
  # 34 %, mes permissiu que api-reserves. Rao: botiga-web es nginx servint
  # estatic. Arrenca en ~1 segon i no te estat ni cache que escalfar,
  # aixi que una replica desallotjada torna gairebe a l'instant en un altre node.
  # Permetre mes desallotjaments simultanis accelera el manteniment sense risc real.
  #
  # Amb 3 repliques -> permet desallotjar-ne 1  (terra(3 x 0,34) = 1)
  # Amb 15 repliques -> permet desallotjar-ne 5
  maxUnavailable: 34%

  selector:
    matchLabels:
      app: botiga-web

  unhealthyPodEvictionPolicy: AlwaysAllow

worker-notificacions

# k8s/entorns/pro/pdb-worker-notificacions.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: worker-notificacions
  namespace: rutas-norte-pro
  labels:
    app: worker-notificacions
    app.kubernetes.io/part-of: rutas-norte
spec:
  # 50 %, el mes permissiu de la plataforma. Justificacio:
  #
  # 1. NINGU ESPERA. Un correu de confirmacio enviat 30 segons mes tard
  #    no te cap consequencia per a l'usuari.
  # 2. LA CUA ES L'AMORTIDOR. Si es desallotja la meitat dels workers, la
  #    feina no es perd: s'acumula a la cua i es processa despres.
  #    El missatge en vol es reentrega automaticament.
  # 3. KEDA REACCIONA. Si la cua creix perque hi ha menys workers, KEDA (09-04)
  #    escalara per compensar.
  #
  # Amb escalat a ZERO (minReplicaCount: 0) hi ha un matis: quan hi ha 0
  # repliques, el PDB no cobreix cap pod i ALLOWED DISRUPTIONS sera 0, pero
  # aixo es irrellevant perque no hi ha res a desallotjar.
  maxUnavailable: 50%

  selector:
    matchLabels:
      app: worker-notificacions

  unhealthyPodEvictionPolicy: AlwaysAllow

redis-cache

# k8s/entorns/pro/pdb-redis-cache.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: redis-cache
  namespace: rutas-norte-pro
  labels:
    app: redis-cache
    app.kubernetes.io/part-of: rutas-norte
  annotations:
    # Anotacio per excloure aquest PDB de l'alerta de "PDB impossible":
    # el seu ALLOWED DISRUPTIONS es 0 de forma DELIBERADA.
    rutasnorte.example/pdb-bloqueig-intencionat: >
      redis-cache es un StatefulSet d'una replica. Desallotjar-lo buida la cache
      i descarrega tota la carrega de consultes de disponibilitat sobre
      postgres-reserves. Requereix intervencio manual i finestra de manteniment.
spec:
  # minAvailable: 1 amb UNA replica = ZERO desallotjaments permesos.
  # Es DELIBERAT. Consequencies assumides:
  #   - El Cluster Autoscaler no retirara el node que allotgi redis-cache.
  #   - kubectl drain sobre aquest node es quedara esperant.
  #   - El manteniment d'aquest node requereix decisio humana explicita.
  #
  # Es un compromis ACCEPTAT: preferim que el manteniment sigui manual a
  # que un mecanisme automatic buidi la cache en el pitjor moment possible.
  minAvailable: 1

  selector:
    matchLabels:
      app: redis-cache

  # IfHealthyBudget (el defecte). No hi posem AlwaysAllow: si redis-cache no
  # esta Ready, pot estar carregant dades o recuperant-se. Desallotjar-lo
  # empitjoraria les coses.
  unhealthyPodEvictionPolicy: IfHealthyBudget

postgres-reserves

# k8s/entorns/pro/pdb-postgres-reserves.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: postgres-reserves
  namespace: rutas-norte-pro
  labels:
    app: postgres-reserves
    app.kubernetes.io/part-of: rutas-norte
  annotations:
    rutasnorte.example/pdb-bloqueig-intencionat: >
      postgres-reserves es la base de dades primaria de la plataforma amb una
      unica replica. CAP mecanisme automatic no l'ha de moure. El manteniment
      del node que l'allotgi requereix una finestra planificada, amb parada
      controlada de l'aplicacio i copia de seguretat previa. Veure 05-06.
      La solucio definitiva es un operador de PostgreSQL: veure 06-07.
spec:
  # ZERO desallotjaments. Es la proteccio mes forta possible, i es intencionada.
  #
  # Un desallotjament automatic de postgres-reserves significaria:
  #   - Tall de TOTES les connexions d'api-reserves.
  #   - Perdua de la cache de pagines de PostgreSQL (minuts de latencia dolenta).
  #   - Risc d'inconsistencia si hi ha transaccions en vol.
  #   - La plataforma sencera caiguda durant l'arrencada.
  #
  # Guarda dades personals de clients: qualsevol risc es inacceptable.
  minAvailable: 1

  selector:
    matchLabels:
      app: postgres-reserves

  unhealthyPodEvictionPolicy: IfHealthyBudget

Taula resum

Component PDB Desallotjaments amb rèpliques mín. unhealthyPodEvictionPolicy Justificació
api-reserves maxUnavailable: 25% 1 de 4 AlwaysAllow Amb HPA: percentatge obligatori. 75 % de servei garantit
botiga-web maxUnavailable: 34% 1 de 3 AlwaysAllow Arrenca en 1 s; més permissiu accelera el manteniment
worker-notificacions maxUnavailable: 50% 1 de 2 AlwaysAllow La cua amortitza; ningú espera
redis-cache minAvailable: 1 0 IfHealthyBudget Una rèplica; buidar la memòria cau és car. Bloqueig deliberat
postgres-reserves minAvailable: 1 0 IfHealthyBudget Base de dades primària. Bloqueig deliberat
informes-ocupacio Cap És un CronJob. Es protegeix amb safe-to-evict: false (09-03)

Fixa't en l'última fila: els Jobs no porten PDB. No té sentit: un Job que es desallotja es reinicia i perd la feina. La seva protecció és l'anotació cluster-autoscaler.kubernetes.io/safe-to-evict: "false" que vam veure a 09-03.

  1. topologySpreadConstraints a fons

Els PDB protegeixen de les interrupcions voluntàries. Per a les involuntàries, la protecció és repartir les rèpliques entre dominis de fallada independents. I aquí entra el camp que vam esmentar a 06-05 i vam prometre desenvolupar aquí.

El problema que resol

Sense cap restricció, el planificador de Kubernetes col·loca els pods on caben, optimitzant l'ús de recursos. Res no li impedeix posar les quatre rèpliques d'api-reserves al mateix node, si és on hi ha forat.

node-1 (zona A):  api-reserves x4, botiga-web x2
node-2 (zona A):  postgres-reserves
node-3 (zona B):  redis-cache, worker x2
node-4 (zona B):  (gairebe buit)

Cau node-1  ->  ZERO repliques d'api-reserves. Plataforma caiguda.
Cau la zona A -> ZERO repliques d'api-reserves I la base de dades.

topologySpreadConstraints diu al planificador: «reparteix aquests pods de forma equilibrada entre aquests dominis».

Els camps

spec:
  template:
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: api-reserves
          minDomains: 3
          nodeAffinityPolicy: Honor
          nodeTaintsPolicy: Honor
          matchLabelKeys:
            - pod-template-hash

topologyKey — l'etiqueta de node que defineix el domini. Els valors estàndard:

Etiqueta Domini Protegeix de
kubernetes.io/hostname Un node Fallada d'un servidor
topology.kubernetes.io/zone Una zona de disponibilitat Fallada d'un centre de dades
topology.kubernetes.io/region Una regió geogràfica Catàstrofe regional
Etiquetes pròpies (rack, bastidor) El que tu defineixis Fallada d'un bastidor o un commutador

maxSkew — la diferència màxima permesa entre el domini amb més pods i el que en té menys. És el cor del mecanisme.

biaix = (pods al domini amb MES) - (pods al domini amb MENYS)

Amb maxSkew: 1, la diferència entre qualsevol parell de dominis no pot superar 1. És el repartiment més equilibrat possible.

whenUnsatisfiable — què fer si no es pot complir:

Valor Comportament Quan fer-lo servir
DoNotSchedule El pod es queda Pending abans que violar la restricció Distribució obligatòria
ScheduleAnyway Es col·loca igualment, però el planificador prefereix complir-la Distribució preferida

Aquesta és la decisió més conseqüent del bloc, i la desenvolupem a l'apartat 11.

labelSelector — quins pods es compten per calcular el biaix. Normalment els del mateix Deployment.

minDomains — el nombre mínim de dominis que han d'existir. Sense ell, hi ha un forat subtil:

Sense minDomains, amb 3 repliques i nomes 1 zona amb nodes disponibles:
  Zona A: 3 pods. Zona B: no hi ha nodes. Zona C: no hi ha nodes.
  Dominis EXISTENTS: 1
  Biaix: 3 - 3 = 0  ->  la restriccio ES COMPLEIX.
  Resultat: les 3 repliques a la mateixa zona. NO es el que volies.

Amb minDomains: 3:
  El planificador considera que hi ha 3 dominis, dos d'ells amb 0 pods.
  Biaix: 3 - 0 = 3 > maxSkew 1  ->  NO es compleix.
  Amb DoNotSchedule: els pods queden Pending fins que hi hagi nodes en altres zones.

minDomains només té efecte amb whenUnsatisfiable: DoNotSchedule. És la garantia que el repartiment entre zones és real i no una il·lusió.

nodeAffinityPolicy i nodeTaintsPolicy — si tenir en compte les afinitats i taints en calcular els dominis:

Valor Comportament
Honor (defecte) Només compta els nodes on el pod podria anar (respectant afinitat i taints)
Ignore Compta tots els nodes

El valor per defecte Honor és gairebé sempre el correcte. Si api-reserves té afinitat cap al grup de nodes generals, no volem que el node de dades (amb el seu taint) compti com un domini buit que mai es podrà omplir.

matchLabelKeys — el camp més subtil i el que resol un problema real durant els desplegaments.

matchLabelKeys i el problema de les actualitzacions

Durant una actualització progressiva (02-04) coexisteixen pods de la versió vella i de la nova. Sense matchLabelKeys, el càlcul del biaix els compta tots junts:

Actualitzacio d'api-reserves de v1.14 a v1.15. 6 repliques.

Estat intermedi:
  zona A: 2 pods v1.14 + 1 pod v1.15 = 3 pods
  zona B: 2 pods v1.14 = 2 pods
  zona C: 1 pod v1.14 = 1 pod

Biaix calculat sobre TOTS: 3 - 1 = 2 > maxSkew 1
-> El planificador NO pot col·locar mes pods v1.15 a la zona A.
-> L'actualitzacio s'encalla o es distribueix malament.

Amb matchLabelKeys: ["pod-template-hash"], el planificador compta només els pods de la mateixa versió (el pod-template-hash és una etiqueta que el controlador de Deployments afegeix automàticament i que identifica el ReplicaSet):

Amb matchLabelKeys: ["pod-template-hash"]:
  Per col·locar un pod v1.15, nomes compten els pods v1.15:
    zona A: 1, zona B: 0, zona C: 0
  Biaix: 1 - 0 = 1 <= maxSkew 1  ->  es pot col·locar a B o C.

La nova versio es distribueix correctament pel seu compte.

Regla pràctica: inclou sempre matchLabelKeys: ["pod-template-hash"] a les restriccions d'un Deployment. Evita encallaments durant els desplegaments i no té contrapartida.

El càlcul del biaix, pas a pas

Practiquem amb un cas concret. Tres zones, maxSkew: 1, 7 rèpliques d'api-reserves.

Col·locacio de la replica 1:
  A: 0, B: 0, C: 0. Qualsevol zona val. -> A
  Estat: A:1, B:0, C:0

Col·locacio de la replica 2:
  Si va a A: A:2, B:0, C:0. Biaix = 2-0 = 2 > 1. NO PERMES.
  Si va a B: A:1, B:1, C:0. Biaix = 1-0 = 1 <= 1. PERMES.
  -> B (o C)
  Estat: A:1, B:1, C:0

Col·locacio de la replica 3:
  Nomes C mante el biaix en 1 o menys.  -> C
  Estat: A:1, B:1, C:1  (biaix 0)

Repliques 4, 5, 6: es reparteixen una per zona.
  Estat: A:2, B:2, C:2  (biaix 0)

Col·locacio de la replica 7:
  Qualsevol zona: el biaix passaria a 1. Permes a les tres.
  -> A (arbitrari)
  Estat FINAL: A:3, B:2, C:2  (biaix 1)

Resultat: 3-2-2. El repartiment més equilibrat possible amb 7 rèpliques en 3 zones.

I ara la comprovació de disponibilitat:

Cau la zona A: queden 4 de 7 repliques (57 %).
Cau la zona B: queden 5 de 7 repliques (71 %).

Sense topologySpreadConstraints, en el pitjor cas les 7 podrien ser en una zona:
Cau aquesta zona: queden 0 de 7 (0 %). Plataforma caiguda.

  1. topologySpreadConstraints davant de podAntiAffinity

A 06-05 vam veure l'antiafinitat entre pods per separar rèpliques. Ara tenim dues eines que semblen fer el mateix. No són equivalents, i convé saber quan fer servir cadascuna.

L'antiafinitat, recordada

affinity:
  podAntiAffinity:
    # Versio DURA: no es pot violar
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels:
            app: api-reserves
        topologyKey: kubernetes.io/hostname

Significa: «no col·loquis aquest pod en un node on ja hi hagi un altre pod amb app: api-reserves».

És una restricció binària: o hi ha un pod del mateix tipus al domini, o no n'hi ha. No hi ha noció d'equilibri.

La diferència fonamental

podAntiAffinity (requerida) topologySpreadConstraints
Semàntica «Com a molt un per domini» «Reparteix equitativament entre dominis»
Granularitat Binària: hi ha o no hi ha Numèrica: controla la diferència
Amb més rèpliques que dominis Els sobrants queden Pending per sempre Es reparteixen equilibradament
Cost computacional Alt: O(n²) en clústers grans Baix
Versió «preferida» preferredDuringScheduling amb weight whenUnsatisfiable: ScheduleAnyway
Multinivell Difícil d'expressar Natural: diverses restriccions alhora
Reequilibrat després d'una fallada No n'hi ha Tampoc, però el repartiment inicial és millor

El cas que ho decideix

api-reserves escala de 4 a 30 rèpliques i el clúster té 9 nodes.

Amb podAntiAffinity requerida sobre hostname:

Regla: com a molt 1 replica d'api-reserves per node.
Nodes: 9.
Repliques maximes col·locables: 9.

L'HPA demana 30 repliques.
Se'n col·loquen 9. Les altres 21 queden en Pending PER SEMPRE.

El Cluster Autoscaler veu pods pendents i arrenca nodes... i amb cada node
nou hi cap UNA replica mes. Per a 30 repliques calarien 30 NODES.
Cost: absurd.

Amb topologySpreadConstraints:

Regla: maxSkew 1 entre nodes.
Nodes: 9. Repliques: 30.

Repartiment: 30 / 9 = 3,33
Resultat: 3 nodes amb 4 repliques, 6 nodes amb 3 repliques.
Biaix: 4 - 3 = 1. Compleix.

Les 30 repliques es col·loquen. Cap Pending.

Aquesta és la raó per la qual topologySpreadConstraints és l'eina correcta per a càrregues amb autoescalat. L'antiafinitat requerida i l'HPA són incompatibles a la pràctica.

Quan continua sent millor l'antiafinitat

Cas Per què
Components amb poques rèpliques fixes i separació estricta Etcd, un pla de control: 3 rèpliques, una per node, sense excepció
Antiafinitat entre aplicacions diferents «api-reserves no ha de compartir node amb informes-ocupacio»: això topologySpread no ho expressa
Regles de coubicació (podAffinity) «Posa aquest pod a prop de la memòria cau»: no té equivalent a topologySpread

Aquest segon cas mereix un exemple, perquè és un ús legítim i freqüent:

# api-reserves NO ha de compartir node amb el CronJob d'informes, que consumeix
# 4 nuclis de cop i estrangularia l'API durant 40 minuts.
affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels:
            app: informes-ocupacio       # UNA ALTRA aplicacio, no la mateixa
        topologyKey: kubernetes.io/hostname

La combinació recomanada

Per a api-reserves a Rutas Norte fem servir les dues, cadascuna per al seu:

spec:
  template:
    spec:
      # 1. Repartiment equilibrat entre zones i nodes (disponibilitat)
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: api-reserves
          matchLabelKeys: ["pod-template-hash"]
        - maxSkew: 2
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: api-reserves
          matchLabelKeys: ["pod-template-hash"]

      # 2. Antiafinitat amb UNA ALTRA aplicacio (aillament de recursos)
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: informes-ocupacio
              topologyKey: kubernetes.io/hostname

  1. Repartir Rutas Norte entre zones i nodes

Apliquem-ho a la plataforma, amb els càlculs de biaix.

api-reserves: dos nivells de distribució

# k8s/base/deployment-api-reserves.yaml (fragment)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
spec:
  # Sense replicas: ho governa KEDA/HPA (09-01, 09-04)
  selector:
    matchLabels:
      app: api-reserves
  template:
    metadata:
      labels:
        app: api-reserves
        app.kubernetes.io/part-of: rutas-norte
    spec:
      topologySpreadConstraints:
        # NIVELL 1: entre ZONES DE DISPONIBILITAT. Restriccio DURA.
        #
        # Aquest es el domini de fallada gran: si cau una zona sencera, cal
        # garantir que sobrevisquin repliques a les altres dues. Es una
        # garantia que NO estem disposats a negociar, aixi que DoNotSchedule:
        # preferim un pod Pending a un repartiment desequilibrat.
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: api-reserves
          # 3 zones sempre, encara que una no tingui nodes ara mateix. Sense aixo,
          # si el Cluster Autoscaler nomes te nodes en 2 zones, el repartiment
          # "equilibrat" entre aquestes 2 compliria la restriccio i perdriem
          # la garantia de les 3 zones.
          minDomains: 3
          matchLabelKeys: ["pod-template-hash"]

        # NIVELL 2: entre NODES. Restriccio TOVA.
        #
        # Repartir entre nodes es desitjable pero NO al preu de deixar pods
        # Pending. Durant el pont de maig, amb 30 repliques i el Cluster
        # Autoscaler arrencant nodes, un DoNotSchedule aqui bloquejaria
        # l'escalat justament quan mes falta fa.
        #
        # maxSkew 2 (no 1): amb 30 repliques i 9 nodes, exigir maxSkew 1 seria
        # innecessariament rigid. Un 4-3-3-4-3-3-4-3-3 es perfectament sa.
        - maxSkew: 2
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: api-reserves
          matchLabelKeys: ["pod-template-hash"]

      containers:
        - name: api
          image: registry.rutasnorte.example/api-reserves:1.14.2
          resources:
            requests: {cpu: 412m, memory: 800Mi}
            limits: {cpu: "1", memory: 1600Mi}

L'asimetria entre els dos nivells és la decisió clau, i mereix explicar-se bé:

Nivell whenUnsatisfiable Raonament
Zona DoNotSchedule La caiguda d'una zona és un esdeveniment catastròfic i plausible. Un pod Pending és preferible a perdre la garantia
Node ScheduleAnyway La caiguda d'un node és freqüent però d'impacte limitat. Bloquejar l'escalat seria pitjor que un repartiment imperfecte

El càlcul amb 4 rèpliques (vall)

3 zones, maxSkew 1, minDomains 3.

Repartiment: 4 / 3 = 1,33
Resultat: A:2, B:1, C:1. Biaix = 2-1 = 1. Compleix.

Caiguda de la zona A: queden 2 de 4 (50 %).
Caiguda de la zona B o C: queden 3 de 4 (75 %).

Amb maxUnavailable: 25% al PDB, 4 repliques permeten 1 desallotjament.
La caiguda d'una zona (2 repliques) es INVOLUNTARIA: el PDB no aplica.
Sobreviuen 2 repliques -> el servei continua, degradat.
KEDA/HPA detectara la carrega per replica duplicada i escalara.

El càlcul amb 30 rèpliques (pic del pont)

Nivell ZONA (maxSkew 1, DoNotSchedule):
  30 / 3 = 10 exactes.
  Resultat: A:10, B:10, C:10. Biaix 0. Perfecte.

Nivell NODE (maxSkew 2, ScheduleAnyway), amb 9 nodes (3 per zona):
  Dins de cada zona, 10 repliques entre 3 nodes: 4-3-3.
  Biaix global entre nodes: 4 - 3 = 1 <= 2. Compleix.

  Repartiment final:
    zona A: node-a1:4, node-a2:3, node-a3:3
    zona B: node-b1:4, node-b2:3, node-b3:3
    zona C: node-c1:4, node-c2:3, node-c3:3

Caiguda d'un NODE: es perden 3 o 4 repliques de 30 (10-13 %). Imperceptible.
Caiguda d'una ZONA: es perden 10 de 30 (33 %). El servei aguanta amb 20.

botiga-web

      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: botiga-web
          minDomains: 3
          matchLabelKeys: ["pod-template-hash"]
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          # maxSkew 1 i ScheduleAnyway: amb nomes 3-15 repliques, un repartiment
          # d'1 per node es assolible i desitjable. Pero continuem sense
          # bloquejar l'escalat.
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: botiga-web
          matchLabelKeys: ["pod-template-hash"]

Amb 3 rèpliques i minDomains: 3, el repartiment és exactament una per zona. És el mínim acceptable per a la porta d'entrada de la plataforma.

postgres-reserves: el cas especial

Amb una sola rèplica, topologySpreadConstraints no aporta res: no hi ha res a repartir. El que sí que importa és on viu:

# k8s/base/statefulset-postgres-reserves.yaml (fragment)
spec:
  template:
    spec:
      # Viu al grup de nodes de dades (09-03), amb el seu taint i la seva afinitat.
      tolerations:
        - key: rol
          operator: Equal
          value: dades
          effect: NoSchedule
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: rol
                    operator: In
                    values: ["dades"]
        # NO compartir node amb redis-cache: si cau aquest node, no volem
        # perdre la base de dades I la cache alhora, perque l'arrencada de
        # la base de dades amb la cache buida es el pitjor escenari possible.
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: redis-cache
              topologyKey: kubernetes.io/hostname

I una consideració crucial que connecta amb el mòdul 5: el volum persistent de postgres-reserves està ancorat a una zona. Un PV de disc de bloc d'un proveïdor de núvol només és accessible des de la seva zona. Això significa que:

  • El pod només es pot planificar en nodes d'aquesta zona.
  • Si cau la zona, no es pot recuperar en una altra zona sense restaurar d'una còpia de seguretat (05-06).

Això no és un problema de Kubernetes, és una propietat de l'emmagatzematge de blocs. I és la raó fonamental per la qual l'alta disponibilitat d'una base de dades no es resol amb objectes de Kubernetes (apartat 12).

  1. Alta disponibilitat a les altres capes

Fins ara hem parlat de les aplicacions. Però l'alta disponibilitat és una propietat del sistema complet, i hi ha tres capes més que convé revisar.

El pla de control i el quòrum d'etcd

Recordem 01-02: el pla de control el formen el servidor d'API, el planificador, el gestor de controladors i etcd, la base de dades que guarda tot l'estat del clúster.

etcd fa servir l'algoritme de consens Raft, que necessita majoria estricta per operar. D'aquí surt la regla del nombre senar:

Membres Quòrum (majoria) Fallades tolerades
1 1 0
2 2 0
3 2 1
4 3 1
5 3 2
6 4 2
7 4 3

Fixa't que 4 membres toleren les mateixes fallades que 3, i 6 les mateixes que 5. Un nombre parell afegeix cost i latència de consens sense afegir tolerància. D'aquí que la configuració estàndard sigui 3 o 5 membres, mai parell.

Què passa en perdre el quòrum:

Cluster de 3 membres d'etcd. En cauen 2.
Quorum necessari: 2. Membres vius: 1. NO HI HA QUORUM.

Consequencies:
  - etcd passa a NOMES LECTURA. No accepta escriptures.
  - El servidor d'API no pot persistir canvis.
  - kubectl apply falla. No es poden crear, modificar ni esborrar objectes.
  - L'HPA no pot canviar repliques.
  - El Cluster Autoscaler no pot fer res.

PERO, i aixo es l'important:
  - Els pods que JA estaven corrent CONTINUEN CORRENT.
  - Els kubelets continuen executant els seus contenidors.
  - Els Services continuen enrutant (kube-proxy te el seu estat local).
  - LA PLATAFORMA CONTINUA ATENENT TRANSIT.

El pla de control caigut no tomba les aplicacions en marxa. És una propietat de disseny molt valuosa de Kubernetes: el pla de dades sobreviu al pla de control. El que es perd és la capacitat de canviar coses: escalar, desplegar, recuperar-se d'una fallada de pod.

Distribució recomanada del pla de control:

3 nodes de pla de control, un per zona de disponibilitat:
  zona A: control-1 (etcd, api-server, scheduler, controller-manager)
  zona B: control-2
  zona C: control-3

Caiguda d'una zona: queden 2 de 3. Quorum = 2. ES MANTE.

A Kubernetes gestionat (10-06) això ho fa el proveïdor i no ho veus. És un dels arguments més forts a favor d'un clúster gestionat: l'alta disponibilitat del pla de control és difícil de fer bé i no aporta valor diferencial.

El controlador d'Ingress

El controlador d'Ingress (04-04) és la porta d'entrada de tot el trànsit extern. Si cau, la plataforma és inabastable encara que tots els pods estiguin sans.

# k8s/entorns/pro/deployment-ingress-nginx.yaml (fragment)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  replicas: 4              # Mai 1. Mai 2 al mateix domini de fallada.
  template:
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app.kubernetes.io/name: ingress-nginx
          minDomains: 3
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule    # Aqui SI que es dur
          labelSelector:
            matchLabels:
              app.kubernetes.io/name: ingress-nginx
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
spec:
  minAvailable: 2          # Numero absolut: les repliques de l'Ingress son FIXES
  selector:
    matchLabels:
      app.kubernetes.io/name: ingress-nginx
  unhealthyPodEvictionPolicy: AlwaysAllow

Dues decisions que s'aparten de l'anterior:

  1. DoNotSchedule també entre nodes. L'Ingress no té HPA (les seves rèpliques són fixes), així que no hi ha risc de bloquejar un escalat. I dues rèpliques de l'Ingress al mateix node són dues rèpliques que es perden juntes.
  2. minAvailable: 2 en números absoluts. Amb rèpliques fixes, el número absolut és més explícit i més fàcil de raonar que un percentatge.

CoreDNS

CoreDNS (04-03) resol tots els noms del clúster. Si falla, api-reserves no troba postgres-reserves, worker-notificacions no troba RabbitMQ, i tot cau amb errors de DNS que despisten moltíssim.

És un component que gairebé ningú revisa i que és un punt únic de fallada silenciosa.

# Fragment del Deployment de CoreDNS a kube-system
spec:
  replicas: 3
  template:
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              k8s-app: kube-dns
      priorityClassName: system-cluster-critical

I el seu PDB:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: coredns
  namespace: kube-system
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      k8s-app: kube-dns
  unhealthyPodEvictionPolicy: AlwaysAllow

Un consell addicional molt rendible: NodeLocal DNSCache, un DaemonSet que posa una memòria cau de DNS a cada node. Redueix la latència de resolució, descarrega CoreDNS, i fa que una fallada temporal de CoreDNS no afecti les consultes cachejades. Hi tornarem a 09-06 en parlar del ndots: 5.

postgres-reserves: la disponibilitat la dona l'operador

I arribem al cas més important, el que resumeix la lliçó de tot el mòdul sobre les càrregues amb estat.

Kubernetes no pot fer que PostgreSQL sigui d'alta disponibilitat. El que Kubernetes ofereix:

  • Reiniciar el pod si el procés mor.
  • Reprogramar-lo en un altre node si el node falla (si el volum és accessible).
  • Un PDB que impedeix desallotjaments automàtics.

El que Kubernetes no ofereix:

Capacitat necessària Per què Kubernetes no la dona
Replicació de dades entre instàncies És un protocol de PostgreSQL, no de Kubernetes
Promoció automàtica d'una rèplica a primària Requereix conèixer l'estat de la replicació
Redirigir les escriptures a la nova primària Requereix canviar el Service en el moment just
Evitar el «cervell dividit» (dues primàries) Requereix una lògica de consens específica
Garantir que no es perden transaccions confirmades Requereix entendre el WAL de PostgreSQL

Tot això ho aporta un operador (06-07): CloudNativePG, Zalando Postgres Operator, Crunchy PGO. Un operador de PostgreSQL:

flowchart TD
    OP[Operador de PostgreSQL<br/>controlador amb coneixement del domini]
    P[(postgres-primari<br/>zona A<br/>ESCRIPTURES)]
    R1[(postgres-replica-1<br/>zona B<br/>lectures)]
    R2[(postgres-replica-2<br/>zona C<br/>lectures)]
    SVCW[Service: postgres-rw<br/>apunta al PRIMARI]
    SVCR[Service: postgres-ro<br/>apunta a les REPLIQUES]

    OP -->|vigila salut i replicacio| P
    OP -->|vigila| R1
    OP -->|vigila| R2
    P -->|replicacio en streaming| R1
    P -->|replicacio en streaming| R2
    OP -.->|si el primari cau:<br/>PROMOCIONA i reapunta| SVCW
    SVCW --> P
    SVCR --> R1
    SVCR --> R2

    style OP fill:#cde,stroke:#369,stroke-width:2px
    style P fill:#cfc,stroke:#393

Amb un operador, el model canvia per complet:

# Exemple amb CloudNativePG
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgres-reserves
  namespace: rutas-norte-pro
spec:
  instances: 3                    # 1 primaria + 2 repliques
  primaryUpdateStrategy: unsupervised

  storage:
    size: 100Gi
    storageClass: rutasnorte-rapida    # De 05-04

  # L'operador reparteix les instancies entre zones automaticament
  affinity:
    enablePodAntiAffinity: true
    topologyKey: topology.kubernetes.io/zone
    podAntiAffinityType: required

  postgresql:
    parameters:
      max_connections: "200"
      shared_buffers: "1GB"

  monitoring:
    enablePodMonitor: true         # S'integra amb Prometheus (07-03)

I l'operador crea, manté i gestiona:

  • Tres StatefulSets amb els seus PVC en tres zones.
  • Els Services postgres-reserves-rw (primari) i postgres-reserves-ro (rèpliques).
  • La replicació en streaming entre instàncies.
  • La commutació automàtica: si el primari cau, promociona una rèplica i reapunta el Service -rw en uns segons.
  • Els seus propis PDB, correctament configurats per al seu model de quòrum.
  • Còpies de seguretat contínues al magatzem d'objectes.

La lliçó: per a les càrregues amb estat, l'alta disponibilitat es delega en un operador que entengui el sistema. Els PDB i topologySpreadConstraints són eines genèriques de Kubernetes; un operador aporta el coneixement específic del domini que cap eina genèrica no pot tenir.

Aplicar això a Rutas Norte és la feina pendent que s'aborda al mòdul 11 (11-02).

  1. El procediment de manteniment d'un node

Reunim-ho tot en el procediment operatiu complet. Això és el que s'executa el dimarts al matí.

El flux

flowchart TD
    A[1. Verificar l'estat previ<br/>PDB, repliques, alertes] --> B{Tots els PDB<br/>permeten desallotjaments?}
    B -->|No| C[Corregir els PDB impossibles<br/>o planificar intervencio manual]
    C --> A
    B -->|Si| D[2. cordon: deixar d'acceptar pods nous]
    D --> E[3. drain: desallotjar els pods existents<br/>respectant els PDB]
    E --> F{S'ha completat?}
    F -->|No, bloquejat| G[Diagnosticar: quin PDB ho impedeix?]
    G --> H[Escalar repliques o corregir]
    H --> E
    F -->|Si| I[4. Verificar: node buit<br/>i servei sa]
    I --> J[5. MANTENIMENT<br/>actualitzar kernel, kubelet, reiniciar]
    J --> K[6. Verificar que el node torna Ready]
    K --> L[7. uncordon: tornar a acceptar pods]
    L --> M[8. Verificar la distribucio<br/>i descordonar el seguent]

    style D fill:#ffd,stroke:#c90
    style E fill:#ffd,stroke:#c90
    style L fill:#cfc,stroke:#393

Pas 1: verificació prèvia

# Tots els PDB permeten almenys un desallotjament?
kubectl get pdb --all-namespaces
NAMESPACE          NAME                   MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS
rutas-norte-pro    api-reserves           N/A             25%               1
rutas-norte-pro    botiga-web             N/A             34%               1
rutas-norte-pro    postgres-reserves      1               N/A               0     <- ATENCIO
rutas-norte-pro    redis-cache            1               N/A               0     <- ATENCIO
rutas-norte-pro    worker-notificacions   N/A             50%               1
ingress-nginx      ingress-nginx          2               N/A               2
kube-system        coredns                N/A             1                 1

Els dos zeros són els deliberats de l'apartat 8. Cal saber en quins nodes són:

kubectl get pods -n rutas-norte-pro -o wide \
  -l 'app in (postgres-reserves,redis-cache)'
NAME                  READY   STATUS    NODE            ZONA
postgres-reserves-0   2/2     Running   node-dades-1    eu-west-1a
redis-cache-0         1/1     Running   node-dades-2    eu-west-1b

Aquests dos nodes requereixen un procediment diferent, amb finestra de manteniment planificada. Els altres es poden drenar sense més.

# Que hi ha al node que drenarem?
kubectl get pods --all-namespaces -o wide --field-selector spec.nodeName=node-a2
NAMESPACE         NAME                            READY   STATUS    AGE
rutas-norte-pro   api-reserves-7c9d4f8b6d-2mkjp   2/2     Running   4h
rutas-norte-pro   api-reserves-7c9d4f8b6d-9nvtr   2/2     Running   4h
rutas-norte-pro   botiga-web-6f7d9c4b58-4kjnx     1/1     Running   2d
kube-system       fluentd-x7kqp                   1/1     Running   12d   <- DaemonSet
kube-system       falco-2wxkp                     1/1     Running   12d   <- DaemonSet
kube-system       kube-proxy-mn2vp                1/1     Running   12d   <- DaemonSet

Tres pods d'aplicació i tres DaemonSets. Els DaemonSets no es drenen (un per node per definició).

# Hi ha alguna alerta activa? No es fa manteniment amb incidents oberts.
kubectl get events --all-namespaces --field-selector type=Warning \
  --sort-by=.lastTimestamp | tail -20

Pas 2: cordon

kubectl cordon node-a2
node/node-a2 cordoned

Què fa exactament: posa spec.unschedulable: true al node i li afegeix el taint node.kubernetes.io/unschedulable:NoSchedule.

kubectl get nodes
NAME      STATUS                     ROLES    AGE   VERSION
node-a1   Ready                      <none>   12d   v1.30.2
node-a2   Ready,SchedulingDisabled   <none>   12d   v1.30.2
node-a3   Ready                      <none>   12d   v1.30.2

cordon NO mou res. Els pods que ja hi són continuen corrent. Només impedeix que n'arribin de nous. És una operació segura i reversible a l'instant.

Pas 3: drain

kubectl drain node-a2 \
  --ignore-daemonsets \
  --delete-emptydir-data \
  --grace-period=60 \
  --timeout=300s

Les opcions, una a una:

Opció Què fa És necessària?
--ignore-daemonsets No intenta desallotjar pods de DaemonSet Sí, sempre. Sense ella drain falla
--delete-emptydir-data Accepta perdre el contingut dels emptyDir si hi ha pods amb emptyDir (memòria cau de nginx)
--grace-period=60 Segons perquè el pod acabi netament Recomanat: dona temps a l'apagada ordenada
--timeout=300s Abandona després de 5 minuts Molt recomanable. Evita esperar per sempre
--force Esborra també pods sense controlador Perillós: aquests pods no tornen
--disable-eviction Ignora els PDB fent servir esborrat directe Només emergències
--pod-selector Drena només els pods que coincideixin Útil per a drenatges parcials
--dry-run=client Mostra què faria sense fer-ho Excel·lent per verificar abans

Sortida esperada:

node/node-a2 already cordoned
Warning: ignoring DaemonSet-managed Pods: kube-system/fluentd-x7kqp,
         kube-system/falco-2wxkp, kube-system/kube-proxy-mn2vp
evicting pod rutas-norte-pro/api-reserves-7c9d4f8b6d-2mkjp
evicting pod rutas-norte-pro/botiga-web-6f7d9c4b58-4kjnx
evicting pod rutas-norte-pro/api-reserves-7c9d4f8b6d-9nvtr
error when evicting pods/"api-reserves-7c9d4f8b6d-9nvtr" -n "rutas-norte-pro"
(will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
pod/botiga-web-6f7d9c4b58-4kjnx evicted
pod/api-reserves-7c9d4f8b6d-2mkjp evicted
evicting pod rutas-norte-pro/api-reserves-7c9d4f8b6d-9nvtr
pod/api-reserves-7c9d4f8b6d-9nvtr evicted
node/node-a2 drained

Fixa't en el segon pod d'api-reserves: el primer intent va fallar amb el missatge del PDB, va esperar 5 segons, i en reintentar va tenir èxit. Què va passar en aquests 5 segons? El primer pod desallotjat es va recrear en un altre node i va passar a Ready, alliberant pressupost d'interrupció.

Aquest és el PDB funcionant exactament com ha de: no ha impedit el manteniment, l'ha serialitzat perquè mai hi hagi més d'un pod caigut alhora.

Pas 4: verificar

# El node ha d'estar buit de pods d'aplicacio
kubectl get pods --all-namespaces -o wide --field-selector spec.nodeName=node-a2
NAMESPACE     NAME                READY   STATUS    AGE
kube-system   fluentd-x7kqp       1/1     Running   12d
kube-system   falco-2wxkp         1/1     Running   12d
kube-system   kube-proxy-mn2vp    1/1     Running   12d

Només DaemonSets. Correcte.

# I el SERVEI ha d'estar sa: aixo es el que de debo importa
kubectl get pods -n rutas-norte-pro -l app=api-reserves
kubectl get pdb -n rutas-norte-pro

Comprovació a Grafana (07-04): latència p95 estable, taxa d'error a zero. Si el drenatge ha degradat el servei, no continuïs amb el següent node.

Pas 5: el manteniment

# Exemple: actualitzar el kubelet en un cluster amb kubeadm (10-02)
ssh node-a2

sudo apt-get update
sudo apt-get install -y kubeadm=1.30.3-1.1
sudo kubeadm upgrade node
sudo apt-get install -y kubelet=1.30.3-1.1 kubectl=1.30.3-1.1
sudo systemctl daemon-reload
sudo systemctl restart kubelet

exit

Pas 6: verificar el node

kubectl get node node-a2
NAME      STATUS                     ROLES    AGE   VERSION
node-a2   Ready,SchedulingDisabled   <none>   12d   v1.30.3

Ready amb la versió nova. Continua acordonat, que és el correcte.

# Comprovar que no hi ha condicions d'error
kubectl describe node node-a2 | grep -A10 Conditions

Pas 7: uncordon

kubectl uncordon node-a2
node/node-a2 uncordoned
NAME      STATUS   ROLES    AGE   VERSION
node-a2   Ready    <none>   12d   v1.30.3

No oblidis mai aquest pas. Un node que queda acordonat és capacitat pagada i invisible: el Cluster Autoscaler pot arrencar nodes nous mentre aquest està buit i disponible però rebutjant pods.

Un detall que sorprèn: uncordon no reequilibra els pods existents. El node està disponible, però els pods que es van moure no tornen. Kubernetes no reprograma pods en marxa. La redistribució passa de forma natural amb el següent escalat o desplegament.

Si vols forçar el reequilibrat, l'eina és el descheduler, un component que desallotja pods que violen les polítiques de distribució perquè es replanifiquin millor. Respecta els PDB, naturalment.

Pas 8: el següent node

# Verificar la distribucio abans de continuar
kubectl get pods -n rutas-norte-pro -l app=api-reserves -o wide \
  | awk '{print $7}' | sort | uniq -c
      2 node-a1
      1 node-a3
      2 node-b1
      1 node-b2

Després del drenatge, node-a2 està buit i els seus pods s'han repartit. La distribució continua sent raonable entre zones (3 a A, 3 a B... encara que falta C: caldria revisar si minDomains s'està complint).

Regla d'or: un node cada vegada, amb verificació entre cadascun. Drenar dos nodes en paral·lel pot violar els PDB de formes que no vas anticipar.

Què es trenca si no hi ha PDB

Val la pena fer l'exercici mental. Sense cap PDB:

kubectl drain node-a2 --ignore-daemonsets --delete-emptydir-data
node/node-a2 cordoned
evicting pod rutas-norte-pro/api-reserves-7c9d4f8b6d-2mkjp
evicting pod rutas-norte-pro/api-reserves-7c9d4f8b6d-9nvtr
evicting pod rutas-norte-pro/botiga-web-6f7d9c4b58-4kjnx
pod/api-reserves-7c9d4f8b6d-2mkjp evicted
pod/api-reserves-7c9d4f8b6d-9nvtr evicted
pod/botiga-web-6f7d9c4b58-4kjnx evicted
node/node-a2 drained

Instantani. Els tres pods desallotjats alhora. I en aquest instant:

api-reserves tenia 4 repliques: 2 a node-a2, 1 a node-a1, 1 a node-b1.
Despres del drenatge: 2 repliques vives, 2 arrencant (25-30 segons).

Durant 30 segons: LA MEITAT de la capacitat de l'API.
Cada replica viva rep el DOBLE de carrega.
Latencia p95: de 180 ms a 900 ms.
Algunes peticions: timeout.

I si haguessin estat 3 de les 4 repliques en aquest node: 25 % de capacitat.
La replica supervivent se satura i cau. CAIGUDA TOTAL.

El PDB converteix aquest desastre en un desallotjament serialitzat de 90 segons sense impacte perceptible. És la diferència entre un manteniment i un incident.

  1. Interacció amb el Cluster Autoscaler i els desplegaments

Dues interaccions que convé tenir presents.

Amb el Cluster Autoscaler (09-03)

El CA fa servir l'API de desallotjament, així que respecta els PDB. Conseqüències:

Conseqüència 1: un PDB impossible ancora el node permanentment. Ja ho vam veure a 09-03: els logs diuen pdb-blocked i el node no es retira mai. Amb postgres-reserves és intencionat; amb un PDB mal configurat, són diners llençats.

Conseqüència 2: la reducció és més lenta amb PDB. El CA ha de desallotjar els pods d'un en un, esperant que es recreïn. Un node amb 8 pods d'api-reserves i maxUnavailable: 25% amb 12 rèpliques totals triga uns quants minuts a buidar-se. És correcte i desitjable, però cal saber-ho per no pensar que el CA està trencat.

Conseqüència 3: cal revisar els PDB quan canvien els maxReplicas. Si puges maxReplicas d'api-reserves de 30 a 60, el maxUnavailable: 25% permetrà 15 desallotjaments simultanis al pic. És acceptable perdre 15 rèpliques alhora? Probablement sí, però és una decisió que mereix pensar-se.

I una interacció especialment subtil: el coixí de sobreaprovisionament no ha de portar PDB. Els pods de farciment de 09-03 hi són per ser desallotjats; un PDB els protegiria i trencaria tot el mecanisme. Si de cas, un PDB explícitament permissiu:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: farciment-capacitat
  namespace: rutas-norte-pro
spec:
  # 100 % d'indisponibilitat permesa: aquests pods hi son PER ser desallotjats.
  # Documenta la intencio millor que l'absencia de PDB.
  maxUnavailable: 100%
  selector:
    matchLabels:
      app: farciment-capacitat

Amb els desplegaments (02-04)

El controlador de Deployments no fa servir l'API de desallotjament durant una actualització progressiva: esborra pods directament i aplica el seu propi maxUnavailable. Els PDB no intervenen.

Això significa que necessites configurar els dos coherentment:

# Al Deployment: governa l'ACTUALITZACIO
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 25%     # Coherent amb el PDB
      maxSurge: 25%
---
# Al PDB: governa els DESALLOTJAMENTS (drain, CA, VPA)
spec:
  maxUnavailable: 25%

Si el Deployment permet el 50 % d'indisponibilitat i el PDB només el 25 %, tindràs una incoherència: durant un desplegament el servei baixarà al 50 %, però un drenatge només el podrà baixar al 75 %. No és un error, però és una inconsistència de criteri que convé resoldre.

Recomanació: fes servir el mateix valor en tots dos, perquè el nivell de servei garantit sigui el mateix passi el que passi.

I un escenari a evitar: un desplegament i un manteniment alhora. Durant una actualització, la meitat dels pods estan arrencant i no compten com a disponibles. Si a més drenes un node, el PDB bloquejarà el drenatge (correctament) i el manteniment s'encallarà. Mai facis manteniment durant un desplegament, i viceversa.

  1. Una prova de caos senzilla

Tot l'anterior és teoria fins que es comprova. Matarem un node i veurem què passa.

Preparació a minikube

minikube start -p rutas-norte --nodes=4 --cpus=2 --memory=4096

# Etiquetar els nodes amb zones ficticies
kubectl label node rutas-norte     topology.kubernetes.io/zone=zona-a
kubectl label node rutas-norte-m02 topology.kubernetes.io/zone=zona-a
kubectl label node rutas-norte-m03 topology.kubernetes.io/zone=zona-b
kubectl label node rutas-norte-m04 topology.kubernetes.io/zone=zona-b

Desplegament amb distribució i PDB:

# /tmp/caos-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: caos-api
  namespace: rutas-norte-dev
spec:
  replicas: 6
  selector:
    matchLabels:
      app: caos-api
  template:
    metadata:
      labels:
        app: caos-api
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: caos-api
          matchLabelKeys: ["pod-template-hash"]
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: caos-api
          matchLabelKeys: ["pod-template-hash"]
      containers:
        - name: web
          image: nginx:1.27-alpine
          resources:
            requests: {cpu: 50m, memory: 32Mi}
          readinessProbe:
            httpGet: {path: /, port: 80}
            initialDelaySeconds: 2
            periodSeconds: 3
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: caos-api
  namespace: rutas-norte-dev
spec:
  maxUnavailable: 25%
  selector:
    matchLabels:
      app: caos-api
  unhealthyPodEvictionPolicy: AlwaysAllow
---
apiVersion: v1
kind: Service
metadata:
  name: caos-api
  namespace: rutas-norte-dev
spec:
  selector:
    app: caos-api
  ports:
    - port: 80
kubectl apply -f /tmp/caos-demo.yaml
kubectl get pods -n rutas-norte-dev -o wide
NAME                        READY   STATUS    NODE              ZONA
caos-api-7d9c5f8b6b-2mkjp   1/1     Running   rutas-norte       zona-a
caos-api-7d9c5f8b6b-4nqzx   1/1     Running   rutas-norte-m02   zona-a
caos-api-7d9c5f8b6b-7wtrv   1/1     Running   rutas-norte-m02   zona-a
caos-api-7d9c5f8b6b-9hbcx   1/1     Running   rutas-norte-m03   zona-b
caos-api-7d9c5f8b6b-kp3ln   1/1     Running   rutas-norte-m04   zona-b
caos-api-7d9c5f8b6b-tv6ws   1/1     Running   rutas-norte-m03   zona-b

Distribució perfecta: 3 a zona-a, 3 a zona-b. El maxSkew: 1 entre nodes també es compleix: 1-2-2-1.

Experiment 1: el manteniment planificat (voluntari)

# Terminal 1: generar transit continu i comptar errors
kubectl -n rutas-norte-dev run client --rm -it --restart=Never \
  --image=busybox:1.36 -- /bin/sh -c \
  'ok=0; err=0; while true; do
     if wget -q -T2 -O- http://caos-api > /dev/null 2>&1; then
       ok=$((ok+1)); else err=$((err+1)); fi;
     echo "OK=$ok ERR=$err";
     sleep 0.2;
   done'
# Terminal 2: el drenatge
kubectl drain rutas-norte-m02 --ignore-daemonsets --delete-emptydir-data

Resultat esperat al terminal 1:

OK=847 ERR=0
OK=848 ERR=0
OK=849 ERR=0
...
OK=1204 ERR=0

Zero errors. El PDB va serialitzar el desallotjament dels 2 pods d'aquest node, i el Service (04-02) va treure dels seus endpoints cada pod tan bon punt va deixar d'estar Ready.

Això és el que es busca: el manteniment és invisible per a l'usuari.

# Restaurar
kubectl uncordon rutas-norte-m02

Experiment 2: la caiguda d'un node (involuntària)

# Simular una fallada de maquinari: apagar el node bruscament
minikube -p rutas-norte node stop rutas-norte-m03

Observem la seqüència completa:

kubectl get nodes --watch
NAME              STATUS     ROLES    AGE
rutas-norte-m03   Ready      <none>   1h
rutas-norte-m03   NotReady   <none>   1h     <- als ~40 segons
kubectl get pods -n rutas-norte-dev -o wide --watch
NAME                        READY   STATUS        NODE
caos-api-7d9c5f8b6b-9hbcx   1/1     Running       rutas-norte-m03
caos-api-7d9c5f8b6b-tv6ws   1/1     Running       rutas-norte-m03
...
(passen ~5 minuts)
...
caos-api-7d9c5f8b6b-9hbcx   1/1     Terminating   rutas-norte-m03
caos-api-7d9c5f8b6b-tv6ws   1/1     Terminating   rutas-norte-m03
caos-api-7d9c5f8b6b-x2klm   0/1     Pending       <none>
caos-api-7d9c5f8b6b-p9wnz   0/1     Pending       <none>
caos-api-7d9c5f8b6b-x2klm   0/1     ContainerCreating   rutas-norte-m04
caos-api-7d9c5f8b6b-p9wnz   1/1     Running             rutas-norte-m04

Els temps són l'ensenyament de l'experiment:

Moment Esdeveniment Temps acumulat
t=0 El node s'apaga 0
t=40 s El pla de control el marca NotReady (node-monitor-grace-period) 40 s
t=40 s El Service treu els seus pods dels endpoints. El trànsit deixa d'anar-hi 40 s
t=5 min Els taints NoExecute desallotgen els pods (tolerationSeconds: 300) 5 min
t=5 min El ReplicaSet crea els pods substituts 5 min
t=5 min 20 s Els nous pods estan Ready 5 min 20 s

El punt crític són aquests primers 40 segons: el node és mort però Kubernetes encara no ho sap, i el Service continua enviant-li trànsit. Durant aquests 40 segons, un terç de les peticions fallen.

Al terminal 1 veuries una cosa així:

OK=1204 ERR=0
OK=1210 ERR=3      <- comencen els errors
OK=1218 ERR=11
...
OK=1301 ERR=68     <- ~40 segons d'errors
OK=1409 ERR=68     <- s'aturen: el Service ja va excloure el node mort
OK=1520 ERR=68

Aquesta és la diferència fonamental entre les interrupcions voluntàries i les involuntàries, mesurada amb un cronòmetre:

Voluntària (drain) Involuntària (node mort)
El Service exclou els pods Immediatament (la readiness falla en rebre SIGTERM) Als ~40 segons
Errors per a l'usuari Zero ~40 segons d'errors
Protecció possible PDB Cap; només limitar el dany repartint

Què fer amb aquests 40 segons

No es poden eliminar, però es poden mitigar:

Mesura Efecte
Repartir bé les rèpliques (topologySpreadConstraints) Amb 2 de 6 rèpliques al node mort, només falla el 33 % de les peticions, no el 100 %
Reintents al client botiga-web reintenta amb un altre pod; l'usuari no veu l'error
Malla de serveis (08-04) Detecció de fallades i reintents automàtics, amb expulsió d'instàncies que fallen
Baixar node-monitor-grace-period Detecta abans... però produeix falsos positius amb latència de xarxa

L'última opció és temptadora i gairebé sempre mala idea: baixar el llindar de detecció fa que una latència temporal de xarxa s'interpreti com un node caigut, i desallotja pods sans. Els 40 segons per defecte són un compromís ben triat.

Neteja

minikube -p rutas-norte node start rutas-norte-m03
kubectl delete -f /tmp/caos-demo.yaml

Escalant la prova de caos

Aquest experiment manual és el nivell més bàsic. En un entorn seriós, s'automatitza amb eines d'enginyeria del caos (Chaos Mesh, LitmusChaos) que permeten:

  • Matar pods aleatoris de forma contínua.
  • Injectar latència de xarxa entre serveis.
  • Simular la caiguda d'una zona completa.
  • Omplir el disc d'un node.
  • Executar-ho periòdicament i alertar si el sistema no aguanta.

Però l'experiment manual és on cal començar, perquè és el que ensenya els temps reals. Fes-ho una vegada a rutas-norte-pre abans del pont de maig i sabràs exactament què esperar.

Errors Comuns i Consells

Error 1: PDB amb minAvailable absolut sobre una càrrega amb HPA. L'error més freqüent d'aquest mòdul. L'HPA baixa les rèpliques a la vall, el minAvailable es torna inassolible, i el PDB passa a bloquejar tot el manteniment i tota la reducció de nodes. Amb autoescalat, sempre percentatges.

Error 2: creure que un PDB protegeix de la caiguda d'un node. No ho fa. Els PDB només intervenen en desallotjaments que passen per l'API. Un node que s'apaga no demana permís. La protecció contra fallades és la distribució topològica.

Error 3: podAntiAffinity requerida sobre hostname en una càrrega amb HPA. Limita les rèpliques al nombre de nodes. L'HPA en demanarà 30, se'n col·locaran 9, i 21 quedaran Pending per sempre mentre el Cluster Autoscaler arrenca nodes que només caben d'un en un. Fes servir topologySpreadConstraints.

Error 4: DoNotSchedule entre nodes en una càrrega amb autoescalat. Durant el pont de maig, amb el CA arrencant nodes, una restricció dura entre nodes bloqueja l'escalat justament quan més falta fa. DoNotSchedule per a zones, ScheduleAnyway per a nodes.

Error 5: oblidar matchLabelKeys: ["pod-template-hash"]. Sense ell, el càlcul del biaix barreja les versions vella i nova durant un desplegament, i l'actualització s'encalla o es distribueix malament. Inclou-lo sempre en Deployments.

Error 6: oblidar l'uncordon. Un node acordonat i oblidat és capacitat pagada i inutilitzable. El símptoma —pods Pending mentre hi ha nodes aparentment sans— és especialment desconcertant. Posa-ho a la llista de comprovació del manteniment.

Error 7: --disable-eviction per «desencallar» un drenatge. Salta els PDB fent servir esborrat directe. Pot deixar un servei sense cap rèplica. Si un drenatge està bloquejat, la resposta correcta és diagnosticar per què, no desactivar la protecció.

Error 8: no posar PDB als components d'infraestructura. CoreDNS, el controlador d'Ingress i Prometheus són tan crítics com l'aplicació. Un drenatge que s'endugui les dues rèpliques de CoreDNS tomba la resolució de noms de tot el clúster, amb símptomes que ningú relaciona amb el manteniment.

Consell 1: revisa ALLOWED DISRUPTIONS abans de cada manteniment. Un kubectl get pdb --all-namespaces de dos segons et diu si el manteniment fluirà o s'encallarà. És la comprovació prèvia més rendible que existeix.

Consell 2: anota els PDB deliberadament bloquejants. postgres-reserves i redis-cache tenen ALLOWED DISRUPTIONS: 0 a propòsit. Una anotació que ho expliqui evita que algú els «arregli» sense entendre per què estan així, i permet excloure'ls de les alertes.

Consell 3: alerta sobre PDB bloquejats no deliberats.

# PDB sense desallotjaments permesos, excloent els intencionats
kube_poddisruptionbudget_status_pod_disruptions_allowed == 0
  unless on(namespace, poddisruptionbudget)
  kube_poddisruptionbudget_annotations{annotation_rutasnorte_example_pdb_bloqueig_intencionat!=""}

Consell 4: verifica la distribució real, no la configurada.

# Repliques per zona
kubectl get pods -n rutas-norte-pro -l app=api-reserves \
  -o custom-columns=NODE:.spec.nodeName --no-headers | \
  while read n; do kubectl get node "$n" -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/zone}{"\n"}'; done | \
  sort | uniq -c
      3 eu-west-1a
      3 eu-west-1b
      2 eu-west-1c

Una restricció ben escrita que no es compleix (perquè no hi ha nodes en una zona, per exemple) és pitjor que cap: dona falsa sensació de seguretat.

Consell 5: assaja el manteniment a rutas-norte-pre. Drena un node de preproducció amb trànsit de prova i mesura l'impacte. Si a pre hi ha errors, a pro n'hi haurà de multiplicats.

Consell 6: fes la prova de caos almenys una vegada. Matar un node i cronometrar la recuperació ensenya més que qualsevol documentació. Els 40 segons de detecció són un número que cal haver vist amb els propis ulls.

Consell 7: coordina el maxUnavailable del Deployment amb el del PDB. Fes servir el mateix valor perquè el nivell de servei garantit sigui el mateix durant un desplegament i durant un manteniment.

Exercicis

Exercici 1: diagnosticar un manteniment bloquejat

És dimarts al matí i cal actualitzar els nodes. El drenatge del primer node porta 20 minuts encallat:

kubectl drain node-b1 --ignore-daemonsets --delete-emptydir-data
node/node-b1 already cordoned
Warning: ignoring DaemonSet-managed Pods: kube-system/fluentd-2wxkp, kube-system/falco-x7kqp
evicting pod monitoratge/prometheus-server-0
evicting pod rutas-norte-pro/api-reserves-7c9d4f8b6d-mn2vp
evicting pod rutas-norte-pro/worker-notificacions-5f8d9c-4kjnx
pod/worker-notificacions-5f8d9c-4kjnx evicted
error when evicting pods/"prometheus-server-0" -n "monitoratge"
(will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
error when evicting pods/"api-reserves-7c9d4f8b6d-mn2vp" -n "rutas-norte-pro"
(will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.

Estat dels PDB:

NAMESPACE          NAME                   MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS
rutas-norte-pro    api-reserves           8               N/A               0
rutas-norte-pro    botiga-web             N/A             34%               1
rutas-norte-pro    worker-notificacions   N/A             50%               1
monitoratge        prometheus             1               N/A               0

Estat dels pods:

NAMESPACE          NAME                            READY   STATUS    NODE
rutas-norte-pro    api-reserves-7c9d4f8b6d-2mkjp   2/2     Running   node-a1
rutas-norte-pro    api-reserves-7c9d4f8b6d-9nvtr   2/2     Running   node-a2
rutas-norte-pro    api-reserves-7c9d4f8b6d-mn2vp   2/2     Running   node-b1
rutas-norte-pro    api-reserves-7c9d4f8b6d-tv6ws   2/2     Running   node-b2
rutas-norte-pro    api-reserves-7c9d4f8b6d-x2klm   1/2     Running   node-a1
monitoratge        prometheus-server-0             1/1     Running   node-b1

Analitza els dos bloquejos per separat. Per a cadascun: és legítim o és un error? Quina és la causa exacta? Com el desbloqueges ara i com evites que torni a passar? Presta especial atenció al pod x2klm.

Exercici 2: calcular la distribució i l'impacte

api-reserves té aquesta configuració:

topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    minDomains: 3
    labelSelector:
      matchLabels:
        app: api-reserves
  - maxSkew: 2
    topologyKey: kubernetes.io/hostname
    whenUnsatisfiable: ScheduleAnyway
    labelSelector:
      matchLabels:
        app: api-reserves
# PDB
spec:
  maxUnavailable: 25%

El clúster té 9 nodes: 3 a eu-west-1a, 3 a eu-west-1b, 3 a eu-west-1c.

Calcula per a 11 rèpliques i per a 26 rèpliques:

(a) El repartiment entre zones i el biaix resultant. (b) El repartiment entre nodes dins de cada zona. (c) Quantes rèpliques es perden si cau un node i quin percentatge de capacitat queda. (d) Quantes rèpliques es perden si cau una zona sencera i quin percentatge queda. (e) Quants desallotjaments simultanis permet el PDB en cada cas. (f) Si en drenar un node amb 26 rèpliques desplegades es pot buidar aquest node d'una sola vegada, o el PDB ho serialitza.

A més: què passaria amb 2 rèpliques i minDomains: 3? I amb 2 rèpliques si una zona es queda sense nodes disponibles?

Exercici 3: dissenyar l'alta disponibilitat d'un component nou

Rutas Norte incorpora passarela-pagaments, el component que parla amb el banc per cobrar les reserves. Perfil:

  • És un servei HTTP sense estat, al camí crític: si falla, no es pot comprar.
  • Cada transacció triga entre 2 i 8 segons (esperant el banc), i no es pot interrompre a mitges: una transacció tallada pot deixar un cobrament sense reserva associada.
  • El banc limita a 50 connexions simultànies des de la IP de Rutas Norte.
  • Trànsit: 5-40 transaccions per segon un dia normal; fins a 300 al pic del pont de maig.
  • Arrencada: 20 segons (cal establir la sessió TLS mútua amb el banc i validar certificats).
  • L'equip de compliment exigeix que cada transacció quedi registrada abans de respondre a l'usuari.
  • Ha de funcionar encara que caigui una zona de disponibilitat completa.

Dissenya l'estratègia completa d'alta disponibilitat: nombre de rèpliques, PDB, topologySpreadConstraints, terminationGracePeriodSeconds, i qualsevol altre mecanisme que consideris necessari. Justifica cada decisió i escriu els manifests. Presta atenció especial al límit de 50 connexions del banc i a l'exigència de no interrompre transaccions.


Solucions

Solució 1

Hi ha tres troballes, no dues.

Bloqueig 1: api-reserves amb minAvailable: 8 — ERROR de configuració.

Repliques totals d'api-reserves: 5
  - 4 amb READY 2/2 (sanes)
  - 1 amb READY 1/2 (el pod x2klm: un contenidor no esta llest)

Repliques DISPONIBLES (Ready): 4
El PDB exigeix: minAvailable: 8

4 < 8  ->  JA estem per sota del minim.
ALLOWED DISRUPTIONS: 0. I continuara sent 0 passi el que passi.

És el cas de llibre del PDB impossible amb HPA: algú va posar minAvailable: 8 quan hi havia 12 rèpliques durant una punta, i ara l'HPA ha baixat a 5. El PDB no es va ajustar perquè és un número absolut.

Tercera troballa: el pod x2klm està en 1/2.

Aquest és el detall que cal caçar. api-reserves té dos contenidors (l'API i l'exportador de mètriques). 1/2 significa que un d'ells no està Ready.

kubectl describe pod api-reserves-7c9d4f8b6d-x2klm -n rutas-norte-pro
kubectl logs api-reserves-7c9d4f8b6d-x2klm -n rutas-norte-pro -c exportador-metriques

Un pod que no està completament Ready no compta com a disponible per al PDB. Així que encara que corregíssim el PDB, aquest pod és un problema a part que cal investigar: pot ser el símptoma d'alguna cosa més greu.

Desbloqueig immediat:

kubectl patch pdb api-reserves -n rutas-norte-pro --type merge \
  -p '{"spec":{"minAvailable":null,"maxUnavailable":"25%"}}'

Efecte immediat:

Repliques desitjades: 5
maxUnavailable: 25% -> terra(5 x 0,25) = 1
Minim disponible: 5 - 1 = 4
Disponibles actuals: 4
ALLOWED DISRUPTIONS: 0

...Continua sent 0, per culpa del pod x2klm que no esta Ready.

El pedaç sol no basta. Cal resoldre també el pod trencat. Dues vies:

# Via A: si el pod esta trencat sense remei, esborrar-lo perque es recrei
kubectl delete pod api-reserves-7c9d4f8b6d-x2klm -n rutas-norte-pro
# (kubectl delete NO respecta PDB: aqui ens va be)

# Via B: afegir unhealthyPodEvictionPolicy perque els pods no sans
# no consumeixin pressupost
kubectl patch pdb api-reserves -n rutas-norte-pro --type merge \
  -p '{"spec":{"unhealthyPodEvictionPolicy":"AlwaysAllow"}}'

La via B és la correcta a llarg termini, i és exactament el problema que resol unhealthyPodEvictionPolicy (apartat 7): un pod no sa estava bloquejant la reparació del sistema.

Manifest corregit:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
  annotations:
    rutasnorte.example/nota: >
      PERCENTATGE obligatori: api-reserves te KEDA/HPA i oscil·la entre
      4 i 30 repliques. Un minAvailable absolut es torna impossible a la
      vall i bloqueja tot el manteniment. Veure 09-05.
spec:
  maxUnavailable: 25%
  selector:
    matchLabels:
      app: api-reserves
  unhealthyPodEvictionPolicy: AlwaysAllow

Bloqueig 2: prometheus amb minAvailable: 1 i una rèplica — LEGÍTIM però mal resolt.

prometheus-server-0: 1 replica (es un StatefulSet).
PDB: minAvailable: 1.
Desallotjar-la deixaria 0 disponibles.
ALLOWED DISRUPTIONS: 0. Permanent i per disseny.

El bloqueig és coherent amb la configuració, però la configuració és discutible. Cal decidir què es vol:

Opció A: acceptar la interrupció. Prometheus amb una rèplica i emmagatzematge local. Perdre uns minuts de mètriques durant un manteniment és molest però no crític: no afecta el servei als usuaris.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: prometheus
  namespace: monitoratge
spec:
  # Una sola replica: permetem el desallotjament. Perdem uns minuts de
  # metriques durant el manteniment, cosa que es acceptable: Prometheus
  # no esta al cami critic de la venda.
  maxUnavailable: 1
  selector:
    matchLabels:
      app: prometheus

Opció B: dues rèpliques. Prometheus en alta disponibilitat es fa amb dues instàncies independents que recol·lecten el mateix, més Thanos o Mimir per desduplicar. És més complex i només es justifica si les alertes són crítiques.

Decisió pragmàtica: opció A. Prometheus no és al camí de la venda, i l'observabilitat pot tenir un forat de tres minuts durant un manteniment planificat.

Nota important i contraintuïtiva: perdre Prometheus durant el manteniment té un efecte secundari del mòdul anterior. Si api-reserves escala amb KEDA per una mètrica de Prometheus (09-04), KEDA entrarà en mode fallback mentre Prometheus no hi sigui. Per això el fallback de 09-04 és tan important: cobreix exactament aquest cas.

Procediment complet per desencallar:

# 1. Corregir el PDB d'api-reserves
kubectl patch pdb api-reserves -n rutas-norte-pro --type merge \
  -p '{"spec":{"minAvailable":null,"maxUnavailable":"25%","unhealthyPodEvictionPolicy":"AlwaysAllow"}}'

# 2. Corregir el PDB de prometheus
kubectl patch pdb prometheus -n monitoratge --type merge \
  -p '{"spec":{"minAvailable":null,"maxUnavailable":1}}'

# 3. Verificar
kubectl get pdb --all-namespaces
NAMESPACE          NAME                   MAX UNAVAILABLE   ALLOWED DISRUPTIONS
rutas-norte-pro    api-reserves           25%               1
monitoratge        prometheus             1                 1

El drenatge, que continuava reintentant en segon pla, avança sol.

# 4. Investigar el pod x2klm (no urgent, pero no ho oblidis)
kubectl describe pod api-reserves-7c9d4f8b6d-x2klm -n rutas-norte-pro

Prevenció estructural:

  1. Comprovació prèvia obligatòria al procediment de manteniment:
kubectl get pdb --all-namespaces \
  -o custom-columns='NS:.metadata.namespace,NOM:.metadata.name,PERMESOS:.status.disruptionsAllowed' \
  | awk 'NR==1 || $3=="0"'
  1. Política de Kyverno (08-03) que rebutgi PDB amb minAvailable absolut en càrregues que tinguin HPA o ScaledObject.

  2. Alerta permanent:

kube_poddisruptionbudget_status_pod_disruptions_allowed == 0

Amb l'anotació d'exclusió per als bloquejos deliberats (postgres-reserves, redis-cache).

Solució 2

Cas A: 11 rèpliques

(a) Repartiment entre zones (maxSkew: 1, DoNotSchedule, 3 zones):

11 / 3 = 3,67

Repartiment mes equilibrat possible:
  eu-west-1a: 4
  eu-west-1b: 4
  eu-west-1c: 3

Biaix = 4 - 3 = 1 <= maxSkew 1. COMPLEIX.

(b) Repartiment entre nodes (maxSkew: 2, ScheduleAnyway, 3 nodes per zona):

Zona a (4 repliques, 3 nodes): 2-1-1
Zona b (4 repliques, 3 nodes): 2-1-1
Zona c (3 repliques, 3 nodes): 1-1-1

Biaix global entre nodes: max 2 - min 1 = 1 <= maxSkew 2. COMPLEIX.

(c) Caiguda d'un node:

Pitjor cas: cau un node amb 2 repliques.
Perdues: 2 d'11.
Queden: 9 d'11 = 81,8 % de capacitat.

(d) Caiguda d'una zona:

Pitjor cas: cau una zona amb 4 repliques.
Perdues: 4 d'11.
Queden: 7 d'11 = 63,6 % de capacitat.

(e) Desallotjaments permesos:

maxUnavailable: 25% sobre 11 repliques.
terra(11 x 0,25) = terra(2,75) = 2

ALLOWED DISRUPTIONS: 2

Cas B: 26 rèpliques

(a) Repartiment entre zones:

26 / 3 = 8,67

Repartiment:
  eu-west-1a: 9
  eu-west-1b: 9
  eu-west-1c: 8

Biaix = 9 - 8 = 1 <= maxSkew 1. COMPLEIX.

(b) Repartiment entre nodes:

Zona a (9 repliques, 3 nodes): 3-3-3
Zona b (9 repliques, 3 nodes): 3-3-3
Zona c (8 repliques, 3 nodes): 3-3-2

Biaix global entre nodes: 3 - 2 = 1 <= maxSkew 2. COMPLEIX.

(c) Caiguda d'un node:

Pitjor cas: cau un node amb 3 repliques.
Perdues: 3 de 26.
Queden: 23 de 26 = 88,5 % de capacitat.

(d) Caiguda d'una zona:

Pitjor cas: cau una zona amb 9 repliques.
Perdues: 9 de 26.
Queden: 17 de 26 = 65,4 % de capacitat.

(e) Desallotjaments permesos:

terra(26 x 0,25) = terra(6,5) = 6

ALLOWED DISRUPTIONS: 6

(f) Es pot buidar un node d'una vegada amb 26 rèpliques?

Un node te 3 repliques d'api-reserves.
El PDB permet 6 desallotjaments simultanis.

3 <= 6  ->  SI, es poden desallotjar les 3 d'una vegada.

El drenatge NO se serialitzara per culpa del PDB d'api-reserves.
Sera gairebe instantani per a aquest component.

Comparem amb el cas d'11 rèpliques:

Amb 11 repliques, un node te 2 repliques i el PDB permet 2 desallotjaments.
2 <= 2 -> tambe hi caben d'una vegada, encara que just al limit.

Si el node tingues 3 repliques (possible amb maxSkew 2), el PDB serialitzaria:
desallotja 2, espera que es recreïn, desallotja la tercera.

Observació important: el maxUnavailable: 25% escala amb les rèpliques, així que el manteniment es torna MÉS ràpid amb més rèpliques, no més lent. És exactament el que es vol: al pic hi ha més capacitat i es pot permetre perdre més rèpliques simultàniament.

Cas especial: 2 rèpliques amb minDomains: 3

minDomains: 3 significa que el planificador considera 3 dominis,
encara que algun estigui buit.

Col·locacio de la replica 1: zona a. Estat: a:1, b:0, c:0.
  Biaix = 1 - 0 = 1 <= maxSkew 1. Compleix.

Col·locacio de la replica 2:
  Si va a la zona a: a:2, b:0, c:0. Biaix = 2 - 0 = 2 > 1. NO PERMES.
  Si va a la zona b: a:1, b:1, c:0. Biaix = 1 - 0 = 1 <= 1. PERMES.

Resultat: 1 replica a la zona a, 1 a la zona b, 0 a la c.
La zona c queda buida, i aixo ES CORRECTE: amb 2 repliques no es poden
cobrir 3 zones.

Biaix final: 1. Compleix. La restricció funciona bé amb menys rèpliques que zones.

Cas especial: 2 rèpliques si una zona es queda sense nodes

Escenari: la zona c perd tots els seus nodes (caiguda completa).
Nodes disponibles: nomes a les zones a i b.

minDomains: 3 fa que el planificador CONTINUI COMPTANT 3 dominis.

Estat despres de la caiguda: a:1, b:1, c:0 (la zona c no te nodes).
Biaix = 1 - 0 = 1 <= maxSkew 1. COMPLEIX. Res a fer.

Pero si l'HPA volgues escalar a 4 repliques:
  Col·locacio de la replica 3:
    Si va a la zona a: a:2, b:1, c:0. Biaix = 2 - 0 = 2 > 1. NO PERMES.
    Si va a la zona b: a:1, b:2, c:0. Biaix = 2 - 0 = 2 > 1. NO PERMES.
    Si va a la zona c: NO HI HA NODES.

  -> La replica 3 es queda en PENDING PER SEMPRE.

Aquest és el perill de minDomains combinat amb DoNotSchedule. La restricció, pensada per garantir el repartiment, impedeix escalar quan una zona no està disponible: exactament el moment en què més falta fa escalar, perquè has perdut un terç de la capacitat.

Mitigacions possibles:

Opció Efecte Contrapartida
Treure minDomains Amb 2 zones vives, el repartiment entre elles compleix Perds la garantia de les 3 zones en operació normal
whenUnsatisfiable: ScheduleAnyway a zona Mai bloqueja Perds la garantia dura
Pujar maxSkew a 2 Amb a:2, b:1, c:0, el biaix és 2 <= 2. Permet Repartiment menys equilibrat
Acceptar-ho i tenir un procediment Davant d'una caiguda de zona, es relaxa la restricció a mà Requereix intervenció humana sota pressió

Recomanació per a Rutas Norte: maxSkew: 1 amb minDomains: 3 i un procediment documentat per relaxar-lo davant d'una caiguda de zona:

# PROCEDIMENT D'EMERGENCIA: caiguda de zona
# Relaxar la restriccio topologica per permetre escalar a les zones vives.
kubectl patch deployment api-reserves -n rutas-norte-pro --type json -p '[
  {"op": "replace", "path": "/spec/template/spec/topologySpreadConstraints/0/whenUnsatisfiable",
   "value": "ScheduleAnyway"}
]'
# REVERTIR quan la zona torni.

Tenir-ho escrit per endavant, provat, i al runbook. És molt millor que improvisar-ho a les tres de la matinada.

Solució 3

Anàlisi dels requisits i les seves conseqüències:

Requisit Conseqüència de disseny
Camí crític, sense estat Mínim 3 rèpliques, repartides entre 3 zones
Transacció de 2-8 s no interrompible terminationGracePeriodSeconds alt + apagada ordenada
50 connexions màximes del banc Límit dur al nombre de rèpliques
5-300 transaccions/s Autoescalat necessari, però acotat pel límit del banc
Arrencada de 20 s Objectiu d'escalat conservador; no escalar a zero
Registre abans de respondre L'apagada ha d'esperar que s'escrigui el registre
Ha de sobreviure a una zona DoNotSchedule entre zones, minDomains: 3

El límit de 50 connexions és la restricció dominant, i cal calcular-lo abans que res.

Pas 1: calcular el sostre real de rèpliques

El banc permet 50 connexions simultanies des de la IP de Rutas Norte.

Si cada replica mante un pool de N connexions:
  Repliques maximes = 50 / N

Amb N = 5 connexions per replica:  10 repliques maximes
Amb N = 3 connexions per replica:  16 repliques maximes
Amb N = 2 connexions per replica:  25 repliques maximes

Quantes transaccions per segon sosté una replica?
  Cada transaccio triga 2-8 s, mitjana ~5 s.
  Amb un pool de N connexions concurrents:
    transaccions/s = N / 5

  Amb N = 5: 1 transaccio/s per replica
  Amb N = 3: 0,6 transaccions/s per replica

Per a 300 transaccions/s al pic:
  Amb N = 5: 300 repliques. IMPOSSIBLE (excedeix el limit del banc per 30x).
  Amb N = 3: 500 repliques. Pitjor.

Aquí hi ha un problema greu que cap configuració de Kubernetes no resol:

Capacitat TEORICA MAXIMA del sistema:
  50 connexions concurrents / 5 segons per transaccio = 10 transaccions/s

OBJECTIU DEL PIC: 300 transaccions/s

EL SISTEMA NO POT PROCESSAR MES DE 10 TRANSACCIONS PER SEGON.
El limit es el BANC, no Kubernetes.

Aquesta és la lliçó de 09-01 sobre el coll d'ampolla real, en la seva forma més pura. Escalar passarela-pagaments a 30 rèpliques no augmenta la capacitat ni una engruna: les 30 rèpliques es barallarien per les mateixes 50 connexions.

Pas 2: l'arquitectura que sí que funciona

La solució no és d'escalat, és d'arquitectura:

1. NEGOCIAR amb el banc un limit mes gran. 50 connexions es ridicul per a
   300 transaccions/s. Es l'accio mes important i no es tecnica.

2. Mentrestant: DESACOBLAR amb una cua.
   - api-reserves encua la peticio de cobrament i respon a l'usuari
     "processant el seu pagament".
   - passarela-pagaments consumeix de la cua a la velocitat que el banc permet.
   - L'usuari rep la confirmacio per notificacio push o correu.

   Aixo converteix una restriccio dura en una latencia acceptable, i encaixa
   perfectament amb KEDA (09-04): la cua es el senyal d'escalat natural.

3. Un POOL COMPARTIT de connexions al banc, en lloc d'un pool per replica.

Com que l'enunciat demana dissenyar l'alta disponibilitat, assumim l'opció 3 amb un nombre de rèpliques acotat.

Pas 3: els manifests

# k8s/entorns/pro/deployment-passarela-pagaments.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: passarela-pagaments
  namespace: rutas-norte-pro
  labels:
    app: passarela-pagaments
    app.kubernetes.io/part-of: rutas-norte
spec:
  # SENSE camp replicas: ho governa l'HPA/KEDA.
  selector:
    matchLabels:
      app: passarela-pagaments

  strategy:
    type: RollingUpdate
    rollingUpdate:
      # Coherent amb el PDB (veure mes avall). Mai perdre mes d'1 replica.
      maxUnavailable: 1
      # maxSurge 1, no mes: cada replica nova obre connexions al banc i
      # tenim un limit dur de 50. Un maxSurge alt podria esgotar-lo
      # durant el desplegament.
      maxSurge: 1

  template:
    metadata:
      labels:
        app: passarela-pagaments
        app.kubernetes.io/part-of: rutas-norte
    spec:
      # PERIODE DE GRACIA DE 90 SEGONS.
      #
      # Una transaccio triga fins a 8 segons i NO es pot interrompre:
      # tallar-la podria deixar un cobrament sense reserva associada, que es el pitjor
      # error possible en una passarel·la de pagaments.
      #
      # 90 s = 8 s (transaccio mes llarga) + marge ampli per:
      #   - buidar la cua interna de transaccions en curs
      #   - escriure el registre de compliment de cadascuna
      #   - tancar netament les connexions TLS amb el banc
      #
      # El proces HA de gestionar SIGTERM: deixar d'acceptar peticions noves,
      # acabar les en curs, escriure els registres, i sortir.
      terminationGracePeriodSeconds: 90

      topologySpreadConstraints:
        # ZONES: restriccio DURA. El requisit de sobreviure a la caiguda d'una
        # zona es explicit i no negociable.
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          minDomains: 3
          labelSelector:
            matchLabels:
              app: passarela-pagaments
          matchLabelKeys: ["pod-template-hash"]

        # NODES: restriccio DURA tambe, i aqui SI que ens la podem permetre.
        # Amb un maxim de 9 repliques i 9 nodes, exigir-ne 1 per node es
        # assolible. I dues repliques de la passarel·la al mateix node son
        # dues repliques que es perden juntes: inacceptable al cami
        # critic del pagament.
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: passarela-pagaments
          matchLabelKeys: ["pod-template-hash"]

      containers:
        - name: passarela
          image: registry.rutasnorte.example/passarela-pagaments:2.4.1

          # PARADA ORDENADA: el hook preStop dona marge perque el Service
          # tregui aquest pod dels seus endpoints ABANS que comenci a apagar-se.
          # Sense aixo, hi ha una finestra d'1-2 segons en que el pod rep
          # peticions noves mentre ja s'esta apagant.
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 10"]

          resources:
            requests: {cpu: 200m, memory: 256Mi}
            limits: {cpu: 500m, memory: 512Mi}

          env:
            # POOL PETIT I EXPLICIT: 5 connexions per replica.
            # Amb maxReplicas 9: 9 x 5 = 45 < 50. Deixa marge de 5 connexions
            # per al desplegament (maxSurge 1) i per a reintents.
            # AQUEST NUMERO ES CRITIC: pujar-lo esgota el limit del banc.
            - name: BANC_POOL_MAX
              value: "5"

          readinessProbe:
            httpGet: {path: /preparat, port: 8080}
            initialDelaySeconds: 20        # L'arrencada triga 20 s (TLS mutu)
            periodSeconds: 5
            failureThreshold: 2
          livenessProbe:
            httpGet: {path: /salut, port: 8080}
            initialDelaySeconds: 30
            periodSeconds: 10
            failureThreshold: 3
          # STARTUP PROBE: dona fins a 60 s per a l'arrencada sense que la
          # livenessProbe mati el pod a mitja negociacio TLS.
          startupProbe:
            httpGet: {path: /salut, port: 8080}
            periodSeconds: 5
            failureThreshold: 12
# k8s/entorns/pro/pdb-passarela-pagaments.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: passarela-pagaments
  namespace: rutas-norte-pro
  labels:
    app: passarela-pagaments
    app.kubernetes.io/part-of: rutas-norte
spec:
  # maxUnavailable: 1 EN NUMERO ABSOLUT, no en percentatge.
  #
  # Es l'excepcio a la regla general de l'apartat 3, i es deliberada:
  #
  # 1. El rang de repliques es ESTRET (3 a 9), no com api-reserves (4 a 30).
  #    Un percentatge del 25 % donaria: 3 repliques -> 0 desallotjaments (BLOQUEIG), i
  #    9 repliques -> 2 desallotjaments. El cas de 3 repliques seria un PDB impossible.
  #
  # 2. Cada replica que cau pot endur-se fins a 5 transaccions en vol,
  #    cadascuna amb diners reals d'un client. Volem que els desallotjaments es
  #    SERIALITZIN sempre, sigui quin sigui el nombre de repliques.
  #
  # 3. Amb el terminationGracePeriodSeconds de 90 s, cada desallotjament triga fins a
  #    un minut i mig. Serialitzar-los fa el manteniment lent pero segur,
  #    que es exactament el compromis que volem en una passarel·la de pagaments.
  maxUnavailable: 1

  selector:
    matchLabels:
      app: passarela-pagaments

  # AlwaysAllow: un pod que no passa la readiness no esta processant pagaments
  # (el Service ja no li envia transit), aixi que desallotjar-lo no posa en
  # risc cap transaccio.
  unhealthyPodEvictionPolicy: AlwaysAllow
# k8s/entorns/pro/scaledobject-passarela-pagaments.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: passarela-pagaments
  namespace: rutas-norte-pro
spec:
  scaleTargetRef:
    name: passarela-pagaments

  pollingInterval: 15
  cooldownPeriod: 600

  # 3 repliques de terra: UNA PER ZONA. Es el minim per sobreviure a la
  # caiguda d'una zona conservant 2 repliques operatives. MAI escalar a zero:
  # l'arrencada de 20 s (negociacio TLS mutua amb el banc) es inacceptable
  # amb un usuari a la pantalla de pagament.
  minReplicaCount: 3

  # 9 REPLIQUES MAXIM. AQUEST NUMERO NO SURT DEL TRANSIT, SURT DEL BANC:
  #   9 repliques x 5 connexions = 45 connexions < 50 del limit.
  #   Marge de 5 connexions per al desplegament (maxSurge 1) i reintents.
  #
  # ADVERTENCIA CRITICA: pujar aquest numero SENSE renegociar el limit amb el
  # banc NO augmenta la capacitat. Nomes fa que les repliques competeixin per
  # les mateixes 50 connexions i comencin a rebre errors de connexio
  # rebutjada. El coll d'ampolla es el BANC (veure 09-01).
  maxReplicaCount: 9

  fallback:
    failureThreshold: 3
    replicas: 6

  advanced:
    horizontalPodAutoscalerConfig:
      behavior:
        scaleUp:
          # 30 s d'estabilitzacio (no 0): cada replica nova triga 20 s a
          # estar llesta i obre 5 connexions al banc. No volem crear
          # repliques a la babala i esgotar el limit de connexions.
          stabilizationWindowSeconds: 30
          selectPolicy: Max
          policies:
            - type: Pods
              value: 2
              periodSeconds: 60
        scaleDown:
          # 15 minuts. Cada replica que s'apaga triga fins a 90 s a acabar
          # les seves transaccions. Baixar a poc a poc es essencial.
          stabilizationWindowSeconds: 900
          selectPolicy: Min
          policies:
            - type: Pods
              value: 1
              periodSeconds: 300

  triggers:
    # Escalat per CONNEXIONS ACTIVES AL BANC, no per CPU ni per peticions.
    # Es la metrica que reflecteix el recurs escas real.
    - type: prometheus
      metadata:
        serverAddress: http://prometheus-operated.monitoratge.svc.cluster.local:9090
        metricName: passarela_connexions_banc_actives
        query: |
          sum(passarela_pagaments_connexions_banc_actives{namespace="rutas-norte-pro"})
        # Objectiu: 3,5 connexions actives per replica (d'un pool de 5).
        # Al 70 % d'ocupacio del pool, escalem.
        threshold: "3.5"
        activationThreshold: "1"
      authenticationRef:
        kind: ClusterTriggerAuthentication
        name: prometheus-lectura

    # Xarxa de seguretat: transaccions encuades esperant una connexio lliure.
    - type: prometheus
      metadata:
        serverAddress: http://prometheus-operated.monitoratge.svc.cluster.local:9090
        metricName: passarela_transaccions_en_espera
        query: |
          sum(passarela_pagaments_transaccions_en_espera{namespace="rutas-norte-pro"})
        threshold: "3"
        activationThreshold: "1"
      authenticationRef:
        kind: ClusterTriggerAuthentication
        name: prometheus-lectura

Resum de decisions i justificacions:

Decisió Elecció Justificació
minReplicaCount 3 Una per zona; sobreviu a la caiguda d'una zona amb 2 operatives
maxReplicaCount 9 9 × 5 = 45 < 50 connexions del banc. No surt del trànsit
PDB maxUnavailable: 1 absolut Rang estret (3-9); un 25 % donaria 0 desallotjaments amb 3 rèpliques
topologySpread zona DoNotSchedule + minDomains: 3 Requisit explícit de sobreviure a una zona
topologySpread node DoNotSchedule, maxSkew: 1 Amb 9 rèpliques i 9 nodes és assolible; dues rèpliques per node és risc innecessari
terminationGracePeriodSeconds 90 s 8 s de transacció + registre + tancament TLS, amb marge ampli
preStop sleep 10 Que el Service tregui el pod abans que comenci a apagar-se
startupProbe 60 s de marge El TLS mutu triga 20 s; evita que la liveness mati l'arrencada
Mètrica d'escalat Connexions actives al banc És el recurs escàs real, no la CPU ni les peticions
maxSurge 1 Cada rèplica extra consumeix connexions del límit del banc
Escalat a zero No 20 s d'arrencada amb un usuari a la pantalla de pagament

La conclusió que cal endur-se de l'exercici, i que va més enllà de la configuració:

L'alta disponibilitat de passarela-pagaments està limitada pel banc, no per Kubernetes. Cap quantitat de PDB, topologySpreadConstraints o autoescalat no permet processar més de 10 transaccions per segon amb 50 connexions i 5 segons per transacció.

Amb 300 transaccions per segon al pic del pont de maig, el sistema se saturarà irremeiablement. Les accions que de debò resolen el problema són:

  1. Renegociar el límit amb el banc. No és tècnica, és comercial, i és la més important.
  2. Desacoblar amb una cua, convertint una restricció dura en una latència acceptable comunicada a l'usuari.
  3. Un pool compartit en lloc d'un pool per rèplica, amb un component intermedi que multiplexi.
  4. Un límit de taxa explícit a api-reserves que rebutgi educadament les peticions de pagament quan la passarel·la està saturada, en lloc de deixar que s'acumulin fins al timeout.

Dissenyar l'alta disponibilitat d'un component sense entendre el seu coll d'ampolla real és exactament l'error que 09-01 advertia: escalar la capa web quan el límit és en una altra banda multiplica el cost sense vendre ni un bitllet més.

Conclusió

L'alta disponibilitat no és un objecte de Kubernetes que s'activa: és una propietat del sistema que es construeix entenent què pot fallar i quina protecció existeix per a cada cosa.

L'essencial:

  • Kubernetes distingeix interrupcions voluntàries i involuntàries, i només et pot protegir de les primeres. Un kubectl drain passa per l'API i es pot negociar; un servidor que s'apaga, no.
  • El PodDisruptionBudget declara quant servei ha de romandre disponible davant dels desallotjaments. El respecten kubectl drain, el Cluster Autoscaler, l'actualitzador del VPA i el descheduler; no el respecten kubectl delete pod ni les actualitzacions progressives del Deployment.
  • minAvailable parla del que queda; maxUnavailable del que se'n va. Els percentatges arrodoneixen en direccions oposades, sempre afavorint la disponibilitat. Amb HPA o KEDA, sempre percentatges: un número absolut es torna impossible a la vall i bloqueja tot el manteniment.
  • El PDB impossible (minAvailable igual a les rèpliques, o una sola rèplica) ancora nodes, bloqueja drenatges i impedeix al Cluster Autoscaler reduir. De vegades és deliberat (postgres-reserves); documenta'l amb una anotació perquè ningú l'«arregli».
  • unhealthyPodEvictionPolicy: AlwaysAllow trenca l'encallament circular d'un desplegament defectuós els pods trencats del qual impedeixen la seva pròpia reparació. Fes-lo servir en tot el que no tingui quòrum.
  • topologySpreadConstraints reparteix equitativament entre dominis de fallada. Davant de la podAntiAffinity de 06-05, guanya clarament en càrregues amb autoescalat: l'antiafinitat requerida limita les rèpliques al nombre de nodes, i amb 30 rèpliques i 9 nodes deixa 21 pods Pending per sempre.
  • L'asimetria entre nivells és la decisió clau: DoNotSchedule entre zones (la caiguda d'una zona és catastròfica) i ScheduleAnyway entre nodes (bloquejar l'escalat seria pitjor). I matchLabelKeys: ["pod-template-hash"] sempre, perquè els desplegaments no s'encallin.
  • L'alta disponibilitat de les altres capes importa igual: tres o cinc membres d'etcd (mai parell) repartits entre zones, diverses rèpliques del controlador d'Ingress i de CoreDNS amb els seus PDB, i per a postgres-reserves, un operador que sàpiga promocionar una rèplica: això Kubernetes no ho fa i no ho pot fer.
  • El procediment de manteniment és cordondrain → actualitzar → uncordon, amb verificació de PDB abans i de servei entre cada node. Sense PDB, un drenatge s'endú totes les rèpliques d'un node en un instant; amb PDB, les serialitza i l'usuari no nota res.
  • La prova de caos revela el que la teoria amaga: un drenatge produeix zero errors; un node que mor produeix uns 40 segons d'errors mentre el pla de control se n'assabenta. Aquesta diferència, mesurada amb cronòmetre, és la definició pràctica de voluntari davant d'involuntari.

Rutas Norte té ara la plataforma repartida entre tres zones amb garanties dures, PDB coherents amb l'autoescalat, i un procediment de manteniment que no interromp la venda. Les rèpliques creixen quan cal, tenen la mida correcta, els nodes apareixen quan no hi caben, l'escalat s'anticipa als esdeveniments coneguts, i res d'això es trenca quan algú actualitza un node o quan cau un servidor.

I tanmateix, queda una pregunta que no hem fet en tot el mòdul: és que cal tant?

Hem donat per bons els números que anàvem arrossegant: que una rèplica d'api-reserves sosté 85 peticions per segon, que cal escalar a 30 al pic, que el requests.cpu ha de ser 412m. Però ningú ha preguntat per què una rèplica només aguanta 85 peticions per segon, ni si podria aguantar-ne 200 amb els mateixos recursos. Ningú ha mirat si el pool de connexions a PostgreSQL està ben dimensionat, si les consultes tenen els índexs que necessiten, si redis-cache s'està fent servir de debò, o si l'estrangulament de CPU que vam veure al mòdul 3 apareix molt abans del que crèiem.

Escalar és multiplicar la ineficiència pel nombre de rèpliques. Si cada rèplica malgasta la meitat de la seva capacitat, trenta rèpliques en malgasten quinze.

A la propera lliçó, Ajust de Rendiment, tanquem el mòdul mirant cap endins: la metodologia de mesurar, trobar el coll d'ampolla real i canviar una sola cosa cada vegada; proves de càrrega amb k6 simulant el pont de maig i com llegir-ne els resultats de debò; l'ajust de la capa d'aplicació, que és on gairebé sempre és el problema; com es tradueix un limit de CPU en quota del kernel i per què l'estrangulament apareix molt abans del 100 %; l'arrencada ràpida com a requisit de l'autoescalat; l'ajust del clúster, inclòs l'efecte del ndots: 5 que vam deixar pendent a 04-03; i un pressupost de latència que converteix l'objectiu de negoci en objectius per component.

Curs de Kubernetes

Mòdul 1: Introducció a Kubernetes

Mòdul 2: Components Principals de Kubernetes

Mòdul 3: Gestió de Configuració i Secrets

Mòdul 4: Xarxes a Kubernetes

Mòdul 5: Emmagatzematge a Kubernetes

Mòdul 6: Conceptes Avançats de Kubernetes

Mòdul 7: Monitoratge i Registre

Mòdul 8: Seguretat a Kubernetes

Mòdul 9: Escalat i Rendiment

Mòdul 10: Ecosistema i Eines de Kubernetes

Mòdul 11: Estudis de Cas i Aplicacions del Món Real

Mòdul 12: Preparació per a la Certificació de Kubernetes

© Copyright 2026. Tots els drets reservats