Todo el módulo se ha ocupado hasta ahora de crecer: más pods, del tamaño correcto, sobre más nodos, y antes de que llegue el tráfico. Pero hay una pregunta que no hemos hecho: ¿qué pasa cuando algo se rompe, o cuando alguien tiene que tocar el clúster?
El próximo martes toca actualizar la versión de Kubernetes de los nodos de Rutas Norte. Alguien ejecutará kubectl drain sobre el primer nodo para vaciarlo. Si en ese nodo están tres de las cuatro réplicas de api-reservas —porque el planificador las puso ahí y nadie le dijo que no lo hiciera—, ese drenaje se llevará el 75 % de la capacidad de la API en un instante, y la cuarta réplica recibirá cuatro veces su carga habitual hasta caer también.
Y hay una variante peor: que el nodo no lo drene nadie, sino que se apague solo por un fallo de hardware en la zona de disponibilidad B, a las tres de la madrugada, sin previo aviso.
Los dos escenarios producen el mismo daño, pero Kubernetes solo puede protegerte de uno de ellos. Entender por qué, y qué hacer con el otro, es el contenido de esta lección.
Vamos a ver la distinción entre interrupciones voluntarias e involuntarias, el PodDisruptionBudget y la API de desalojo que lo respeta, los PDB imposibles que bloquean el mantenimiento para siempre, la distribución topológica con topologySpreadConstraints frente a la podAntiAffinity de 06-05, qué significa la alta disponibilidad en las demás capas del sistema, el procedimiento completo de mantenimiento de un nodo, y una prueba de caos para comprobar que todo funciona de verdad.
Contenido
- Interrupciones voluntarias e involuntarias
- El PodDisruptionBudget: qué es y qué protege
minAvailablefrente amaxUnavailable- La API de desalojo y
kubectl drain - Demostración: un drenaje que se queda esperando
- El PDB imposible que bloquea el mantenimiento
unhealthyPodEvictionPolicy- Los PDB de Rutas Norte, componente a componente
topologySpreadConstraintsa fondotopologySpreadConstraintsfrente apodAntiAffinity- Repartir Rutas Norte entre zonas y nodos
- Alta disponibilidad en las demás capas
- El procedimiento de mantenimiento de un nodo
- Interacción con el Cluster Autoscaler y los despliegues
- Una prueba de caos sencilla
- Errores comunes y consejos
- Ejercicios
- Conclusión
- Interrupciones voluntarias e involuntarias
Kubernetes distingue formalmente dos clases de interrupción, y la distinción no es académica: determina qué mecanismos de protección existen.
Interrupciones involuntarias
Ocurren sin que nadie las haya pedido. Son fallos:
| Causa | Ejemplo en Rutas Norte |
|---|---|
| Fallo de hardware del nodo | La fuente de alimentación del servidor que aloja tres réplicas |
| Caída de la zona de disponibilidad | Un corte eléctrico en el centro de datos de la zona B |
| El kernel mata un proceso por falta de memoria | OOMKilled de api-reservas (módulo 3) |
| Desalojo del kubelet por presión de recursos | El nodo se queda sin disco y desaloja pods BestEffort |
| Partición de red | El nodo pierde conectividad con el plano de control |
| Borrado accidental de una máquina virtual | Alguien se equivoca en la consola del proveedor |
Kubernetes no puede impedir ninguna de estas. Cuando el hardware falla, falla. Lo único que se puede hacer es limitar el daño: si las réplicas están repartidas, la caída de un nodo se lleva una fracción; si están amontonadas, se lleva todo.
La protección contra interrupciones involuntarias es arquitectónica: réplicas suficientes, repartidas entre dominios de fallo independientes. Eso es el apartado 9 en adelante.
Interrupciones voluntarias
Las provoca alguien deliberadamente, a través de la API:
| Causa | Ejemplo en Rutas Norte |
|---|---|
| Drenar un nodo para mantenimiento | Actualizar el kernel o la versión de Kubernetes |
| Reducción del clúster | El Cluster Autoscaler retira un nodo infrautilizado (09-03) |
| Redesplegar una aplicación | Una actualización progresiva de api-reservas (02-04) |
| El VPA ajusta recursos | El actualizador desaloja un pod para recrearlo (09-02) |
| Borrar un pod a mano | kubectl delete pod |
| Reprogramación por un descheduler | Una herramienta que reequilibra pods entre nodos |
Aquí sí puede intervenir Kubernetes, porque estas acciones pasan por el servidor de API y pueden ser evaluadas antes de ejecutarse. El mecanismo se llama PodDisruptionBudget, y es el objeto que dice: «puedes hacer mantenimiento, pero no a costa de dejarme sin servicio».
flowchart TD
subgraph INVOL["Interrupciones INVOLUNTARIAS"]
I1[Fallo de hardware]
I2[Caida de zona]
I3[OOMKilled]
I4[Particion de red]
end
subgraph VOL["Interrupciones VOLUNTARIAS"]
V1[kubectl drain]
V2[Reduccion del Cluster Autoscaler]
V3[Actualizacion progresiva]
V4[Desalojo del VPA]
end
INVOL -->|Kubernetes NO puede impedirlas| ARQ["Proteccion ARQUITECTONICA:<br/>replicas repartidas entre<br/>dominios de fallo<br/>topologySpreadConstraints"]
VOL -->|Pasan por la API de desalojo| PDB["Proteccion por 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
Las dos protecciones son complementarias y hacen falta ambas. Un PDB perfecto no te salva de que se caiga la zona donde tienes todas las réplicas. Y una distribución topológica perfecta no impide que un kubectl drain descuidado se lleve tres réplicas de golpe.
Un matiz importante sobre el maxUnavailable del Deployment
Conviene aclarar una confusión frecuente. El Deployment tiene su propio control de interrupción durante las actualizaciones:
Eso gobierna solo el despliegue progresivo (02-04). No protege de un kubectl drain, ni de la reducción del Cluster Autoscaler, ni de un desalojo del VPA. Son mecanismos distintos con ámbitos distintos, y necesitas los dos.
strategy.rollingUpdate.maxUnavailable |
PodDisruptionBudget | |
|---|---|---|
| Ámbito | Solo actualizaciones del Deployment | Cualquier desalojo por la API |
| Lo aplica | El controlador de Deployments | El servidor de API, en la petición de desalojo |
| Protege de | Que la propia actualización deje sin servicio | drain, CA, VPA, descheduler |
| Se define en | El Deployment | Un objeto PodDisruptionBudget aparte |
- El PodDisruptionBudget: qué es y qué protege
Un PodDisruptionBudget (PDB) es un objeto que declara: «de este conjunto de pods, siempre debe haber al menos N disponibles» (o «como mucho N no disponibles»).
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-reservas
namespace: rutas-norte-pro
spec:
maxUnavailable: 25%
selector:
matchLabels:
app: api-reservasTres campos y nada más:
selector: qué pods cubre. Igual que el selector de un Service o un Deployment (02-07).minAvailableomaxUnavailable: la restricción. Mutuamente excluyentes: uno de los dos, nunca ambos.- (Opcional)
unhealthyPodEvictionPolicy: apartado 7.
Qué hace exactamente
Cuando alguien pide desalojar un pod a través de la API de desalojo, el servidor de API:
- Busca los PDB cuyo
selectorcoincida con ese pod. - Para cada uno, calcula cuántos pods están actualmente disponibles (
Ready). - Comprueba si desalojar este pod violaría la restricción.
- Si la violaría, rechaza la petición con un error HTTP 429 (
TooManyRequests). - Si no, la acepta y el pod se elimina.
Es una comprobación en el momento de la petición, no una reserva. El cliente que recibe el 429 normalmente reintenta, y así el desalojo espera hasta que sea seguro.
Qué NO hace un PDB
Esta lista es tan importante como la anterior:
| El PDB no | Explicación |
|---|---|
| Impide que un nodo se caiga | Es una interrupción involuntaria: nadie pide permiso |
Impide kubectl delete pod |
El borrado directo no pasa por la API de desalojo |
| Garantiza que haya N réplicas | Eso lo hace el Deployment/ReplicaSet |
| Crea pods nuevos | No es un controlador de réplicas |
Protege de un OOMKilled |
Involuntario |
| Protege de que el kubelet desaloje por presión de recursos | Involuntario |
El segundo punto sorprende a mucha gente y merece énfasis: kubectl delete pod ignora los PDB por completo. El borrado directo es una operación distinta del desalojo. Si quieres respetar los PDB desde la línea de comandos, usa kubectl drain (que sí usa la API de desalojo) o la propia API:
# Esto IGNORA el PDB
kubectl delete pod api-reservas-7c9d4f8b6d-2mk8p -n rutas-norte-pro
# Esto RESPETA el PDB
kubectl drain <nodo> --pod-selector=app=api-reservas
minAvailable frente a maxUnavailable
minAvailable frente a maxUnavailableLos dos expresan lo mismo desde lados opuestos, pero se comportan de forma muy distinta cuando el número de réplicas cambia, que es exactamente lo que pasa cuando hay un HPA.
minAvailable
«Siempre debe haber al menos N pods disponibles.»
Con 4 réplicas: se puede desalojar 1 (quedan 3). Con 10 réplicas: se pueden desalojar 7.
maxUnavailable
«Como mucho puede haber N pods no disponibles.»
Con 4 réplicas: se puede desalojar 1. Con 10 réplicas: también solo 1.
La diferencia crítica con los porcentajes
Aquí está el detalle que casi nadie conoce y que produce comportamientos sorprendentes: el redondeo es distinto en cada caso.
| Campo | Redondeo del porcentaje | Motivo |
|---|---|---|
minAvailable: 50% |
Hacia arriba | Más conservador: garantiza más pods |
maxUnavailable: 50% |
Hacia abajo | Más conservador: permite menos desalojos |
En ambos casos el redondeo favorece la disponibilidad. Veámoslo con números:
Con 5 réplicas:
| PDB | Cálculo | Resultado | Desalojos permitidos |
|---|---|---|---|
minAvailable: 50% |
techo(5 × 0,5) = 3 |
Mínimo 3 disponibles | 2 |
maxUnavailable: 50% |
suelo(5 × 0,5) = 2 |
Máximo 2 no disponibles | 2 |
Aquí coinciden. Pero con 7 réplicas:
| PDB | Cálculo | Resultado | Desalojos permitidos |
|---|---|---|---|
minAvailable: 50% |
techo(7 × 0,5) = 4 |
Mínimo 4 disponibles | 3 |
maxUnavailable: 50% |
suelo(7 × 0,5) = 3 |
Máximo 3 no disponibles | 3 |
Coinciden otra vez. Con 3 réplicas:
| PDB | Cálculo | Resultado | Desalojos permitidos |
|---|---|---|---|
minAvailable: 50% |
techo(3 × 0,5) = 2 |
Mínimo 2 disponibles | 1 |
maxUnavailable: 50% |
suelo(3 × 0,5) = 1 |
Máximo 1 no disponible | 1 |
Para el 50 % el resultado es equivalente. La diferencia aparece con otros porcentajes. Con 10 réplicas y un 20 %:
| PDB | Cálculo | Resultado | Desalojos permitidos |
|---|---|---|---|
minAvailable: 20% |
techo(10 × 0,2) = 2 |
Mínimo 2 disponibles | 8 |
maxUnavailable: 20% |
suelo(10 × 0,2) = 2 |
Máximo 2 no disponibles | 2 |
Radicalmente distinto. minAvailable: 20% permite desalojar el 80 % de los pods; maxUnavailable: 20% permite desalojar el 20 %.
Regla mental: minAvailable habla de lo que queda; maxUnavailable habla de lo que se va.
Cuál usar: la regla del HPA
Y ahora la razón por la que esto importa tanto en este módulo. Con un HPA, el número de réplicas cambia solo. Un PDB en número absoluto que era razonable con 30 réplicas se vuelve imposible con 4.
Recordemos el caso del ejercicio 2 de 09-03:
Durante el puente: 30 replicas. Alguien pone minAvailable: 25.
Desalojos permitidos: 5. Razonable.
Tras el puente: el HPA baja a 4 replicas. El PDB sigue diciendo minAvailable: 25.
Pods disponibles: 4. Minimo exigido: 25.
YA se esta por debajo del minimo.
Desalojos permitidos: CERO. Para siempre.
El nodo que aloje cualquier replica de api-reservas queda ANCLADO.
El Cluster Autoscaler no lo puede retirar. El mantenimiento se bloquea.Por eso:
| Situación | Recomendación |
|---|---|
| Carga con HPA o KEDA | maxUnavailable en porcentaje |
| Número de réplicas fijo y conocido | Cualquiera; minAvailable es más explícito |
| Carga con estado y quórum (etcd, bases de datos) | maxUnavailable: 1 siempre |
| Una sola réplica | Ninguno de los dos funciona bien (apartado 6) |
maxUnavailable: 25% es la elección por defecto sensata para una carga con HPA. Se adapta sola: con 4 réplicas permite desalojar 1; con 30, permite desalojar 7. La proporción de servicio protegida es constante.
Una nota sobre el denominador: el porcentaje se calcula sobre el número de réplicas deseadas por el controlador (el spec.replicas del Deployment), no sobre los pods Ready en ese momento. Esto importa cuando hay pods arrancando: durante un escalado del HPA de 4 a 12, el denominador ya es 12 aunque solo 5 estén listos.
- La API de desalojo y
kubectl drain
kubectl drainEl PDB solo funciona si quien quiere quitar un pod pide permiso. Ese mecanismo es la API de desalojo (Eviction API).
Cómo funciona
Es un subrecurso del pod:
Con este cuerpo:
{
"apiVersion": "policy/v1",
"kind": "Eviction",
"metadata": {
"name": "api-reservas-7c9d4f8b6d-2mk8p",
"namespace": "rutas-norte-pro"
}
}Respuestas posibles:
| Código | Significado |
|---|---|
200 OK |
Desalojo aceptado; el pod se elimina con su periodo de gracia |
429 TooManyRequests |
Un PDB lo impide. Reintentar más tarde |
500 Internal Server Error |
Configuración incoherente (un PDB mal formado) |
El 429 incluye un mensaje explicativo:
{
"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-reservas needs 3 healthy pods and has 3 currently"
}
]
}
}Ese mensaje —«necesita 3 pods sanos y tiene 3 actualmente»— es el que verás en la práctica y el que hay que saber leer.
Quién usa la API de desalojo
| Cliente | ¿Respeta los PDB? |
|---|---|
kubectl drain |
Sí |
| Cluster Autoscaler (09-03) | Sí |
| Actualizador del VPA (09-02) | Sí |
| Descheduler | Sí |
| Karpenter (consolidación) | Sí |
kubectl delete pod |
No. Borrado directo |
| Controlador de Deployments (actualización progresiva) | No. Usa maxUnavailable propio |
| El kubelet desalojando por presión de recursos | No. Es involuntario |
| Un nodo que se apaga | No. Es involuntario |
Las cuatro primeras filas son las que importan: todos los mecanismos automáticos que hemos ido añadiendo en este módulo respetan los PDB. Es la garantía de que el autoescalado no se lleve por delante la disponibilidad.
El campo disruptionsAllowed
El PDB publica en su estado cuántos desalojos permite en este momento:
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
api-reservas N/A 25% 1 12d
tienda-web N/A 1 1 12d
postgres-reservas 1 N/A 0 12d
worker-notificaciones N/A 50% 3 12dALLOWED DISRUPTIONS es el número más útil de toda la lección. Si es 0, ningún pod de ese conjunto puede desalojarse ahora mismo, y cualquier drenaje que los afecte se quedará esperando.
Un 0 puede ser correcto (postgres-reservas con una réplica y minAvailable: 1) o síntoma de un problema (un PDB imposible, o pods que no están Ready).
Vista detallada:
Name: api-reservas
Namespace: rutas-norte-pro
Max unavailable: 25%
Selector: app=api-reservas
Status:
Allowed disruptions: 2
Current: 8
Desired: 6
Total: 8| Campo | Significado |
|---|---|
Current |
Pods Ready ahora mismo |
Desired |
Mínimo que exige el PDB (calculado) |
Total |
Pods que cubre el selector |
Allowed disruptions |
Current - Desired |
- Demostración: un drenaje que se queda esperando
Nada enseña mejor que verlo. Preparemos el escenario en minikube.
Preparación
minikube start -p rutas-norte --nodes=3 --cpus=2 --memory=4096
kubectl create namespace rutas-norte-dev
# Un Deployment con 3 replicas
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=64MiUn PDB deliberadamente restrictivo:
# /tmp/pdb-demo.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: demo-pdb
namespace: rutas-norte-dev
spec:
# Con 3 replicas, exigir 3 disponibles significa CERO desalojos permitidos.
minAvailable: 3
selector:
matchLabels:
app: demo-pdbALLOWED DISRUPTIONS: 0. Ya sabemos qué va a pasar.
El drenaje
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-m03node/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 drenaje se queda en bucle infinito, reintentando cada 5 segundos.
Observaciones importantes:
- El nodo YA está acordonado (
cordoned). Aunque el drenaje no avance, el nodo no acepta pods nuevos. Es lo primero que hacedrain. - El mensaje es explícito:
Cannot evict pod as it would violate the pod's disruption budget. drainno se rinde. Reintenta indefinidamente hasta que se lo permitan o hasta que lo interrumpas.
Este es exactamente el comportamiento deseado: el PDB ha impedido que el mantenimiento rompa el servicio. Pero también es exactamente el comportamiento que bloquea el mantenimiento para siempre si el PDB está mal, y esa es la lección del apartado 6.
Desbloquearlo
Tres formas:
# Opcion A: mas replicas. Con 4 replicas y minAvailable: 3, se permite 1 desalojo.
kubectl -n rutas-norte-dev scale deployment demo-pdb --replicas=4En cuanto la cuarta réplica esté Ready, el drenaje que estaba reintentando avanza solo. Es muy satisfactorio de ver.
# Opcion B: corregir el PDB
kubectl -n rutas-norte-dev patch pdb demo-pdb --type merge \
-p '{"spec":{"minAvailable":null,"maxUnavailable":"25%"}}'Ojo: minAvailable y maxUnavailable son mutuamente excluyentes, así que hay que anular uno al poner el otro.
# Opcion C (ULTIMO RECURSO, peligroso): forzar el drenaje
kubectl drain rutas-norte-m02 --ignore-daemonsets --delete-emptydir-data \
--disable-eviction--disable-eviction usa borrado directo en lugar de la API de desalojo, saltándose los PDB. Es la opción de emergencia, y hay que saber que puede dejar el servicio sin ninguna réplica disponible. Úsala solo cuando entiendas exactamente qué se va a caer y hayas decidido que es aceptable.
Limpieza
kubectl uncordon rutas-norte-m02
kubectl delete -f /tmp/pdb-demo.yaml
kubectl delete deployment demo-pdb -n rutas-norte-devNunca olvides el uncordon. Un nodo acordonado que nadie descordona es capacidad pagada e inutilizable, y el síntoma (pods Pending mientras hay nodos aparentemente libres) desconcierta muchísimo.
- El PDB imposible que bloquea el mantenimiento
Formalicemos el problema, porque es la trampa más peligrosa de esta lección.
La definición
Un PDB imposible es aquel cuya restricción no puede satisfacerse desalojando ni un solo pod. Su ALLOWED DISRUPTIONS es 0 de forma permanente.
Los dos casos:
Caso 1: minAvailable igual (o mayor) al número de réplicas.
Desalojar cualquier pod dejaría 2 disponibles, por debajo del mínimo de 3. Cero desalojos, siempre.
Y la variante insidiosa: minAvailable: 25 sobre un Deployment con HPA que ha bajado a 4 réplicas. El PDB era razonable cuando se escribió; el HPA lo ha vuelto imposible sin que nadie lo tocara.
Caso 2: una sola réplica con cualquier PDB restrictivo.
Con una réplica, desalojarla deja cero disponibles. Cero desalojos, siempre.
Las consecuencias
| Consecuencia | Detalle |
|---|---|
| El mantenimiento de nodos se bloquea | kubectl drain reintenta para siempre |
| El Cluster Autoscaler no puede reducir | Nodos infrautilizados que nadie retira, pagándose (09-03) |
| El VPA no puede aplicar recomendaciones | El actualizador falla al desalojar (09-02) |
| Las actualizaciones del clúster se atascan | Un proveedor gestionado puede abortar la actualización |
| El diagnóstico es difícil | El síntoma (nodos zombis) está lejos de la causa (un PDB) |
Ese último punto es el que hace peligroso el problema: nadie relaciona un nodo que no se retira con un PDB escrito hace tres meses. Los logs del Cluster Autoscaler lo dicen (pdb-blocked), pero hay que saber mirar ahí.
Detectarlos
NAMESPACE NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS
rutas-norte-pro api-reservas N/A 25% 2
rutas-norte-pro tienda-web N/A 1 1
rutas-norte-pro postgres-reservas 1 N/A 0
rutas-norte-pro redis-cache 1 N/A 0
rutas-norte-pro worker-notificaciones N/A 50% 4
monitorizacion prometheus 1 N/A 0Los tres ceros hay que analizarlos uno por uno:
postgres-reservas: una réplica primaria.ALLOWED DISRUPTIONS: 0es deliberado y correcto: no queremos que ningún mecanismo automático mueva la base de datos. El precio es que su nodo requiere intervención manual para el mantenimiento.redis-cache: mismo caso, aunque aquí es más discutible: perder la caché unos minutos es molesto pero no catastrófico. Habría argumentos para permitir el desalojo.prometheus: si Prometheus tiene una sola réplica (habitual), es el mismo patrón.
Una alerta útil para detectar los imposibles no deliberados:
# PDB que llevan mas de una hora sin permitir ningun desalojo
kube_poddisruptionbudget_status_pod_disruptions_allowed == 0Con una anotación en los PDB deliberados para excluirlos de la alerta.
Qué hacer con las cargas de una sola réplica
Es una pregunta que hay que responder de forma explícita, y hay tres respuestas legítimas:
Opción A: no poner PDB. Sin PDB, el desalojo se permite siempre. El servicio tendrá un corte durante el mantenimiento (los segundos que tarde en arrancar en otro nodo), pero el mantenimiento fluye. Es la opción correcta para servicios no críticos.
Opción B: poner maxUnavailable: 1 (equivalente a minAvailable: 0). Permite siempre el desalojo. Parece inútil, pero documenta explícitamente que se ha pensado en ello y se acepta la interrupción. Mejor que la ausencia de PDB, que es ambigua.
Opción C: poner dos réplicas. Si el servicio de verdad no puede interrumpirse, la solución no es un PDB: es tener más de una réplica. Un PDB no crea disponibilidad, solo la protege.
Para postgres-reservas, la respuesta correcta no es ninguna de las tres: es un operador (06-07) que gestione un clúster de PostgreSQL con primario y réplicas, y que sepa promocionar una réplica cuando el primario tenga que moverse. Lo desarrollamos en el apartado 12.
unhealthyPodEvictionPolicy
unhealthyPodEvictionPolicyUn campo relativamente reciente que resuelve un problema muy concreto y muy molesto.
El problema
Imagina que api-reservas tiene 6 réplicas y un PDB de maxUnavailable: 1. Un despliegue defectuoso hace que 4 de las 6 réplicas no pasen la sonda de readiness (07-01): están Running pero no Ready.
Replicas totales: 6
Replicas Ready: 2
Replicas no Ready: 4
PDB: maxUnavailable: 1 -> minimo 5 disponibles.
Disponibles actuales: 2. Ya estamos POR DEBAJO del minimo.
ALLOWED DISRUPTIONS: 0.Ahora quieres desalojar uno de los pods rotos —precisamente porque está roto y quieres que se recree en otro nodo— y el PDB no te deja. El PDB está protegiendo pods que no están sirviendo tráfico.
Es una situación circular y absurda: el sistema está roto, y el mecanismo de protección impide arreglarlo.
La solución
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-reservas
namespace: rutas-norte-pro
spec:
maxUnavailable: 25%
selector:
matchLabels:
app: api-reservas
# Permite desalojar SIEMPRE los pods que no estan Ready, sin consumir
# presupuesto de interrupcion.
unhealthyPodEvictionPolicy: AlwaysAllowLos dos valores:
| Valor | Comportamiento |
|---|---|
IfHealthyBudget (defecto) |
Los pods no sanos solo se pueden desalojar si el PDB tiene presupuesto disponible |
AlwaysAllow |
Los pods no sanos (que no están Ready) se pueden desalojar siempre |
Con AlwaysAllow, el razonamiento es directo: un pod que no está Ready no está sirviendo tráfico, así que desalojarlo no reduce la disponibilidad. Al contrario: permite que se recree, posiblemente en un nodo sano.
Cuándo usarlo
| Situación | Recomendación |
|---|---|
Aplicaciones sin estado (api-reservas, tienda-web, workers) |
AlwaysAllow. Siempre |
| Aplicaciones con quórum (etcd, Kafka, Zookeeper) | IfHealthyBudget (el defecto). Un pod no Ready puede estar recuperándose y seguir contando para el quórum |
| Bases de datos con réplicas | Depende del sistema; generalmente IfHealthyBudget |
La distinción es importante: en un sistema de quórum, un miembro que no responde a la sonda puede seguir participando en el consenso. Desalojarlo alegremente puede perder el quórum. En una aplicación sin estado, un pod no Ready es puro lastre.
Para Rutas Norte, AlwaysAllow en todos los componentes sin estado. Evita el atasco circular y no tiene contrapartidas.
- Los PDB de Rutas Norte, componente a componente
Escribamos los manifiestos definitivos, con la justificación de cada decisión.
api-reservas
# k8s/entornos/pro/pdb-api-reservas.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-reservas
namespace: rutas-norte-pro
labels:
app: api-reservas
app.kubernetes.io/part-of: rutas-norte
spec:
# PORCENTAJE, no numero absoluto. api-reservas tiene KEDA/HPA (09-01, 09-04)
# y oscila entre 4 y 30 replicas a lo largo del dia. Un minAvailable
# absoluto quedaria obsoleto en cuanto el HPA se moviera, y con el valle
# nocturno se convertiria en un PDB IMPOSIBLE que bloquearia el
# mantenimiento y la reduccion de nodos.
#
# Con 25 %:
# 4 replicas -> permite desalojar 1 (suelo(4 x 0,25) = 1)
# 12 replicas -> permite desalojar 3
# 30 replicas -> permite desalojar 7
# La PROPORCION de servicio protegida es constante: siempre el 75 %.
maxUnavailable: 25%
selector:
matchLabels:
app: api-reservas
# Los pods que no pasan la readiness no estan sirviendo trafico: desalojarlos
# no reduce la disponibilidad y evita el atasco circular de un despliegue
# defectuoso que impide su propia reparacion.
unhealthyPodEvictionPolicy: AlwaysAllowtienda-web
# k8s/entornos/pro/pdb-tienda-web.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: tienda-web
namespace: rutas-norte-pro
labels:
app: tienda-web
app.kubernetes.io/part-of: rutas-norte
spec:
# 34 %, mas permisivo que api-reservas. Razon: tienda-web es nginx sirviendo
# estatico. Arranca en ~1 segundo y no tiene estado ni cache que calentar,
# asi que una replica desalojada vuelve casi al instante en otro nodo.
# Permitir mas desalojos simultaneos acelera el mantenimiento sin riesgo real.
#
# Con 3 replicas -> permite desalojar 1 (suelo(3 x 0,34) = 1)
# Con 15 replicas -> permite desalojar 5
maxUnavailable: 34%
selector:
matchLabels:
app: tienda-web
unhealthyPodEvictionPolicy: AlwaysAllowworker-notificaciones
# k8s/entornos/pro/pdb-worker-notificaciones.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: worker-notificaciones
namespace: rutas-norte-pro
labels:
app: worker-notificaciones
app.kubernetes.io/part-of: rutas-norte
spec:
# 50 %, el mas permisivo de la plataforma. Justificacion:
#
# 1. NADIE ESPERA. Un correo de confirmacion enviado 30 segundos mas tarde
# no tiene consecuencia alguna para el usuario.
# 2. LA COLA ES EL AMORTIGUADOR. Si se desaloja la mitad de los workers, el
# trabajo no se pierde: se acumula en la cola y se procesa despues.
# El mensaje en vuelo se reentrega automaticamente.
# 3. KEDA REACCIONA. Si la cola crece porque hay menos workers, KEDA (09-04)
# escalara para compensar.
#
# Con escalado a CERO (minReplicaCount: 0) hay un matiz: cuando hay 0
# replicas, el PDB no cubre ningun pod y ALLOWED DISRUPTIONS sera 0, pero
# eso es irrelevante porque no hay nada que desalojar.
maxUnavailable: 50%
selector:
matchLabels:
app: worker-notificaciones
unhealthyPodEvictionPolicy: AlwaysAllowredis-cache
# k8s/entornos/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:
# Anotacion para excluir este PDB de la alerta de "PDB imposible":
# su ALLOWED DISRUPTIONS es 0 de forma DELIBERADA.
rutasnorte.example/pdb-bloqueo-intencionado: >
redis-cache es un StatefulSet de una replica. Desalojarlo vacia la cache
y descarga toda la carga de consultas de disponibilidad sobre
postgres-reservas. Requiere intervencion manual y ventana de mantenimiento.
spec:
# minAvailable: 1 con UNA replica = CERO desalojos permitidos.
# Es DELIBERADO. Consecuencias asumidas:
# - El Cluster Autoscaler no retirara el nodo que aloje a redis-cache.
# - kubectl drain sobre ese nodo se quedara esperando.
# - El mantenimiento de ese nodo requiere decision humana explicita.
#
# Es un compromiso ACEPTADO: preferimos que el mantenimiento sea manual a
# que un mecanismo automatico vacie la cache en el peor momento posible.
minAvailable: 1
selector:
matchLabels:
app: redis-cache
# IfHealthyBudget (el defecto). No ponemos AlwaysAllow: si redis-cache no
# esta Ready, puede estar cargando datos o recuperandose. Desalojarlo
# empeoraria las cosas.
unhealthyPodEvictionPolicy: IfHealthyBudgetpostgres-reservas
# k8s/entornos/pro/pdb-postgres-reservas.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: postgres-reservas
namespace: rutas-norte-pro
labels:
app: postgres-reservas
app.kubernetes.io/part-of: rutas-norte
annotations:
rutasnorte.example/pdb-bloqueo-intencionado: >
postgres-reservas es la base de datos primaria de la plataforma con una
unica replica. NINGUN mecanismo automatico debe moverla. El mantenimiento
del nodo que la aloje requiere una ventana planificada, con parada
controlada de la aplicacion y copia de seguridad previa. Ver 05-06.
La solucion definitiva es un operador de PostgreSQL: ver 06-07.
spec:
# CERO desalojos. Es la proteccion mas fuerte posible, y es intencionada.
#
# Un desalojo automatico de postgres-reservas significaria:
# - Corte de TODAS las conexiones de api-reservas.
# - Perdida de la cache de paginas de PostgreSQL (minutos de latencia mala).
# - Riesgo de inconsistencia si hay transacciones en vuelo.
# - La plataforma entera caida durante el arranque.
#
# Guarda datos personales de clientes: cualquier riesgo es inaceptable.
minAvailable: 1
selector:
matchLabels:
app: postgres-reservas
unhealthyPodEvictionPolicy: IfHealthyBudgetTabla resumen
| Componente | PDB | Desalojos con réplicas mín. | unhealthyPodEvictionPolicy |
Justificación |
|---|---|---|---|---|
api-reservas |
maxUnavailable: 25% |
1 de 4 | AlwaysAllow |
Con HPA: porcentaje obligatorio. 75 % de servicio garantizado |
tienda-web |
maxUnavailable: 34% |
1 de 3 | AlwaysAllow |
Arranca en 1 s; más permisivo acelera el mantenimiento |
worker-notificaciones |
maxUnavailable: 50% |
1 de 2 | AlwaysAllow |
La cola amortigua; nadie espera |
redis-cache |
minAvailable: 1 |
0 | IfHealthyBudget |
Una réplica; vaciar la caché es caro. Bloqueo deliberado |
postgres-reservas |
minAvailable: 1 |
0 | IfHealthyBudget |
Base de datos primaria. Bloqueo deliberado |
informes-ocupacion |
Ninguno | — | — | Es un CronJob. Se protege con safe-to-evict: false (09-03) |
Nótese la última fila: los Jobs no llevan PDB. No tiene sentido: un Job que se desaloja se reinicia y pierde el trabajo. Su protección es la anotación cluster-autoscaler.kubernetes.io/safe-to-evict: "false" que vimos en 09-03.
topologySpreadConstraints a fondo
topologySpreadConstraints a fondoLos PDB protegen de las interrupciones voluntarias. Para las involuntarias, la protección es repartir las réplicas entre dominios de fallo independientes. Y ahí entra el campo que mencionamos en 06-05 y prometimos desarrollar aquí.
El problema que resuelve
Sin ninguna restricción, el planificador de Kubernetes coloca los pods donde caben, optimizando el uso de recursos. Nada le impide poner las cuatro réplicas de api-reservas en el mismo nodo, si es donde hay hueco.
nodo-1 (zona A): api-reservas x4, tienda-web x2
nodo-2 (zona A): postgres-reservas
nodo-3 (zona B): redis-cache, worker x2
nodo-4 (zona B): (casi vacio)
Se cae nodo-1 -> CERO replicas de api-reservas. Plataforma caida.
Se cae la zona A -> CERO replicas de api-reservas Y la base de datos.topologySpreadConstraints le dice al planificador: «reparte estos pods de forma equilibrada entre estos dominios».
Los campos
spec:
template:
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api-reservas
minDomains: 3
nodeAffinityPolicy: Honor
nodeTaintsPolicy: Honor
matchLabelKeys:
- pod-template-hashtopologyKey — la etiqueta de nodo que define el dominio. Los valores estándar:
| Etiqueta | Dominio | Protege de |
|---|---|---|
kubernetes.io/hostname |
Un nodo | Fallo de un servidor |
topology.kubernetes.io/zone |
Una zona de disponibilidad | Fallo de un centro de datos |
topology.kubernetes.io/region |
Una región geográfica | Catástrofe regional |
Etiquetas propias (rack, bastidor) |
Lo que tú definas | Fallo de un bastidor o un conmutador |
maxSkew — la diferencia máxima permitida entre el dominio con más pods y el que menos. Es el corazón del mecanismo.
Con maxSkew: 1, la diferencia entre cualquier par de dominios no puede superar 1. Es el reparto más equilibrado posible.
whenUnsatisfiable — qué hacer si no se puede cumplir:
| Valor | Comportamiento | Cuándo usarlo |
|---|---|---|
DoNotSchedule |
El pod se queda Pending antes que violar la restricción |
Distribución obligatoria |
ScheduleAnyway |
Se coloca igualmente, pero el planificador prefiere cumplirla | Distribución preferida |
Esta es la decisión más consecuente del bloque, y la desarrollamos en el apartado 11.
labelSelector — qué pods se cuentan para calcular el sesgo. Normalmente los del propio Deployment.
minDomains — el número mínimo de dominios que deben existir. Sin él, hay un agujero sutil:
Sin minDomains, con 3 replicas y solo 1 zona con nodos disponibles:
Zona A: 3 pods. Zona B: no hay nodos. Zona C: no hay nodos.
Dominios EXISTENTES: 1
Sesgo: 3 - 3 = 0 -> la restriccion SE CUMPLE.
Resultado: las 3 replicas en la misma zona. NO es lo que querias.
Con minDomains: 3:
El planificador considera que hay 3 dominios, dos de ellos con 0 pods.
Sesgo: 3 - 0 = 3 > maxSkew 1 -> NO se cumple.
Con DoNotSchedule: los pods quedan Pending hasta que haya nodos en otras zonas.minDomains solo tiene efecto con whenUnsatisfiable: DoNotSchedule. Es la garantía de que el reparto entre zonas es real y no una ilusión.
nodeAffinityPolicy y nodeTaintsPolicy — si tener en cuenta las afinidades y taints al calcular los dominios:
| Valor | Comportamiento |
|---|---|
Honor (defecto) |
Solo cuenta los nodos donde el pod podría ir (respetando afinidad y taints) |
Ignore |
Cuenta todos los nodos |
El valor por defecto Honor es casi siempre lo correcto. Si api-reservas tiene afinidad hacia el grupo de nodos generales, no queremos que el nodo de datos (con su taint) cuente como un dominio vacío que nunca se podrá llenar.
matchLabelKeys — el campo más sutil y el que resuelve un problema real durante los despliegues.
matchLabelKeys y el problema de las actualizaciones
Durante una actualización progresiva (02-04) coexisten pods de la versión vieja y de la nueva. Sin matchLabelKeys, el cálculo del sesgo los cuenta todos juntos:
Actualizacion de api-reservas de v1.14 a v1.15. 6 replicas.
Estado intermedio:
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
Sesgo calculado sobre TODOS: 3 - 1 = 2 > maxSkew 1
-> El planificador NO puede colocar mas pods v1.15 en la zona A.
-> La actualizacion se atasca o se distribuye mal.Con matchLabelKeys: ["pod-template-hash"], el planificador cuenta solo los pods de la misma versión (el pod-template-hash es una etiqueta que el controlador de Deployments añade automáticamente y que identifica el ReplicaSet):
Con matchLabelKeys: ["pod-template-hash"]:
Para colocar un pod v1.15, solo cuenta los pods v1.15:
zona A: 1, zona B: 0, zona C: 0
Sesgo: 1 - 0 = 1 <= maxSkew 1 -> se puede colocar en B o C.
La nueva version se distribuye correctamente por su cuenta.Regla práctica: incluye siempre matchLabelKeys: ["pod-template-hash"] en las restricciones de un Deployment. Evita atascos durante los despliegues y no tiene contrapartida.
El cálculo del sesgo, paso a paso
Practiquemos con un caso concreto. Tres zonas, maxSkew: 1, 7 réplicas de api-reservas.
Colocacion de la replica 1:
A: 0, B: 0, C: 0. Cualquier zona vale. -> A
Estado: A:1, B:0, C:0
Colocacion de la replica 2:
Si va a A: A:2, B:0, C:0. Sesgo = 2-0 = 2 > 1. NO PERMITIDO.
Si va a B: A:1, B:1, C:0. Sesgo = 1-0 = 1 <= 1. PERMITIDO.
-> B (o C)
Estado: A:1, B:1, C:0
Colocacion de la replica 3:
Solo C mantiene el sesgo en 1 o menos. -> C
Estado: A:1, B:1, C:1 (sesgo 0)
Replicas 4, 5, 6: se reparten una por zona.
Estado: A:2, B:2, C:2 (sesgo 0)
Colocacion de la replica 7:
Cualquier zona: sesgo pasaria a 1. Permitido en las tres.
-> A (arbitrario)
Estado FINAL: A:3, B:2, C:2 (sesgo 1)Resultado: 3-2-2. El reparto más equilibrado posible con 7 réplicas en 3 zonas.
Y ahora la comprobación de disponibilidad:
Se cae la zona A: quedan 4 de 7 replicas (57 %).
Se cae la zona B: quedan 5 de 7 replicas (71 %).
Sin topologySpreadConstraints, en el peor caso las 7 podrian estar en una zona:
Se cae esa zona: quedan 0 de 7 (0 %). Plataforma caida.
topologySpreadConstraints frente a podAntiAffinity
topologySpreadConstraints frente a podAntiAffinityEn 06-05 vimos la antiafinidad entre pods para separar réplicas. Ahora tenemos dos herramientas que parecen hacer lo mismo. No son equivalentes, y conviene saber cuándo usar cada una.
La antiafinidad, recordada
affinity:
podAntiAffinity:
# Version DURA: no se puede violar
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: api-reservas
topologyKey: kubernetes.io/hostnameSignifica: «no coloques este pod en un nodo donde ya haya otro pod con app: api-reservas».
Es una restricción binaria: o hay un pod del mismo tipo en el dominio, o no lo hay. No hay noción de equilibrio.
La diferencia fundamental
podAntiAffinity (requerida) |
topologySpreadConstraints |
|
|---|---|---|
| Semántica | «Como mucho uno por dominio» | «Reparte equitativamente entre dominios» |
| Granularidad | Binaria: hay o no hay | Numérica: controla la diferencia |
| Con más réplicas que dominios | Los sobrantes quedan Pending para siempre |
Se reparten equilibradamente |
| Coste computacional | Alto: O(n²) en clústeres grandes | Bajo |
| Versión «preferida» | preferredDuringScheduling con weight |
whenUnsatisfiable: ScheduleAnyway |
| Multi-nivel | Difícil de expresar | Natural: varias restricciones a la vez |
| Reequilibrado tras un fallo | No hay | Tampoco, pero el reparto inicial es mejor |
El caso que lo decide
api-reservas escala de 4 a 30 réplicas y el clúster tiene 9 nodos.
Con podAntiAffinity requerida sobre hostname:
Regla: como mucho 1 replica de api-reservas por nodo.
Nodos: 9.
Replicas maximas colocables: 9.
El HPA pide 30 replicas.
Se colocan 9. Las otras 21 quedan en Pending PARA SIEMPRE.
El Cluster Autoscaler ve pods pendientes y arranca nodos... y con cada nodo
nuevo cabe UNA replica mas. Para 30 replicas harian falta 30 NODOS.
Coste: absurdo.Con topologySpreadConstraints:
Regla: maxSkew 1 entre nodos.
Nodos: 9. Replicas: 30.
Reparto: 30 / 9 = 3,33
Resultado: 3 nodos con 4 replicas, 6 nodos con 3 replicas.
Sesgo: 4 - 3 = 1. Cumple.
Las 30 replicas se colocan. Ningun Pending.Esta es la razón por la que topologySpreadConstraints es la herramienta correcta para cargas con autoescalado. La antiafinidad requerida y el HPA son incompatibles en la práctica.
Cuándo sigue siendo mejor la antiafinidad
| Caso | Por qué |
|---|---|
| Componentes con pocas réplicas fijas y separación estricta | Etcd, un plano de control: 3 réplicas, una por nodo, sin excepción |
| Antiafinidad entre aplicaciones distintas | «api-reservas no debe compartir nodo con informes-ocupacion»: eso topologySpread no lo expresa |
Reglas de coubicación (podAffinity) |
«Pon este pod cerca de la caché»: no tiene equivalente en topologySpread |
Ese segundo caso merece un ejemplo, porque es un uso legítimo y frecuente:
# api-reservas NO debe compartir nodo con el CronJob de informes, que consume
# 4 nucleos de golpe y estrangularia a la API durante 40 minutos.
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: informes-ocupacion # OTRA aplicacion, no la misma
topologyKey: kubernetes.io/hostnameLa combinación recomendada
Para api-reservas en Rutas Norte usamos las dos, cada una para lo suyo:
spec:
template:
spec:
# 1. Reparto equilibrado entre zonas y nodos (disponibilidad)
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api-reservas
matchLabelKeys: ["pod-template-hash"]
- maxSkew: 2
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: api-reservas
matchLabelKeys: ["pod-template-hash"]
# 2. Antiafinidad con OTRA aplicacion (aislamiento de recursos)
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: informes-ocupacion
topologyKey: kubernetes.io/hostname
- Repartir Rutas Norte entre zonas y nodos
Apliquémoslo a la plataforma, con los cálculos de sesgo.
api-reservas: dos niveles de distribución
# k8s/base/deployment-api-reservas.yaml (fragmento)
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-reservas
namespace: rutas-norte-pro
spec:
# Sin replicas: lo gobierna KEDA/HPA (09-01, 09-04)
selector:
matchLabels:
app: api-reservas
template:
metadata:
labels:
app: api-reservas
app.kubernetes.io/part-of: rutas-norte
spec:
topologySpreadConstraints:
# NIVEL 1: entre ZONAS DE DISPONIBILIDAD. Restriccion DURA.
#
# Este es el dominio de fallo grande: si se cae una zona entera, hay
# que garantizar que sobrevivan replicas en las otras dos. Es una
# garantia que NO estamos dispuestos a negociar, asi que DoNotSchedule:
# preferimos un pod Pending a un reparto desequilibrado.
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api-reservas
# 3 zonas siempre, aunque una no tenga nodos ahora mismo. Sin esto,
# si el Cluster Autoscaler solo tiene nodos en 2 zonas, el reparto
# "equilibrado" entre esas 2 cumpliria la restriccion y perderiamos
# la garantia de las 3 zonas.
minDomains: 3
matchLabelKeys: ["pod-template-hash"]
# NIVEL 2: entre NODOS. Restriccion BLANDA.
#
# Repartir entre nodos es deseable pero NO al precio de dejar pods
# Pending. Durante el puente de mayo, con 30 replicas y el Cluster
# Autoscaler arrancando nodos, un DoNotSchedule aqui bloquearia el
# escalado justo cuando mas falta hace.
#
# maxSkew 2 (no 1): con 30 replicas y 9 nodos, exigir maxSkew 1 seria
# innecesariamente rigido. Un 4-3-3-4-3-3-4-3-3 es perfectamente sano.
- maxSkew: 2
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: api-reservas
matchLabelKeys: ["pod-template-hash"]
containers:
- name: api
image: registry.rutasnorte.example/api-reservas:1.14.2
resources:
requests: {cpu: 412m, memory: 800Mi}
limits: {cpu: "1", memory: 1600Mi}La asimetría entre los dos niveles es la decisión clave, y merece explicarse bien:
| Nivel | whenUnsatisfiable |
Razonamiento |
|---|---|---|
| Zona | DoNotSchedule |
La caída de una zona es un evento catastrófico y plausible. Un pod Pending es preferible a perder la garantía |
| Nodo | ScheduleAnyway |
La caída de un nodo es frecuente pero de impacto limitado. Bloquear el escalado sería peor que un reparto imperfecto |
El cálculo con 4 réplicas (valle)
3 zonas, maxSkew 1, minDomains 3.
Reparto: 4 / 3 = 1,33
Resultado: A:2, B:1, C:1. Sesgo = 2-1 = 1. Cumple.
Caida de la zona A: quedan 2 de 4 (50 %).
Caida de la zona B o C: quedan 3 de 4 (75 %).
Con maxUnavailable: 25% en el PDB, 4 replicas permiten 1 desalojo.
La caida de una zona (2 replicas) es INVOLUNTARIA: el PDB no aplica.
Sobreviven 2 replicas -> el servicio sigue, degradado.
KEDA/HPA detectara la carga por replica duplicada y escalara.El cálculo con 30 réplicas (pico del puente)
Nivel ZONA (maxSkew 1, DoNotSchedule):
30 / 3 = 10 exactas.
Resultado: A:10, B:10, C:10. Sesgo 0. Perfecto.
Nivel NODO (maxSkew 2, ScheduleAnyway), con 9 nodos (3 por zona):
Dentro de cada zona, 10 replicas entre 3 nodos: 4-3-3.
Sesgo global entre nodos: 4 - 3 = 1 <= 2. Cumple.
Reparto final:
zona A: nodo-a1:4, nodo-a2:3, nodo-a3:3
zona B: nodo-b1:4, nodo-b2:3, nodo-b3:3
zona C: nodo-c1:4, nodo-c2:3, nodo-c3:3
Caida de un NODO: se pierden 3 o 4 replicas de 30 (10-13 %). Imperceptible.
Caida de una ZONA: se pierden 10 de 30 (33 %). El servicio aguanta con 20.tienda-web
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: tienda-web
minDomains: 3
matchLabelKeys: ["pod-template-hash"]
- maxSkew: 1
topologyKey: kubernetes.io/hostname
# maxSkew 1 y ScheduleAnyway: con solo 3-15 replicas, un reparto
# de 1 por nodo es alcanzable y deseable. Pero seguimos sin
# bloquear el escalado.
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: tienda-web
matchLabelKeys: ["pod-template-hash"]Con 3 réplicas y minDomains: 3, el reparto es exactamente una por zona. Es el mínimo aceptable para la puerta de entrada de la plataforma.
postgres-reservas: el caso especial
Con una sola réplica, topologySpreadConstraints no aporta nada: no hay nada que repartir. Lo que sí importa es dónde vive:
# k8s/base/statefulset-postgres-reservas.yaml (fragmento)
spec:
template:
spec:
# Vive en el grupo de nodos de datos (09-03), con su taint y su afinidad.
tolerations:
- key: rol
operator: Equal
value: datos
effect: NoSchedule
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: rol
operator: In
values: ["datos"]
# NO compartir nodo con redis-cache: si se cae ese nodo, no queremos
# perder la base de datos Y la cache a la vez, porque el arranque de
# la base de datos con la cache vacia es el peor escenario posible.
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: redis-cache
topologyKey: kubernetes.io/hostnameY una consideración crucial que conecta con el módulo 5: el volumen persistente de postgres-reservas está anclado a una zona. Un PV de disco de bloque de un proveedor de nube solo es accesible desde su zona. Eso significa que:
- El pod solo puede planificarse en nodos de esa zona.
- Si se cae la zona, no se puede recuperar en otra zona sin restaurar de una copia de seguridad (05-06).
Esto no es un problema de Kubernetes, es una propiedad del almacenamiento de bloques. Y es la razón fundamental por la que la alta disponibilidad de una base de datos no se resuelve con objetos de Kubernetes (apartado 12).
- Alta disponibilidad en las demás capas
Hasta ahora hemos hablado de las aplicaciones. Pero la alta disponibilidad es una propiedad del sistema completo, y hay tres capas más que conviene revisar.
El plano de control y el quórum de etcd
Recordemos 01-02: el plano de control lo forman el servidor de API, el planificador, el gestor de controladores y etcd, la base de datos que guarda todo el estado del clúster.
etcd usa el algoritmo de consenso Raft, que necesita mayoría estricta para operar. De ahí sale la regla del número impar:
| Miembros | Quórum (mayoría) | Fallos tolerados |
|---|---|---|
| 1 | 1 | 0 |
| 2 | 2 | 0 |
| 3 | 2 | 1 |
| 4 | 3 | 1 |
| 5 | 3 | 2 |
| 6 | 4 | 2 |
| 7 | 4 | 3 |
Fíjate en que 4 miembros toleran los mismos fallos que 3, y 6 los mismos que 5. Un número par añade coste y latencia de consenso sin añadir tolerancia. De ahí que la configuración estándar sea 3 o 5 miembros, nunca par.
Qué pasa al perder el quórum:
Cluster de 3 miembros de etcd. Se caen 2.
Quorum necesario: 2. Miembros vivos: 1. NO HAY QUORUM.
Consecuencias:
- etcd pasa a SOLO LECTURA. No acepta escrituras.
- El servidor de API no puede persistir cambios.
- kubectl apply falla. No se pueden crear, modificar ni borrar objetos.
- El HPA no puede cambiar replicas.
- El Cluster Autoscaler no puede hacer nada.
PERO, y esto es lo importante:
- Los pods que YA estaban corriendo SIGUEN CORRIENDO.
- Los kubelets siguen ejecutando sus contenedores.
- Los Services siguen enrutando (kube-proxy tiene su estado local).
- LA PLATAFORMA SIGUE ATENDIENDO TRAFICO.El plano de control caído no tumba las aplicaciones en marcha. Es una propiedad de diseño muy valiosa de Kubernetes: el plano de datos sobrevive al plano de control. Lo que se pierde es la capacidad de cambiar cosas: escalar, desplegar, recuperarse de un fallo de pod.
Distribución recomendada del plano de control:
3 nodos de plano de control, uno por zona de disponibilidad:
zona A: control-1 (etcd, api-server, scheduler, controller-manager)
zona B: control-2
zona C: control-3
Caida de una zona: quedan 2 de 3. Quorum = 2. SE MANTIENE.En Kubernetes gestionado (10-06) esto lo hace el proveedor y no lo ves. Es uno de los argumentos más fuertes a favor de un clúster gestionado: la alta disponibilidad del plano de control es difícil de hacer bien y no aporta valor diferencial.
El controlador de Ingress
El controlador de Ingress (04-04) es la puerta de entrada de todo el tráfico externo. Si cae, la plataforma es inalcanzable aunque todos los pods estén sanos.
# k8s/entornos/pro/deployment-ingress-nginx.yaml (fragmento)
apiVersion: apps/v1
kind: Deployment
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
spec:
replicas: 4 # Nunca 1. Nunca 2 en el mismo dominio de fallo.
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 es duro
labelSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: ingress-nginx
namespace: ingress-nginx
spec:
minAvailable: 2 # Numero absoluto: las replicas del Ingress son FIJAS
selector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
unhealthyPodEvictionPolicy: AlwaysAllowDos decisiones que se apartan de lo anterior:
DoNotScheduletambién entre nodos. El Ingress no tiene HPA (sus réplicas son fijas), así que no hay riesgo de bloquear un escalado. Y dos réplicas del Ingress en el mismo nodo son dos réplicas que se pierden juntas.minAvailable: 2en números absolutos. Con réplicas fijas, el número absoluto es más explícito y más fácil de razonar que un porcentaje.
CoreDNS
CoreDNS (04-03) resuelve todos los nombres del clúster. Si falla, api-reservas no encuentra postgres-reservas, worker-notificaciones no encuentra RabbitMQ, y todo se cae con errores de DNS que despistan muchísimo.
Es un componente que casi nadie revisa y que es un punto único de fallo silencioso.
# Fragmento del Deployment de CoreDNS en 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-criticalY su PDB:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: coredns
namespace: kube-system
spec:
maxUnavailable: 1
selector:
matchLabels:
k8s-app: kube-dns
unhealthyPodEvictionPolicy: AlwaysAllowUn consejo adicional muy rentable: NodeLocal DNSCache, un DaemonSet que pone una caché de DNS en cada nodo. Reduce la latencia de resolución, descarga a CoreDNS, y hace que un fallo temporal de CoreDNS no afecte a las consultas cacheadas. Volveremos a él en 09-06 al hablar del ndots: 5.
postgres-reservas: la disponibilidad la da el operador
Y llegamos al caso más importante, el que resume la lección de todo el módulo sobre las cargas con estado.
Kubernetes no puede hacer que PostgreSQL sea de alta disponibilidad. Lo que Kubernetes ofrece:
- Reiniciar el pod si el proceso muere.
- Reprogramarlo en otro nodo si el nodo falla (si el volumen es accesible).
- Un PDB que impide desalojos automáticos.
Lo que Kubernetes no ofrece:
| Capacidad necesaria | Por qué Kubernetes no la da |
|---|---|
| Replicación de datos entre instancias | Es un protocolo de PostgreSQL, no de Kubernetes |
| Promoción automática de una réplica a primaria | Requiere conocer el estado de la replicación |
| Redirigir las escrituras a la nueva primaria | Requiere cambiar el Service en el momento justo |
| Evitar el «cerebro dividido» (dos primarias) | Requiere una lógica de consenso específica |
| Garantizar que no se pierden transacciones confirmadas | Requiere entender el WAL de PostgreSQL |
Todo eso lo aporta un operador (06-07): CloudNativePG, Zalando Postgres Operator, Crunchy PGO. Un operador de PostgreSQL:
flowchart TD
OP[Operador de PostgreSQL<br/>controlador con conocimiento del dominio]
P[(postgres-primario<br/>zona A<br/>ESCRITURAS)]
R1[(postgres-replica-1<br/>zona B<br/>lecturas)]
R2[(postgres-replica-2<br/>zona C<br/>lecturas)]
SVCW[Service: postgres-rw<br/>apunta al PRIMARIO]
SVCR[Service: postgres-ro<br/>apunta a las REPLICAS]
OP -->|vigila salud y replicacion| P
OP -->|vigila| R1
OP -->|vigila| R2
P -->|replicacion en streaming| R1
P -->|replicacion en streaming| R2
OP -.->|si el primario cae:<br/>PROMOCIONA y reapunta| SVCW
SVCW --> P
SVCR --> R1
SVCR --> R2
style OP fill:#cde,stroke:#369,stroke-width:2px
style P fill:#cfc,stroke:#393
Con un operador, el modelo cambia por completo:
# Ejemplo con CloudNativePG
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgres-reservas
namespace: rutas-norte-pro
spec:
instances: 3 # 1 primaria + 2 replicas
primaryUpdateStrategy: unsupervised
storage:
size: 100Gi
storageClass: rutasnorte-rapida # De 05-04
# El operador reparte las instancias entre zonas automaticamente
affinity:
enablePodAntiAffinity: true
topologyKey: topology.kubernetes.io/zone
podAntiAffinityType: required
postgresql:
parameters:
max_connections: "200"
shared_buffers: "1GB"
monitoring:
enablePodMonitor: true # Se integra con Prometheus (07-03)Y el operador crea, mantiene y gestiona:
- Tres StatefulSets con sus PVC en tres zonas.
- Los Services
postgres-reservas-rw(primario) ypostgres-reservas-ro(réplicas). - La replicación en streaming entre instancias.
- La conmutación automática: si el primario cae, promociona una réplica y reapunta el Service
-rwen unos segundos. - Sus propios PDB, correctamente configurados para su modelo de quórum.
- Copias de seguridad continuas al almacén de objetos.
La lección: para las cargas con estado, la alta disponibilidad se delega en un operador que entienda el sistema. Los PDB y topologySpreadConstraints son herramientas genéricas de Kubernetes; un operador aporta el conocimiento específico del dominio que ninguna herramienta genérica puede tener.
Aplicar esto en Rutas Norte es el trabajo pendiente que se aborda en el módulo 11 (11-02).
- El procedimiento de mantenimiento de un nodo
Reunamos todo en el procedimiento operativo completo. Esto es lo que se ejecuta el martes por la mañana.
El flujo
flowchart TD
A[1. Verificar el estado previo<br/>PDB, replicas, alertas] --> B{Todos los PDB<br/>permiten desalojos?}
B -->|No| C[Corregir los PDB imposibles<br/>o planificar intervencion manual]
C --> A
B -->|Si| D[2. cordon: dejar de aceptar pods nuevos]
D --> E[3. drain: desalojar los pods existentes<br/>respetando los PDB]
E --> F{Se completo?}
F -->|No, bloqueado| G[Diagnosticar: que PDB lo impide?]
G --> H[Escalar replicas o corregir]
H --> E
F -->|Si| I[4. Verificar: nodo vacio<br/>y servicio sano]
I --> J[5. MANTENIMIENTO<br/>actualizar kernel, kubelet, reiniciar]
J --> K[6. Verificar que el nodo vuelve Ready]
K --> L[7. uncordon: volver a aceptar pods]
L --> M[8. Verificar la distribucion<br/>y descordonar el siguiente]
style D fill:#ffd,stroke:#c90
style E fill:#ffd,stroke:#c90
style L fill:#cfc,stroke:#393
Paso 1: verificación previa
NAMESPACE NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS
rutas-norte-pro api-reservas N/A 25% 1
rutas-norte-pro tienda-web N/A 34% 1
rutas-norte-pro postgres-reservas 1 N/A 0 <- ATENCION
rutas-norte-pro redis-cache 1 N/A 0 <- ATENCION
rutas-norte-pro worker-notificaciones N/A 50% 1
ingress-nginx ingress-nginx 2 N/A 2
kube-system coredns N/A 1 1Los dos ceros son los deliberados del apartado 8. Hay que saber en qué nodos están:
NAME READY STATUS NODE ZONA
postgres-reservas-0 2/2 Running nodo-datos-1 eu-west-1a
redis-cache-0 1/1 Running nodo-datos-2 eu-west-1bEsos dos nodos requieren un procedimiento distinto, con ventana de mantenimiento planificada. Los demás se pueden drenar sin más.
# ¿Qué hay en el nodo que vamos a drenar?
kubectl get pods --all-namespaces -o wide --field-selector spec.nodeName=nodo-a2NAMESPACE NAME READY STATUS AGE
rutas-norte-pro api-reservas-7c9d4f8b6d-2mkjp 2/2 Running 4h
rutas-norte-pro api-reservas-7c9d4f8b6d-9nvtr 2/2 Running 4h
rutas-norte-pro tienda-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 <- DaemonSetTres pods de aplicación y tres DaemonSets. Los DaemonSets no se drenan (uno por nodo por definición).
# ¿Hay alguna alerta activa? No se hace mantenimiento con incidentes abiertos.
kubectl get events --all-namespaces --field-selector type=Warning \
--sort-by=.lastTimestamp | tail -20Paso 2: cordon
Qué hace exactamente: pone spec.unschedulable: true en el nodo y le añade el taint node.kubernetes.io/unschedulable:NoSchedule.
NAME STATUS ROLES AGE VERSION
nodo-a1 Ready <none> 12d v1.30.2
nodo-a2 Ready,SchedulingDisabled <none> 12d v1.30.2
nodo-a3 Ready <none> 12d v1.30.2cordon NO mueve nada. Los pods que ya están siguen corriendo. Solo impide que lleguen nuevos. Es una operación segura y reversible al instante.
Paso 3: drain
kubectl drain nodo-a2 \
--ignore-daemonsets \
--delete-emptydir-data \
--grace-period=60 \
--timeout=300sLas opciones, una a una:
| Opción | Qué hace | ¿Necesaria? |
|---|---|---|
--ignore-daemonsets |
No intenta desalojar pods de DaemonSet | Sí, siempre. Sin ella drain falla |
--delete-emptydir-data |
Acepta perder el contenido de los emptyDir |
Sí si hay pods con emptyDir (caché de nginx) |
--grace-period=60 |
Segundos para que el pod termine limpiamente | Recomendado: da tiempo al apagado ordenado |
--timeout=300s |
Abandona tras 5 minutos | Muy recomendable. Evita esperar para siempre |
--force |
Borra también pods sin controlador | Peligroso: esos pods no vuelven |
--disable-eviction |
Ignora los PDB usando borrado directo | Solo emergencias |
--pod-selector |
Drena solo los pods que coincidan | Útil para drenajes parciales |
--dry-run=client |
Muestra qué haría sin hacerlo | Excelente para verificar antes |
Salida esperada:
node/nodo-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-reservas-7c9d4f8b6d-2mkjp
evicting pod rutas-norte-pro/tienda-web-6f7d9c4b58-4kjnx
evicting pod rutas-norte-pro/api-reservas-7c9d4f8b6d-9nvtr
error when evicting pods/"api-reservas-7c9d4f8b6d-9nvtr" -n "rutas-norte-pro"
(will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
pod/tienda-web-6f7d9c4b58-4kjnx evicted
pod/api-reservas-7c9d4f8b6d-2mkjp evicted
evicting pod rutas-norte-pro/api-reservas-7c9d4f8b6d-9nvtr
pod/api-reservas-7c9d4f8b6d-9nvtr evicted
node/nodo-a2 drainedFíjate en el segundo pod de api-reservas: el primer intento falló con el mensaje del PDB, esperó 5 segundos, y al reintentar tuvo éxito. ¿Qué pasó en esos 5 segundos? El primer pod desalojado se recreó en otro nodo y pasó a Ready, liberando presupuesto de interrupción.
Este es el PDB funcionando exactamente como debe: no ha impedido el mantenimiento, lo ha serializado para que nunca haya más de un pod caído a la vez.
Paso 4: verificar
# El nodo debe estar vacio de pods de aplicacion
kubectl get pods --all-namespaces -o wide --field-selector spec.nodeName=nodo-a2NAMESPACE 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 12dSolo DaemonSets. Correcto.
# Y el SERVICIO debe estar sano: esto es lo que de verdad importa
kubectl get pods -n rutas-norte-pro -l app=api-reservas
kubectl get pdb -n rutas-norte-proComprobación en Grafana (07-04): latencia p95 estable, tasa de error en cero. Si el drenaje ha degradado el servicio, no continúes con el siguiente nodo.
Paso 5: el mantenimiento
# Ejemplo: actualizar el kubelet en un cluster con kubeadm (10-02)
ssh nodo-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
exitPaso 6: verificar el nodo
Ready con la versión nueva. Sigue acordonado, que es lo correcto.
Paso 7: uncordon
Nunca olvides este paso. Un nodo que queda acordonado es capacidad pagada e invisible: el Cluster Autoscaler puede arrancar nodos nuevos mientras este está vacío y disponible pero rechazando pods.
Un detalle que sorprende: uncordon no reequilibra los pods existentes. El nodo está disponible, pero los pods que se movieron no vuelven. Kubernetes no reprograma pods en marcha. La redistribución ocurre de forma natural con el siguiente escalado o despliegue.
Si quieres forzar el reequilibrado, la herramienta es el descheduler, un componente que desaloja pods que violan las políticas de distribución para que se replanifiquen mejor. Respeta los PDB, naturalmente.
Paso 8: el siguiente nodo
# Verificar la distribucion antes de continuar
kubectl get pods -n rutas-norte-pro -l app=api-reservas -o wide \
| awk '{print $7}' | sort | uniq -cTras el drenaje, nodo-a2 está vacío y sus pods se repartieron. La distribución sigue siendo razonable entre zonas (3 en A, 3 en B... aunque falta C: habría que revisar si minDomains se está cumpliendo).
Regla de oro: un nodo cada vez, con verificación entre cada uno. Drenar dos nodos en paralelo puede violar los PDB de formas que no anticipaste.
Qué se rompe si no hay PDB
Vale la pena hacer el ejercicio mental. Sin ningún PDB:
node/nodo-a2 cordoned
evicting pod rutas-norte-pro/api-reservas-7c9d4f8b6d-2mkjp
evicting pod rutas-norte-pro/api-reservas-7c9d4f8b6d-9nvtr
evicting pod rutas-norte-pro/tienda-web-6f7d9c4b58-4kjnx
pod/api-reservas-7c9d4f8b6d-2mkjp evicted
pod/api-reservas-7c9d4f8b6d-9nvtr evicted
pod/tienda-web-6f7d9c4b58-4kjnx evicted
node/nodo-a2 drainedInstantáneo. Los tres pods desalojados a la vez. Y en ese instante:
api-reservas tenia 4 replicas: 2 en nodo-a2, 1 en nodo-a1, 1 en nodo-b1.
Tras el drenaje: 2 replicas vivas, 2 arrancando (25-30 segundos).
Durante 30 segundos: LA MITAD de la capacidad de la API.
Cada replica viva recibe el DOBLE de carga.
Latencia p95: de 180 ms a 900 ms.
Algunas peticiones: timeout.
Y si hubieran sido 3 de las 4 replicas en ese nodo: 25 % de capacidad.
La replica superviviente se satura y cae. CAIDA TOTAL.El PDB convierte ese desastre en un desalojo serializado de 90 segundos sin impacto perceptible. Es la diferencia entre un mantenimiento y un incidente.
- Interacción con el Cluster Autoscaler y los despliegues
Dos interacciones que conviene tener presentes.
Con el Cluster Autoscaler (09-03)
El CA usa la API de desalojo, así que respeta los PDB. Consecuencias:
Consecuencia 1: un PDB imposible ancla el nodo permanentemente. Ya lo vimos en 09-03: los logs dicen pdb-blocked y el nodo nunca se retira. Con postgres-reservas es intencionado; con un PDB mal configurado, es dinero tirado.
Consecuencia 2: la reducción es más lenta con PDB. El CA tiene que desalojar los pods de uno en uno, esperando a que se recreen. Un nodo con 8 pods de api-reservas y maxUnavailable: 25% con 12 réplicas totales tarda varios minutos en vaciarse. Es correcto y deseable, pero hay que saberlo para no pensar que el CA está roto.
Consecuencia 3: hay que revisar los PDB cuando cambian los maxReplicas. Si subes maxReplicas de api-reservas de 30 a 60, el maxUnavailable: 25% permitirá 15 desalojos simultáneos en el pico. ¿Es aceptable perder 15 réplicas a la vez? Probablemente sí, pero es una decisión que merece pensarse.
Y una interacción especialmente sutil: el colchón de sobreaprovisionamiento no debe llevar PDB. Los pods de relleno de 09-03 están para ser desalojados; un PDB los protegería y rompería todo el mecanismo. Si acaso, un PDB explícitamente permisivo:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: relleno-capacidad
namespace: rutas-norte-pro
spec:
# 100 % de indisponibilidad permitida: estos pods estan PARA ser desalojados.
# Documenta la intencion mejor que la ausencia de PDB.
maxUnavailable: 100%
selector:
matchLabels:
app: relleno-capacidadCon los despliegues (02-04)
El controlador de Deployments no usa la API de desalojo durante una actualización progresiva: borra pods directamente y aplica su propio maxUnavailable. Los PDB no intervienen.
Esto significa que necesitas configurar los dos coherentemente:
# En el Deployment: gobierna la ACTUALIZACION
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 25% # Coherente con el PDB
maxSurge: 25%
---
# En el PDB: gobierna los DESALOJOS (drain, CA, VPA)
spec:
maxUnavailable: 25%Si el Deployment permite el 50 % de indisponibilidad y el PDB solo el 25 %, tendrás una incoherencia: durante un despliegue el servicio bajará al 50 %, pero un drenaje solo podrá bajarlo al 75 %. No es un error, pero es una inconsistencia de criterio que conviene resolver.
Recomendación: usa el mismo valor en ambos, para que el nivel de servicio garantizado sea el mismo pase lo que pase.
Y un escenario a evitar: un despliegue y un mantenimiento a la vez. Durante una actualización, la mitad de los pods están arrancando y no cuentan como disponibles. Si además drenas un nodo, el PDB bloqueará el drenaje (correctamente) y el mantenimiento se atascará. Nunca hagas mantenimiento durante un despliegue, y viceversa.
- Una prueba de caos sencilla
Todo lo anterior es teoría hasta que se comprueba. Vamos a matar un nodo y ver qué pasa.
Preparación en minikube
minikube start -p rutas-norte --nodes=4 --cpus=2 --memory=4096
# Etiquetar los nodos con zonas ficticias
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-bDespliegue con distribución y 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: 80NAME 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-bDistribución perfecta: 3 en zona-a, 3 en zona-b. El maxSkew: 1 entre nodos también se cumple: 1-2-2-1.
Experimento 1: el mantenimiento planificado (voluntario)
# Terminal 1: generar trafico continuo y contar errores
kubectl -n rutas-norte-dev run cliente --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'Resultado esperado en el terminal 1:
Cero errores. El PDB serializó el desalojo de los 2 pods de ese nodo, y el Service (04-02) sacó de sus endpoints cada pod en cuanto dejó de estar Ready.
Esto es lo que se busca: el mantenimiento es invisible para el usuario.
Experimento 2: la caída de un nodo (involuntaria)
# Simular un fallo de hardware: apagar el nodo bruscamente
minikube -p rutas-norte node stop rutas-norte-m03Observemos la secuencia completa:
NAME STATUS ROLES AGE
rutas-norte-m03 Ready <none> 1h
rutas-norte-m03 NotReady <none> 1h <- a los ~40 segundosNAME READY STATUS NODE
caos-api-7d9c5f8b6b-9hbcx 1/1 Running rutas-norte-m03
caos-api-7d9c5f8b6b-tv6ws 1/1 Running rutas-norte-m03
...
(pasan ~5 minutos)
...
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-m04Los tiempos son la enseñanza del experimento:
| Momento | Evento | Tiempo acumulado |
|---|---|---|
| t=0 | El nodo se apaga | 0 |
| t=40 s | El plano de control lo marca NotReady (node-monitor-grace-period) |
40 s |
| t=40 s | El Service saca sus pods de los endpoints. El tráfico deja de ir ahí | 40 s |
| t=5 min | Los taints NoExecute desalojan los pods (tolerationSeconds: 300) |
5 min |
| t=5 min | El ReplicaSet crea los pods sustitutos | 5 min |
| t=5 min 20 s | Los nuevos pods están Ready |
5 min 20 s |
El punto crítico son esos primeros 40 segundos: el nodo está muerto pero Kubernetes todavía no lo sabe, y el Service sigue enviándole tráfico. Durante esos 40 segundos, un tercio de las peticiones fallan.
En el terminal 1 verías algo así:
OK=1204 ERR=0
OK=1210 ERR=3 <- empiezan los errores
OK=1218 ERR=11
...
OK=1301 ERR=68 <- ~40 segundos de errores
OK=1409 ERR=68 <- se detienen: el Service ya excluyo el nodo muerto
OK=1520 ERR=68Esta es la diferencia fundamental entre las interrupciones voluntarias y las involuntarias, medida con un cronómetro:
Voluntaria (drain) |
Involuntaria (nodo muerto) | |
|---|---|---|
| El Service excluye los pods | Inmediatamente (readiness falla al recibir SIGTERM) | A los ~40 segundos |
| Errores para el usuario | Cero | ~40 segundos de errores |
| Protección posible | PDB | Ninguna; solo limitar el daño repartiendo |
Qué hacer con esos 40 segundos
No se pueden eliminar, pero se pueden mitigar:
| Medida | Efecto |
|---|---|
Repartir bien las réplicas (topologySpreadConstraints) |
Con 2 de 6 réplicas en el nodo muerto, solo falla el 33 % de las peticiones, no el 100 % |
| Reintentos en el cliente | tienda-web reintenta con otro pod; el usuario no ve el error |
| Malla de servicio (08-04) | Detección de fallos y reintentos automáticos, con expulsión de instancias que fallan |
Bajar node-monitor-grace-period |
Detecta antes... pero produce falsos positivos con latencia de red |
La última opción es tentadora y casi siempre mala idea: bajar el umbral de detección hace que una latencia temporal de red se interprete como un nodo caído, y desaloja pods sanos. Los 40 segundos por defecto son un compromiso bien elegido.
Limpieza
Escalando la prueba de caos
Este experimento manual es el nivel más básico. En un entorno serio, se automatiza con herramientas de ingeniería del caos (Chaos Mesh, LitmusChaos) que permiten:
- Matar pods aleatorios de forma continua.
- Inyectar latencia de red entre servicios.
- Simular la caída de una zona completa.
- Llenar el disco de un nodo.
- Ejecutarlo periódicamente y alertar si el sistema no aguanta.
Pero el experimento manual es donde hay que empezar, porque es el que enseña los tiempos reales. Hazlo una vez en rutas-norte-pre antes del puente de mayo y sabrás exactamente qué esperar.
Errores Comunes y Consejos
Error 1: PDB con minAvailable absoluto sobre una carga con HPA. El error más frecuente de este módulo. El HPA baja las réplicas en el valle, el minAvailable se vuelve inalcanzable, y el PDB pasa a bloquear todo el mantenimiento y toda la reducción de nodos. Con autoescalado, siempre porcentajes.
Error 2: creer que un PDB protege de la caída de un nodo. No lo hace. Los PDB solo intervienen en desalojos que pasan por la API. Un nodo que se apaga no pide permiso. La protección contra fallos es la distribución topológica.
Error 3: podAntiAffinity requerida sobre hostname en una carga con HPA. Limita las réplicas al número de nodos. El HPA pedirá 30, se colocarán 9, y 21 quedarán Pending para siempre mientras el Cluster Autoscaler arranca nodos que solo caben de uno en uno. Usa topologySpreadConstraints.
Error 4: DoNotSchedule entre nodos en una carga con autoescalado. Durante el puente de mayo, con el CA arrancando nodos, una restricción dura entre nodos bloquea el escalado justo cuando más falta hace. DoNotSchedule para zonas, ScheduleAnyway para nodos.
Error 5: olvidar matchLabelKeys: ["pod-template-hash"]. Sin él, el cálculo del sesgo mezcla las versiones vieja y nueva durante un despliegue, y la actualización se atasca o se distribuye mal. Inclúyelo siempre en Deployments.
Error 6: olvidar el uncordon. Un nodo acordonado y olvidado es capacidad pagada e inutilizable. El síntoma —pods Pending mientras hay nodos aparentemente sanos— es especialmente desconcertante. Ponlo en la lista de comprobación del mantenimiento.
Error 7: --disable-eviction para «desatascar» un drenaje. Salta los PDB usando borrado directo. Puede dejar un servicio sin ninguna réplica. Si un drenaje está bloqueado, la respuesta correcta es diagnosticar por qué, no desactivar la protección.
Error 8: no poner PDB a los componentes de infraestructura. CoreDNS, el controlador de Ingress y Prometheus son tan críticos como la aplicación. Un drenaje que se lleve las dos réplicas de CoreDNS tumba la resolución de nombres de todo el clúster, con síntomas que nadie relaciona con el mantenimiento.
Consejo 1: revisa ALLOWED DISRUPTIONS antes de cada mantenimiento. Un kubectl get pdb --all-namespaces de dos segundos te dice si el mantenimiento va a fluir o a atascarse. Es la comprobación previa más rentable que existe.
Consejo 2: anota los PDB deliberadamente bloqueantes. postgres-reservas y redis-cache tienen ALLOWED DISRUPTIONS: 0 a propósito. Una anotación que lo explique evita que alguien los «arregle» sin entender por qué están así, y permite excluirlos de las alertas.
Consejo 3: alerta sobre PDB bloqueados no deliberados.
# PDB sin desalojos permitidos, excluyendo los intencionados
kube_poddisruptionbudget_status_pod_disruptions_allowed == 0
unless on(namespace, poddisruptionbudget)
kube_poddisruptionbudget_annotations{annotation_rutasnorte_example_pdb_bloqueo_intencionado!=""}Consejo 4: verifica la distribución real, no la configurada.
# Replicas por zona
kubectl get pods -n rutas-norte-pro -l app=api-reservas \
-o custom-columns=NODO:.spec.nodeName --no-headers | \
while read n; do kubectl get node "$n" -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/zone}{"\n"}'; done | \
sort | uniq -cUna restricción bien escrita que no se cumple (porque no hay nodos en una zona, por ejemplo) es peor que ninguna: da falsa sensación de seguridad.
Consejo 5: ensaya el mantenimiento en rutas-norte-pre. Drena un nodo de preproducción con tráfico de prueba y mide el impacto. Si en pre hay errores, en pro los habrá multiplicados.
Consejo 6: haz la prueba de caos al menos una vez. Matar un nodo y cronometrar la recuperación enseña más que cualquier documentación. Los 40 segundos de detección son un número que hay que haber visto con los propios ojos.
Consejo 7: coordina el maxUnavailable del Deployment con el del PDB. Usa el mismo valor para que el nivel de servicio garantizado sea el mismo durante un despliegue y durante un mantenimiento.
Ejercicios
Ejercicio 1: diagnosticar un mantenimiento bloqueado
Es martes por la mañana y hay que actualizar los nodos. El drenaje del primer nodo lleva 20 minutos atascado:
node/nodo-b1 already cordoned
Warning: ignoring DaemonSet-managed Pods: kube-system/fluentd-2wxkp, kube-system/falco-x7kqp
evicting pod monitorizacion/prometheus-server-0
evicting pod rutas-norte-pro/api-reservas-7c9d4f8b6d-mn2vp
evicting pod rutas-norte-pro/worker-notificaciones-5f8d9c-4kjnx
pod/worker-notificaciones-5f8d9c-4kjnx evicted
error when evicting pods/"prometheus-server-0" -n "monitorizacion"
(will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
error when evicting pods/"api-reservas-7c9d4f8b6d-mn2vp" -n "rutas-norte-pro"
(will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.Estado de los PDB:
NAMESPACE NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS
rutas-norte-pro api-reservas 8 N/A 0
rutas-norte-pro tienda-web N/A 34% 1
rutas-norte-pro worker-notificaciones N/A 50% 1
monitorizacion prometheus 1 N/A 0Estado de los pods:
NAMESPACE NAME READY STATUS NODE
rutas-norte-pro api-reservas-7c9d4f8b6d-2mkjp 2/2 Running nodo-a1
rutas-norte-pro api-reservas-7c9d4f8b6d-9nvtr 2/2 Running nodo-a2
rutas-norte-pro api-reservas-7c9d4f8b6d-mn2vp 2/2 Running nodo-b1
rutas-norte-pro api-reservas-7c9d4f8b6d-tv6ws 2/2 Running nodo-b2
rutas-norte-pro api-reservas-7c9d4f8b6d-x2klm 1/2 Running nodo-a1
monitorizacion prometheus-server-0 1/1 Running nodo-b1Analiza los dos bloqueos por separado. Para cada uno: ¿es legítimo o es un error? ¿Cuál es la causa exacta? ¿Cómo lo desbloqueas ahora y cómo evitas que vuelva a pasar? Presta especial atención al pod x2klm.
Ejercicio 2: calcular la distribución y el impacto
api-reservas tiene esta configuración:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
minDomains: 3
labelSelector:
matchLabels:
app: api-reservas
- maxSkew: 2
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: api-reservasEl clúster tiene 9 nodos: 3 en eu-west-1a, 3 en eu-west-1b, 3 en eu-west-1c.
Calcula para 11 réplicas y para 26 réplicas:
(a) El reparto entre zonas y el sesgo resultante. (b) El reparto entre nodos dentro de cada zona. (c) Cuántas réplicas se pierden si cae un nodo y qué porcentaje de capacidad queda. (d) Cuántas réplicas se pierden si cae una zona entera y qué porcentaje queda. (e) Cuántos desalojos simultáneos permite el PDB en cada caso. (f) Si al drenar un nodo con 26 réplicas desplegadas se puede vaciar ese nodo de una sola vez, o el PDB lo serializa.
Además: ¿qué pasaría con 2 réplicas y minDomains: 3? ¿Y con 2 réplicas si una zona se queda sin nodos disponibles?
Ejercicio 3: diseñar la alta disponibilidad de un componente nuevo
Rutas Norte incorpora pasarela-pagos, el componente que habla con el banco para cobrar las reservas. Perfil:
- Es un servicio HTTP sin estado, en el camino crítico: si falla, no se puede comprar.
- Cada transacción tarda entre 2 y 8 segundos (esperando al banco), y no se puede interrumpir a mitad: una transacción cortada puede dejar un cobro sin reserva asociada.
- El banco limita a 50 conexiones simultáneas desde la IP de Rutas Norte.
- Tráfico: 5-40 transacciones por segundo en un día normal; hasta 300 en el pico del puente de mayo.
- Arranque: 20 segundos (hay que establecer la sesión TLS mutua con el banco y validar certificados).
- El equipo de cumplimiento exige que cada transacción quede registrada antes de responder al usuario.
- Debe funcionar aunque caiga una zona de disponibilidad completa.
Diseña la estrategia completa de alta disponibilidad: número de réplicas, PDB, topologySpreadConstraints, terminationGracePeriodSeconds, y cualquier otro mecanismo que consideres necesario. Justifica cada decisión y escribe los manifiestos. Presta atención especial al límite de 50 conexiones del banco y a la exigencia de no interrumpir transacciones.
Soluciones
Solución 1
Hay tres hallazgos, no dos.
Bloqueo 1: api-reservas con minAvailable: 8 — ERROR de configuración.
Replicas totales de api-reservas: 5
- 4 con READY 2/2 (sanas)
- 1 con READY 1/2 (el pod x2klm: un contenedor no esta listo)
Replicas DISPONIBLES (Ready): 4
PDB exige: minAvailable: 8
4 < 8 -> YA estamos por debajo del minimo.
ALLOWED DISRUPTIONS: 0. Y seguira siendo 0 pase lo que pase.Es el caso de libro del PDB imposible con HPA: alguien puso minAvailable: 8 cuando había 12 réplicas durante una punta, y ahora el HPA ha bajado a 5. El PDB no se ajustó porque es un número absoluto.
Tercer hallazgo: el pod x2klm está en 1/2.
Este es el detalle que hay que cazar. api-reservas tiene dos contenedores (la API y el exportador de métricas). 1/2 significa que uno de ellos no está Ready.
kubectl describe pod api-reservas-7c9d4f8b6d-x2klm -n rutas-norte-pro
kubectl logs api-reservas-7c9d4f8b6d-x2klm -n rutas-norte-pro -c exportador-metricasUn pod que no está completamente Ready no cuenta como disponible para el PDB. Así que aunque corrigiéramos el PDB, ese pod es un problema aparte que hay que investigar: puede ser el síntoma de algo más grave.
Desbloqueo inmediato:
kubectl patch pdb api-reservas -n rutas-norte-pro --type merge \
-p '{"spec":{"minAvailable":null,"maxUnavailable":"25%"}}'Efecto inmediato:
Replicas deseadas: 5
maxUnavailable: 25% -> suelo(5 x 0,25) = 1
Minimo disponible: 5 - 1 = 4
Disponibles actuales: 4
ALLOWED DISRUPTIONS: 0
...Sigue siendo 0, por culpa del pod x2klm que no esta Ready.El parche solo no basta. Hay que resolver también el pod roto. Dos vías:
# Via A: si el pod esta roto sin remedio, borrarlo para que se recree
kubectl delete pod api-reservas-7c9d4f8b6d-x2klm -n rutas-norte-pro
# (kubectl delete NO respeta PDB: aqui nos viene bien)
# Via B: anadir unhealthyPodEvictionPolicy para que los pods no sanos
# no consuman presupuesto
kubectl patch pdb api-reservas -n rutas-norte-pro --type merge \
-p '{"spec":{"unhealthyPodEvictionPolicy":"AlwaysAllow"}}'La vía B es la correcta a largo plazo, y es exactamente el problema que resuelve unhealthyPodEvictionPolicy (apartado 7): un pod no sano estaba bloqueando la reparación del sistema.
Manifiesto corregido:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-reservas
namespace: rutas-norte-pro
annotations:
rutasnorte.example/nota: >
PORCENTAJE obligatorio: api-reservas tiene KEDA/HPA y oscila entre
4 y 30 replicas. Un minAvailable absoluto se vuelve imposible en el
valle y bloquea todo el mantenimiento. Ver 09-05.
spec:
maxUnavailable: 25%
selector:
matchLabels:
app: api-reservas
unhealthyPodEvictionPolicy: AlwaysAllowBloqueo 2: prometheus con minAvailable: 1 y una réplica — LEGÍTIMO pero mal resuelto.
prometheus-server-0: 1 replica (es un StatefulSet).
PDB: minAvailable: 1.
Desalojarla dejaria 0 disponibles.
ALLOWED DISRUPTIONS: 0. Permanente y por diseno.El bloqueo es coherente con la configuración, pero la configuración es discutible. Hay que decidir qué se quiere:
Opción A: aceptar la interrupción. Prometheus con una réplica y almacenamiento local. Perder unos minutos de métricas durante un mantenimiento es molesto pero no crítico: no afecta al servicio a los usuarios.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: prometheus
namespace: monitorizacion
spec:
# Una sola replica: permitimos el desalojo. Perdemos unos minutos de
# metricas durante el mantenimiento, lo cual es aceptable: Prometheus
# no esta en el camino critico de la venta.
maxUnavailable: 1
selector:
matchLabels:
app: prometheusOpción B: dos réplicas. Prometheus en alta disponibilidad se hace con dos instancias independientes que recolectan lo mismo, más Thanos o Mimir para deduplicar. Es más complejo y solo se justifica si las alertas son críticas.
Decisión pragmática: opción A. Prometheus no está en el camino de la venta, y la observabilidad puede tener un hueco de tres minutos durante un mantenimiento planificado.
Nota importante y contraintuitiva: perder Prometheus durante el mantenimiento tiene un efecto secundario del módulo anterior. Si api-reservas escala con KEDA por una métrica de Prometheus (09-04), KEDA entrará en modo fallback mientras Prometheus no esté. Por eso el fallback de 09-04 es tan importante: cubre exactamente este caso.
Procedimiento completo para desatascar:
# 1. Corregir el PDB de api-reservas
kubectl patch pdb api-reservas -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 monitorizacion --type merge \
-p '{"spec":{"minAvailable":null,"maxUnavailable":1}}'
# 3. Verificar
kubectl get pdb --all-namespacesNAMESPACE NAME MAX UNAVAILABLE ALLOWED DISRUPTIONS
rutas-norte-pro api-reservas 25% 1
monitorizacion prometheus 1 1El drenaje, que seguía reintentando en segundo plano, avanza solo.
# 4. Investigar el pod x2klm (no urgente, pero no lo olvides)
kubectl describe pod api-reservas-7c9d4f8b6d-x2klm -n rutas-norte-proPrevención estructural:
- Comprobación previa obligatoria en el procedimiento de mantenimiento:
kubectl get pdb --all-namespaces \
-o custom-columns='NS:.metadata.namespace,NOMBRE:.metadata.name,PERMITIDOS:.status.disruptionsAllowed' \
| awk 'NR==1 || $3=="0"'-
Política de Kyverno (08-03) que rechace PDB con
minAvailableabsoluto en cargas que tengan HPA oScaledObject. -
Alerta permanente:
Con la anotación de exclusión para los bloqueos deliberados (postgres-reservas, redis-cache).
Solución 2
Caso A: 11 réplicas
(a) Reparto entre zonas (maxSkew: 1, DoNotSchedule, 3 zonas):
11 / 3 = 3,67
Reparto mas equilibrado posible:
eu-west-1a: 4
eu-west-1b: 4
eu-west-1c: 3
Sesgo = 4 - 3 = 1 <= maxSkew 1. CUMPLE.(b) Reparto entre nodos (maxSkew: 2, ScheduleAnyway, 3 nodos por zona):
Zona a (4 replicas, 3 nodos): 2-1-1
Zona b (4 replicas, 3 nodos): 2-1-1
Zona c (3 replicas, 3 nodos): 1-1-1
Sesgo global entre nodos: max 2 - min 1 = 1 <= maxSkew 2. CUMPLE.(c) Caída de un nodo:
(d) Caída de una zona:
(e) Desalojos permitidos:
Caso B: 26 réplicas
(a) Reparto entre zonas:
26 / 3 = 8,67
Reparto:
eu-west-1a: 9
eu-west-1b: 9
eu-west-1c: 8
Sesgo = 9 - 8 = 1 <= maxSkew 1. CUMPLE.(b) Reparto entre nodos:
Zona a (9 replicas, 3 nodos): 3-3-3
Zona b (9 replicas, 3 nodos): 3-3-3
Zona c (8 replicas, 3 nodos): 3-3-2
Sesgo global entre nodos: 3 - 2 = 1 <= maxSkew 2. CUMPLE.(c) Caída de un nodo:
(d) Caída de una zona:
(e) Desalojos permitidos:
(f) ¿Se puede vaciar un nodo de una vez con 26 réplicas?
Un nodo tiene 3 replicas de api-reservas.
El PDB permite 6 desalojos simultaneos.
3 <= 6 -> SI, se pueden desalojar las 3 de una vez.
El drenaje NO se serializara por culpa del PDB de api-reservas.
Sera casi instantaneo para este componente.Comparemos con el caso de 11 réplicas:
Con 11 replicas, un nodo tiene 2 replicas y el PDB permite 2 desalojos.
2 <= 2 -> tambien caben de una vez, aunque justo al limite.
Si el nodo tuviera 3 replicas (posible con maxSkew 2), el PDB serializaria:
desaloja 2, espera a que se recreen, desaloja la tercera.Observación importante: el maxUnavailable: 25% escala con las réplicas, así que el mantenimiento se vuelve MÁS rápido con más réplicas, no más lento. Es exactamente lo que se quiere: en el pico hay más capacidad y se puede permitir perder más réplicas simultáneamente.
Caso especial: 2 réplicas con minDomains: 3
minDomains: 3 significa que el planificador considera 3 dominios,
aunque alguno este vacio.
Colocacion de la replica 1: zona a. Estado: a:1, b:0, c:0.
Sesgo = 1 - 0 = 1 <= maxSkew 1. Cumple.
Colocacion de la replica 2:
Si va a la zona a: a:2, b:0, c:0. Sesgo = 2 - 0 = 2 > 1. NO PERMITIDO.
Si va a la zona b: a:1, b:1, c:0. Sesgo = 1 - 0 = 1 <= 1. PERMITIDO.
Resultado: 1 replica en la zona a, 1 en la zona b, 0 en la c.
La zona c queda vacia, y eso ES CORRECTO: con 2 replicas no se pueden
cubrir 3 zonas.Sesgo final: 1. Cumple. La restricción funciona bien con menos réplicas que zonas.
Caso especial: 2 réplicas si una zona se queda sin nodos
Escenario: la zona c pierde todos sus nodos (caida completa).
Nodos disponibles: solo en las zonas a y b.
minDomains: 3 hace que el planificador SIGA CONTANDO 3 dominios.
Estado tras la caida: a:1, b:1, c:0 (la zona c no tiene nodos).
Sesgo = 1 - 0 = 1 <= maxSkew 1. CUMPLE. Nada que hacer.
Pero si el HPA quisiera escalar a 4 replicas:
Colocacion de la replica 3:
Si va a la zona a: a:2, b:1, c:0. Sesgo = 2 - 0 = 2 > 1. NO PERMITIDO.
Si va a la zona b: a:1, b:2, c:0. Sesgo = 2 - 0 = 2 > 1. NO PERMITIDO.
Si va a la zona c: NO HAY NODOS.
-> La replica 3 se queda en PENDING PARA SIEMPRE.Este es el peligro de minDomains combinado con DoNotSchedule. La restricción, pensada para garantizar el reparto, impide escalar cuando una zona no está disponible: exactamente el momento en que más falta hace escalar, porque has perdido un tercio de la capacidad.
Mitigaciones posibles:
| Opción | Efecto | Contrapartida |
|---|---|---|
Quitar minDomains |
Con 2 zonas vivas, el reparto entre ellas cumple | Pierdes la garantía de las 3 zonas en operación normal |
whenUnsatisfiable: ScheduleAnyway en zona |
Nunca bloquea | Pierdes la garantía dura |
Subir maxSkew a 2 |
Con a:2, b:1, c:0, el sesgo es 2 <= 2. Permite | Reparto menos equilibrado |
| Aceptarlo y tener un procedimiento | Ante una caída de zona, se relaja la restricción a mano | Requiere intervención humana bajo presión |
Recomendación para Rutas Norte: maxSkew: 1 con minDomains: 3 y un procedimiento documentado para relajarlo ante una caída de zona:
# PROCEDIMIENTO DE EMERGENCIA: caida de zona
# Relajar la restriccion topologica para permitir escalar en las zonas vivas.
kubectl patch deployment api-reservas -n rutas-norte-pro --type json -p '[
{"op": "replace", "path": "/spec/template/spec/topologySpreadConstraints/0/whenUnsatisfiable",
"value": "ScheduleAnyway"}
]'
# REVERTIR cuando la zona vuelva.Tenerlo escrito de antemano, probado, y en el runbook. Es mucho mejor que improvisarlo a las tres de la madrugada.
Solución 3
Análisis de los requisitos y sus consecuencias:
| Requisito | Consecuencia de diseño |
|---|---|
| Camino crítico, sin estado | Mínimo 3 réplicas, repartidas entre 3 zonas |
| Transacción de 2-8 s no interrumpible | terminationGracePeriodSeconds alto + apagado ordenado |
| 50 conexiones máximas del banco | Límite duro al número de réplicas |
| 5-300 transacciones/s | Autoescalado necesario, pero acotado por el límite del banco |
| Arranque de 20 s | Objetivo de escalado conservador; no escalar a cero |
| Registro antes de responder | El apagado debe esperar a que se escriba el registro |
| Debe sobrevivir a una zona | DoNotSchedule entre zonas, minDomains: 3 |
El límite de 50 conexiones es la restricción dominante, y hay que calcularlo antes que nada.
Paso 1: calcular el techo real de réplicas
El banco permite 50 conexiones simultaneas desde la IP de Rutas Norte.
Si cada replica mantiene un pool de N conexiones:
Replicas maximas = 50 / N
Con N = 5 conexiones por replica: 10 replicas maximas
Con N = 3 conexiones por replica: 16 replicas maximas
Con N = 2 conexiones por replica: 25 replicas maximas
Cuantas transacciones por segundo sostiene una replica?
Cada transaccion tarda 2-8 s, media ~5 s.
Con un pool de N conexiones concurrentes:
transacciones/s = N / 5
Con N = 5: 1 transaccion/s por replica
Con N = 3: 0,6 transacciones/s por replica
Para 300 transacciones/s en el pico:
Con N = 5: 300 replicas. IMPOSIBLE (excede el limite del banco por 30x).
Con N = 3: 500 replicas. Peor.Aquí hay un problema grave que ninguna configuración de Kubernetes resuelve:
Capacidad TEORICA MAXIMA del sistema:
50 conexiones concurrentes / 5 segundos por transaccion = 10 transacciones/s
OBJETIVO DEL PICO: 300 transacciones/s
EL SISTEMA NO PUEDE PROCESAR MAS DE 10 TRANSACCIONES POR SEGUNDO.
El limite es el BANCO, no Kubernetes.Esta es la lección de 09-01 sobre el cuello de botella real, en su forma más pura. Escalar pasarela-pagos a 30 réplicas no aumenta la capacidad ni un ápice: las 30 réplicas se pelearían por las mismas 50 conexiones.
Paso 2: la arquitectura que sí funciona
La solución no es de escalado, es de arquitectura:
1. NEGOCIAR con el banco un limite mayor. 50 conexiones es ridiculo para
300 transacciones/s. Es la accion mas importante y no es tecnica.
2. Mientras tanto: DESACOPLAR con una cola.
- api-reservas encola la peticion de cobro y responde al usuario
"procesando su pago".
- pasarela-pagos consume de la cola a la velocidad que el banco permite.
- El usuario recibe la confirmacion por notificacion push o correo.
Esto convierte una restriccion dura en una latencia aceptable, y encaja
perfectamente con KEDA (09-04): la cola es la senal de escalado natural.
3. Un POOL COMPARTIDO de conexiones al banco, en lugar de un pool por replica.Como el enunciado pide diseñar la alta disponibilidad, asumimos la opción 3 con un número de réplicas acotado.
Paso 3: los manifiestos
# k8s/entornos/pro/deployment-pasarela-pagos.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: pasarela-pagos
namespace: rutas-norte-pro
labels:
app: pasarela-pagos
app.kubernetes.io/part-of: rutas-norte
spec:
# SIN campo replicas: lo gobierna el HPA/KEDA.
selector:
matchLabels:
app: pasarela-pagos
strategy:
type: RollingUpdate
rollingUpdate:
# Coherente con el PDB (ver mas abajo). Nunca perder mas de 1 replica.
maxUnavailable: 1
# maxSurge 1, no mas: cada replica nueva abre conexiones al banco y
# tenemos un limite duro de 50. Un maxSurge alto podria agotarlo
# durante el despliegue.
maxSurge: 1
template:
metadata:
labels:
app: pasarela-pagos
app.kubernetes.io/part-of: rutas-norte
spec:
# PERIODO DE GRACIA DE 90 SEGUNDOS.
#
# Una transaccion tarda hasta 8 segundos y NO se puede interrumpir:
# cortarla podria dejar un cobro sin reserva asociada, que es el peor
# fallo posible en una pasarela de pagos.
#
# 90 s = 8 s (transaccion mas larga) + margen amplio para:
# - vaciar la cola interna de transacciones en curso
# - escribir el registro de cumplimiento de cada una
# - cerrar limpiamente las conexiones TLS con el banco
#
# El proceso DEBE manejar SIGTERM: dejar de aceptar peticiones nuevas,
# terminar las en curso, escribir los registros, y salir.
terminationGracePeriodSeconds: 90
topologySpreadConstraints:
# ZONAS: restriccion DURA. El requisito de sobrevivir a la caida de
# una zona es explicito y no negociable.
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
minDomains: 3
labelSelector:
matchLabels:
app: pasarela-pagos
matchLabelKeys: ["pod-template-hash"]
# NODOS: restriccion DURA tambien, y aqui SI podemos permitirnosla.
# Con un maximo de 9 replicas y 9 nodos, exigir 1 por nodo es
# alcanzable. Y dos replicas de la pasarela en el mismo nodo son
# dos replicas que se pierden juntas: inaceptable en el camino
# critico del pago.
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: pasarela-pagos
matchLabelKeys: ["pod-template-hash"]
containers:
- name: pasarela
image: registry.rutasnorte.example/pasarela-pagos:2.4.1
# PARADA ORDENADA: el hook preStop da margen para que el Service
# saque este pod de sus endpoints ANTES de que empiece a apagarse.
# Sin esto, hay una ventana de 1-2 segundos en la que el pod recibe
# peticiones nuevas mientras ya se esta apagando.
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
resources:
requests: {cpu: 200m, memory: 256Mi}
limits: {cpu: 500m, memory: 512Mi}
env:
# POOL PEQUENO Y EXPLICITO: 5 conexiones por replica.
# Con maxReplicas 9: 9 x 5 = 45 < 50. Deja margen de 5 conexiones
# para el despliegue (maxSurge 1) y para reintentos.
# ESTE NUMERO ES CRITICO: subirlo agota el limite del banco.
- name: BANCO_POOL_MAX
value: "5"
readinessProbe:
httpGet: {path: /preparado, port: 8080}
initialDelaySeconds: 20 # El arranque tarda 20 s (TLS mutuo)
periodSeconds: 5
failureThreshold: 2
livenessProbe:
httpGet: {path: /salud, port: 8080}
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
# STARTUP PROBE: da hasta 60 s para el arranque sin que la
# livenessProbe mate el pod a mitad de la negociacion TLS.
startupProbe:
httpGet: {path: /salud, port: 8080}
periodSeconds: 5
failureThreshold: 12# k8s/entornos/pro/pdb-pasarela-pagos.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: pasarela-pagos
namespace: rutas-norte-pro
labels:
app: pasarela-pagos
app.kubernetes.io/part-of: rutas-norte
spec:
# maxUnavailable: 1 EN NUMERO ABSOLUTO, no en porcentaje.
#
# Es la excepcion a la regla general del apartado 3, y es deliberada:
#
# 1. El rango de replicas es ESTRECHO (3 a 9), no como api-reservas (4 a 30).
# Un porcentaje del 25 % daria: 3 replicas -> 0 desalojos (BLOQUEO), y
# 9 replicas -> 2 desalojos. El caso de 3 replicas seria un PDB imposible.
#
# 2. Cada replica que se cae puede llevarse hasta 5 transacciones en vuelo,
# cada una con dinero real de un cliente. Queremos que los desalojos se
# SERIALICEN siempre, sea cual sea el numero de replicas.
#
# 3. Con el terminationGracePeriodSeconds de 90 s, cada desalojo tarda hasta
# minuto y medio. Serializarlos hace el mantenimiento lento pero seguro,
# que es exactamente el compromiso que queremos en una pasarela de pagos.
maxUnavailable: 1
selector:
matchLabels:
app: pasarela-pagos
# AlwaysAllow: un pod que no pasa la readiness no esta procesando pagos
# (el Service ya no le manda trafico), asi que desalojarlo no pone en
# riesgo ninguna transaccion.
unhealthyPodEvictionPolicy: AlwaysAllow# k8s/entornos/pro/scaledobject-pasarela-pagos.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: pasarela-pagos
namespace: rutas-norte-pro
spec:
scaleTargetRef:
name: pasarela-pagos
pollingInterval: 15
cooldownPeriod: 600
# 3 replicas de suelo: UNA POR ZONA. Es el minimo para sobrevivir a la
# caida de una zona conservando 2 replicas operativas. NUNCA escalar a cero:
# el arranque de 20 s (negociacion TLS mutua con el banco) es inaceptable
# con un usuario en la pantalla de pago.
minReplicaCount: 3
# 9 REPLICAS MAXIMO. ESTE NUMERO NO SALE DEL TRAFICO, SALE DEL BANCO:
# 9 replicas x 5 conexiones = 45 conexiones < 50 del limite.
# Margen de 5 conexiones para el despliegue (maxSurge 1) y reintentos.
#
# ADVERTENCIA CRITICA: subir este numero SIN renegociar el limite con el
# banco NO aumenta la capacidad. Solo hace que las replicas compitan por
# las mismas 50 conexiones y empiecen a recibir errores de conexion
# rechazada. El cuello de botella es el BANCO (ver 09-01).
maxReplicaCount: 9
fallback:
failureThreshold: 3
replicas: 6
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleUp:
# 30 s de estabilizacion (no 0): cada replica nueva tarda 20 s en
# estar lista y abre 5 conexiones al banco. No queremos crear
# replicas a lo loco y agotar el limite de conexiones.
stabilizationWindowSeconds: 30
selectPolicy: Max
policies:
- type: Pods
value: 2
periodSeconds: 60
scaleDown:
# 15 minutos. Cada replica que se apaga tarda hasta 90 s en terminar
# sus transacciones. Bajar despacio es esencial.
stabilizationWindowSeconds: 900
selectPolicy: Min
policies:
- type: Pods
value: 1
periodSeconds: 300
triggers:
# Escalado por CONEXIONES ACTIVAS AL BANCO, no por CPU ni por peticiones.
# Es la metrica que refleja el recurso escaso real.
- type: prometheus
metadata:
serverAddress: http://prometheus-operated.monitorizacion.svc.cluster.local:9090
metricName: pasarela_conexiones_banco_activas
query: |
sum(pasarela_pagos_conexiones_banco_activas{namespace="rutas-norte-pro"})
# Objetivo: 3,5 conexiones activas por replica (de un pool de 5).
# Al 70 % de ocupacion del pool, escalamos.
threshold: "3.5"
activationThreshold: "1"
authenticationRef:
kind: ClusterTriggerAuthentication
name: prometheus-lectura
# Red de seguridad: transacciones encoladas esperando una conexion libre.
- type: prometheus
metadata:
serverAddress: http://prometheus-operated.monitorizacion.svc.cluster.local:9090
metricName: pasarela_transacciones_en_espera
query: |
sum(pasarela_pagos_transacciones_en_espera{namespace="rutas-norte-pro"})
threshold: "3"
activationThreshold: "1"
authenticationRef:
kind: ClusterTriggerAuthentication
name: prometheus-lecturaResumen de decisiones y justificaciones:
| Decisión | Elección | Justificación |
|---|---|---|
minReplicaCount |
3 | Una por zona; sobrevive a la caída de una zona con 2 operativas |
maxReplicaCount |
9 | 9 × 5 = 45 < 50 conexiones del banco. No sale del tráfico |
| PDB | maxUnavailable: 1 absoluto |
Rango estrecho (3-9); un 25 % daría 0 desalojos con 3 réplicas |
topologySpread zona |
DoNotSchedule + minDomains: 3 |
Requisito explícito de sobrevivir a una zona |
topologySpread nodo |
DoNotSchedule, maxSkew: 1 |
Con 9 réplicas y 9 nodos es alcanzable; dos réplicas por nodo es riesgo innecesario |
terminationGracePeriodSeconds |
90 s | 8 s de transacción + registro + cierre TLS, con margen amplio |
preStop |
sleep 10 |
Que el Service saque el pod antes de que empiece a apagarse |
startupProbe |
60 s de margen | El TLS mutuo tarda 20 s; evita que la liveness mate el arranque |
| Métrica de escalado | Conexiones activas al banco | Es el recurso escaso real, no la CPU ni las peticiones |
maxSurge |
1 | Cada réplica extra consume conexiones del límite del banco |
| Escalado a cero | No | 20 s de arranque con un usuario en la pantalla de pago |
La conclusión que hay que llevarse del ejercicio, y que va más allá de la configuración:
La alta disponibilidad de pasarela-pagos está limitada por el banco, no por Kubernetes. Ninguna cantidad de PDB, topologySpreadConstraints o autoescalado permite procesar más de 10 transacciones por segundo con 50 conexiones y 5 segundos por transacción.
Con 300 transacciones por segundo en el pico del puente de mayo, el sistema se saturará irremediablemente. Las acciones que de verdad resuelven el problema son:
- Renegociar el límite con el banco. No es técnica, es comercial, y es la más importante.
- Desacoplar con una cola, convirtiendo una restricción dura en una latencia aceptable comunicada al usuario.
- Un pool compartido en lugar de un pool por réplica, con un componente intermedio que multiplexe.
- Un límite de tasa explícito en
api-reservasque rechace educadamente las peticiones de pago cuando la pasarela está saturada, en lugar de dejar que se acumulen hasta el timeout.
Diseñar la alta disponibilidad de un componente sin entender su cuello de botella real es exactamente el error que 09-01 advertía: escalar la capa web cuando el límite está en otra parte multiplica el coste sin vender un solo billete más.
Conclusión
La alta disponibilidad no es un objeto de Kubernetes que se activa: es una propiedad del sistema que se construye entendiendo qué puede fallar y qué protección existe para cada cosa.
Lo esencial:
- Kubernetes distingue interrupciones voluntarias e involuntarias, y solo puede protegerte de las primeras. Un
kubectl drainpasa por la API y puede negociarse; un servidor que se apaga, no. - El PodDisruptionBudget declara cuánto servicio debe permanecer disponible ante los desalojos. Lo respetan
kubectl drain, el Cluster Autoscaler, el actualizador del VPA y el descheduler; no lo respetankubectl delete podni las actualizaciones progresivas del Deployment. minAvailablehabla de lo que queda;maxUnavailablede lo que se va. Los porcentajes redondean en direcciones opuestas, siempre favoreciendo la disponibilidad. Con HPA o KEDA, siempre porcentajes: un número absoluto se vuelve imposible en el valle y bloquea todo el mantenimiento.- El PDB imposible (
minAvailableigual a las réplicas, o una sola réplica) ancla nodos, bloquea drenajes y le impide al Cluster Autoscaler reducir. A veces es deliberado (postgres-reservas); documéntalo con una anotación para que nadie lo «arregle». unhealthyPodEvictionPolicy: AlwaysAllowrompe el atasco circular de un despliegue defectuoso cuyos pods rotos impiden su propia reparación. Úsalo en todo lo que no tenga quórum.topologySpreadConstraintsreparte equitativamente entre dominios de fallo. Frente a lapodAntiAffinityde 06-05, gana claramente en cargas con autoescalado: la antiafinidad requerida limita las réplicas al número de nodos, y con 30 réplicas y 9 nodos deja 21 podsPendingpara siempre.- La asimetría entre niveles es la decisión clave:
DoNotScheduleentre zonas (la caída de una zona es catastrófica) yScheduleAnywayentre nodos (bloquear el escalado sería peor). YmatchLabelKeys: ["pod-template-hash"]siempre, para que los despliegues no se atasquen. - La alta disponibilidad de las demás capas importa igual: tres o cinco miembros de etcd (nunca par) repartidos entre zonas, varias réplicas del controlador de Ingress y de CoreDNS con sus PDB, y para
postgres-reservas, un operador que sepa promocionar una réplica: eso Kubernetes no lo hace y no puede hacerlo. - El procedimiento de mantenimiento es
cordon→drain→ actualizar →uncordon, con verificación de PDB antes y de servicio entre cada nodo. Sin PDB, un drenaje se lleva todas las réplicas de un nodo en un instante; con PDB, las serializa y el usuario no nota nada. - La prueba de caos revela lo que la teoría esconde: un drenaje produce cero errores; un nodo que muere produce unos 40 segundos de errores mientras el plano de control se entera. Esa diferencia, medida con cronómetro, es la definición práctica de voluntario frente a involuntario.
Rutas Norte tiene ahora la plataforma repartida entre tres zonas con garantías duras, PDB coherentes con el autoescalado, y un procedimiento de mantenimiento que no interrumpe la venta. Las réplicas crecen cuando hace falta, tienen el tamaño correcto, los nodos aparecen cuando no caben, el escalado se anticipa a los eventos conocidos, y nada de eso se rompe cuando alguien actualiza un nodo o cuando se cae un servidor.
Y sin embargo, queda una pregunta que no hemos hecho en todo el módulo: ¿es que hace falta tanto?
Hemos dado por buenos los números que veníamos arrastrando: que una réplica de api-reservas sostiene 85 peticiones por segundo, que hace falta escalar a 30 en el pico, que el requests.cpu debe ser 412m. Pero nadie ha preguntado por qué una réplica solo aguanta 85 peticiones por segundo, ni si podría aguantar 200 con los mismos recursos. Nadie ha mirado si el pool de conexiones a PostgreSQL está bien dimensionado, si las consultas tienen los índices que necesitan, si redis-cache se está usando de verdad, o si el estrangulamiento de CPU que vimos en el módulo 3 aparece mucho antes de lo que creíamos.
Escalar es multiplicar la ineficiencia por el número de réplicas. Si cada réplica desperdicia la mitad de su capacidad, treinta réplicas desperdician quince.
En la próxima lección, Ajuste de Rendimiento, cerramos el módulo mirando hacia dentro: la metodología de medir, encontrar el cuello de botella real y cambiar una sola cosa cada vez; pruebas de carga con k6 simulando el puente de mayo y cómo leer sus resultados de verdad; el ajuste de la capa de aplicación, que es donde casi siempre está el problema; cómo se traduce un limit de CPU en cuota del kernel y por qué el estrangulamiento aparece mucho antes del 100 %; el arranque rápido como requisito del autoescalado; el ajuste del clúster, incluido el efecto del ndots: 5 que dejamos pendiente en 04-03; y un presupuesto de latencia que convierte el objetivo de negocio en objetivos por componente.
Curso de Kubernetes
Módulo 1: Introducción a Kubernetes
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
