Lesson  in  Kubernetes 101

Priority y Preemption

No todos los Pods valen lo mismo. Crea PriorityClasses, llena un nodo con una carga poco importante y observa cómo el scheduler la expulsa para hacer sitio a un Pod crítico.

Hasta ahora, cuando un Pod no cabía, se quedaba en Pending y ahí acababa la historia. Educado, pero ingenuo: si el Pod que no cabe es la pasarela de pagos y lo que ocupa el nodo es un trabajo de informes que puede esperar a mañana, alguien ha tomado la decisión equivocada.

La prioridad es cómo se le dice al scheduler que no todos los Pods valen lo mismo. Y la preemption (expulsión) es lo que hace con esa información: cuando un Pod importante no cabe en ningún sitio, el scheduler busca un nodo donde, echando a Pods menos importantes, sí quepa. Y los echa.

El playground ya ha calculado el tamaño exacto para que solo quepa un Pod de estos en node-01, y ha dejado los dos manifiestos en /home/laborant/manifests. Míralos desde la pestaña dev-machine:

cat /home/laborant/manifests/reports.yaml
cat /home/laborant/manifests/api.yaml
kubectl get node node-01 -o yaml | yq .status.allocatable

Son idénticos salvo el nombre. Tu trabajo es darles una prioridad distinta y ver qué pasa.

Paso 1: Las PriorityClasses

Crea prioridades.yaml:

cat << 'EOF' > prioridades.yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: baja
value: 100
globalDefault: false
description: "Cargas que pueden esperar: informes, procesos por lotes, experimentos."
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: alta
value: 1000000
globalDefault: false
preemptionPolicy: PreemptLowerPriority
description: "Servicios críticos de negocio."
EOF

El YAML, explicado con preguntas y respuestas

¿Una PriorityClass tiene namespace?

No. Es un objeto de clúster, como las StorageClasses, las IngressClasses o los nodos. Tiene sentido: la prioridad es una escala común, y no serviría de nada que cada equipo tuviera la suya.

¿Qué es el value y qué escala se usa?

Un número entero, y cuanto más alto, más importante. La escala te la inventas tú, pero hay un tramo reservado: por encima de mil millones (1000000000) están las clases del sistema, system-cluster-critical y system-node-critical, que usan los componentes del propio Kubernetes. Míralas, ya existen en tu clúster:

kubectl get priorityclass

¿Qué pasa si pongo globalDefault: true?

Que todos los Pods que no declaren prioridad heredan esa clase. Solo puede haber una en todo el clúster, y es una decisión seria: convertir "crítico" en el valor por defecto es exactamente igual de inútil que no tener prioridades. Si no hay ninguna clase por defecto, la prioridad de un Pod sin clase es cero.

¿Y preemptionPolicy?

Decide si esta clase expulsa o solo adelanta en la cola. Con PreemptLowerPriority (el valor por defecto), un Pod que no cabe puede echar a otros. Con Never, el Pod es prioritario para elegir sitio cuando lo haya, pero no desaloja a nadie: se pone el primero de la cola y espera. Esa segunda opción es muy útil para cargas importantes pero no urgentes, y es la que evita sustos.

kubectl apply -f prioridades.yaml
kubectl get priorityclass

Paso 2: Llenar el nodo con lo prescindible

Edita /home/laborant/manifests/reports.yaml y añade una sola línea dentro de su spec:

  priorityClassName: baja

Y aplícalo:

kubectl apply -f manifests/reports.yaml
kubectl get pod reports -o wide

Está en node-01, tan tranquilo, ocupando el 70 % de la CPU del nodo. Nadie lo molesta.

Paso 3: Llega el importante

Ahora edita /home/laborant/manifests/api.yaml con la otra clase:

  priorityClassName: alta

Y prepárate para mirar dos cosas a la vez. Aplica y observa:

kubectl apply -f manifests/api.yaml
kubectl get pods -o wide --watch

En unos segundos: el Pod reports desaparece y el Pod api pasa de Pending a Running en node-01.

Lo que acaba de ocurrir, paso a paso, y merece la pena entenderlo bien:

  1. El scheduler intenta colocar a api. No cabe en node-01 (el único que su nodeSelector permite) porque reports tiene la CPU reservada.
  2. En vez de rendirse, evalúa la expulsión: busca qué Pods de menor prioridad tendría que echar de ese nodo para hacer sitio.
  3. Elige a reports como Pod a desalojar y lo desaloja, respetando su período de gracia (y, en la medida de lo posible, sus PodDisruptionBudgets).
  4. Con el hueco libre, programa a api.

Léelo en los eventos, que es donde queda la confesión:

kubectl get events --sort-by=.lastTimestamp | tail -10
kubectl describe pod api | grep -A5 Events

Verás el motivo Preempted y el nombre del Pod desalojado.

La letra pequeña que hay que conocer

El Pod expulsado no vuelve. reports era un Pod suelto, así que ha desaparecido para siempre. Si hubiera sido parte de un Deployment, el ReplicaSet crearía un sustituto que iría a otro nodo, o se quedaría en Pending esperando sitio. Moraleja: la preemption no reprograma al Pod desalojado, solo lo expulsa; quien lo recrea, si acaso, es su controlador.

La prioridad también ordena la cola. No es solo para expulsar: entre varios Pods pendientes, el scheduler atiende primero a los de mayor prioridad, aunque no haya que echar a nadie.

Cuidado con la prioridad como arma. En un clúster compartido, si cada equipo puede crear PriorityClasses y usarlas libremente, todos acabarán siendo críticos y habrás vuelto al punto de partida (con la desventaja de haber añadido complejidad). Por eso las PriorityClasses se gobiernan: se restringe su creación con RBAC y se limita su uso con ResourceQuotas y scopeSelector, que permiten decir "en este namespace solo puedes tener 2 Pods de prioridad alta".

No confundas prioridad con QoS. Son dos jerarquías distintas y actúan en momentos distintos: la prioridad la usa el scheduler para decidir a quién coloca y a quién echa; la QoS Class la usa el kubelet para decidir a quién desaloja cuando el nodo se queda sin memoria. Un Pod puede ser de prioridad alta y BestEffort a la vez, y ser el primero en caer cuando al nodo le falte memoria.

Resumen

  • Una PriorityClass es un objeto de clúster con un value entero: cuanto más alto, más importante.
  • La preemption expulsa Pods de menor prioridad para hacer sitio a uno mayor. preemptionPolicy: Never prioriza sin expulsar.
  • El Pod desalojado no se reprograma solo: lo recrea su controlador, o no lo recrea nadie.
  • globalDefault solo puede haber uno, y sin él la prioridad por defecto es cero.
  • Prioridad (scheduler, dónde) y QoS (kubelet, a quién matar) son escalas distintas. No se mezclan.