PodDisruptionBudget y drenado de nodos
Tarde o temprano hay que tocar un nodo: actualizar el kernel, subir la versión de Kubernetes, cambiar el tipo de máquina. Eso significa vaciarlo de Pods, y ahí aparece la pregunta que separa un mantenimiento tranquilo de una llamada a las tres de la mañana: ¿cuántas réplicas de mi aplicación pueden desaparecer a la vez sin que el servicio se caiga?
Kubernetes distingue dos tipos de interrupción:
- Involuntarias: se cae un nodo, se queda sin memoria, arde el rack. Nadie las pide y nadie las puede negociar.
- Voluntarias: tú drenas un nodo, el autoescalador lo retira, un operador reemplaza Pods. Estas sí se pueden negociar, y el PodDisruptionBudget es el contrato de esa negociación.
Trabaja desde la pestaña dev-machine.
Paso 1: Una aplicación repartida y su presupuesto
Crea web.yaml, con el Deployment y el PDB juntos:
cat << 'EOF' > web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: web
containers:
- name: nginx
image: ghcr.io/iximiuz/labs/nginx:alpine
ports:
- containerPort: 80
resources:
requests:
cpu: 20m
memory: 32Mi
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-pdb
spec:
minAvailable: 3
selector:
matchLabels:
app: web
EOF
El YAML, explicado con preguntas y respuestas
¿Qué hacen los topologySpreadConstraints?
Reparten los Pods de forma equilibrada a lo largo de una dimensión de la topología. topologyKey: kubernetes.io/hostname significa "reparte por nodo"; maxSkew: 1 significa "que entre el nodo más cargado y el menos cargado no haya más de un Pod de diferencia". Con 3 réplicas y los 2 nodos de este playground, el reparto más equilibrado posible es 2-1, y eso es exactamente lo que verás: maxSkew: 1 no exige un Pod por nodo, exige que la diferencia no pase de uno. Con 3 nodos sí saldría uno por nodo, que es el caso que dibuja el mapa del libro. Cambiando la clave a topology.kubernetes.io/zone repartes por zona de disponibilidad, que es como se sobrevive a la caída de un centro de datos.
¿Y whenUnsatisfiable?
Qué hacer si la restricción no se puede cumplir. DoNotSchedule la convierte en un requisito duro (el Pod se queda en Pending antes que desequilibrar el reparto). ScheduleAnyway la convierte en una preferencia: el scheduler intenta repartir, pero si no puede, coloca el Pod igualmente. Aquí usamos la versión blanda a propósito, porque en un rato vamos a acordonar un nodo y no queremos que la tercera réplica se quede sin sitio.
¿Por qué esto en vez de una pod anti-affinity?
Porque la anti-affinity es binaria (o hay un Pod en el nodo, o no lo hay) y topologySpreadConstraints permite decir "reparte, pero admite cierto desequilibrio". Es la herramienta moderna para lo que antes se hacía a martillazos con anti-affinity.
¿Qué declara exactamente minAvailable: 3?
Que en todo momento debe haber al menos 3 Pods disponibles de los que casan con el selector. Como el Deployment tiene exactamente 3 réplicas, acabamos de firmar un contrato imposible de cumplir si alguien quiere retirar aunque sea uno.
¿Es lo mismo que maxUnavailable?
Son las dos caras de la misma moneda, y se declara una o la otra. minAvailable: 2 y maxUnavailable: 1 significan lo mismo con 3 réplicas, pero se comportan distinto si alguien escala. La forma más robusta es el porcentaje (minAvailable: 50%), porque sobrevive a los cambios de tamaño del Deployment.
¿A quién protege un PDB de verdad?
Solo de las interrupciones voluntarias. Un PDB no impide que un nodo se muera de golpe ni que un Pod sea desalojado por falta de memoria. No es alta disponibilidad: es un freno de mano para las operaciones que se hacen a propósito.
Aplícalo y mira las dos cosas que importan:
kubectl apply -f web.yaml
kubectl get pods -l app=web -o wide
kubectl get pdb web-pdb
Tres Pods repartidos entre los dos nodos: dos en uno y uno en el otro, que es el reparto más equilibrado posible aquí. Y en el PDB, fíjate en la columna ALLOWED DISRUPTIONS: vale 0. Acabas de declarar, sin querer, que este servicio no admite ningún mantenimiento.
Paso 2: El drenado que se bloquea
Elige un nodo que tenga Pods de web (cualquiera de los workers vale) y drénalo, como harías antes de apagarlo:
kubectl get pods -l app=web -o wide
kubectl drain node-02 --ignore-daemonsets --delete-emptydir-data --timeout=30s
¿Qué hace drain exactamente? Dos cosas seguidas: primero acordona el nodo (cordon, lo marca como no programable, para que no lleguen Pods nuevos) y luego desaloja (evict) los Pods que ya tiene. Y ese desalojo no es un delete: es una petición a la API que pasa por el filtro de los PodDisruptionBudgets.
Por eso el comando falla, con un mensaje que ya sabes leer:
Cannot evict pod as it would violate the pod's disruption budget.
El nodo se ha quedado acordonado (compruébalo con kubectl get nodes, dirá SchedulingDisabled), pero el Pod sigue ahí. El presupuesto ha hecho su trabajo: ha preferido bloquear tu mantenimiento antes que arriesgar el servicio. Un PDB mal calculado no rompe la aplicación, rompe tus operaciones, y por eso es un fallo que se descubre siempre en el peor momento.
Las banderas, ya que estás:
--ignore-daemonsets: sin ella,drainse niega a empezar. Los Pods de un DaemonSet no se pueden desalojar de verdad, porque el controlador los recrearía en el acto en ese mismo nodo (recuerda la lección: van atados a su nodo).--delete-emptydir-data: confirma que aceptas perder los datos de los volúmenesemptyDirde los Pods desalojados. Kubernetes te obliga a decirlo en voz alta.
Paso 3: Un presupuesto que permite trabajar
Corrige el contrato. Con 3 réplicas, exigir 2 disponibles deja margen para retirar una:
kubectl patch pdb web-pdb --type='merge' -p='{"spec":{"minAvailable":2}}'
kubectl get pdb web-pdb
La columna ALLOWED DISRUPTIONS ya vale 1. Ese número es el que hay que mirar antes de cualquier mantenimiento, y el que hay que vigilar en producción: un PDB con 0 disrupciones permitidas de forma permanente es una bomba de relojería para el día que el clúster necesite mover ese Pod.
Repite el drenado:
kubectl drain node-02 --ignore-daemonsets --delete-emptydir-data
kubectl get pods -l app=web -o wide
kubectl get deployment web
Ahora sí. El Pod se desaloja, el ReplicaSet crea el sustituto en otro nodo (por eso el reparto era una preferencia y no una exigencia), y el Deployment vuelve a 3/3. Servicio intacto, nodo vacío y listo para el mantenimiento.
Paso 4: Devolver el nodo al clúster
El paso que todo el mundo olvida y que deja clústeres a media capacidad durante semanas:
kubectl uncordon node-02
kubectl get nodes
💡 Un PDB puede bloquear un drenado para siempre si los Pods que protege nunca llegan a estar Ready (por ejemplo, una réplica en CrashLoopBackOff que impide alcanzar el minAvailable). Para ese callejón sin salida existe unhealthyPodEvictionPolicy: AlwaysAllow, que permite desalojar Pods que ni siquiera están listos. Y existe también el atajo peligroso, kubectl drain --disable-eviction, que ignora los PDB por completo: úsalo solo sabiendo exactamente lo que estás tirando.
Resumen
drain=cordon(no entran Pods nuevos) +evict(salen los que hay), y el desalojo respeta los PodDisruptionBudgets.- Un PDB solo gobierna las interrupciones voluntarias. No es un sustituto de tener réplicas.
minAvailableigual al número de réplicas significa cero mantenimientos posibles. Mira siempre la columna ALLOWED DISRUPTIONS.topologySpreadConstraintsreparte réplicas por nodo o por zona; conScheduleAnywayes una preferencia, conDoNotScheduleuna exigencia.- Después de drenar,
uncordon. Siempre.
- Previous lesson
- Priority y Preemption
- Next lesson
- Dispositivos y Dynamic Resource Allocation