Node Affinity en Kubernetes: Reglas de Scheduling, Trade-offs y Buenas Prácticas
El scheduling de pods en Kubernetes puede parecer un proceso automático y transparente. Y durante la mayor parte del tiempo, lo es. El scheduler elige un nodo disponible con recursos suficientes y la carga de trabajo arranca. Sin problemas. Sin configuración adicional. Sin que el equipo tenga que intervenir.
El problema aparece cuando ese comportamiento por defecto ya no es suficiente. Cuando tienes nodos con hardware especializado —GPUs, SSDs NVMe, instancias de alta memoria— y necesitas garantizar que ciertos workloads acaben en los nodos correctos. O cuando regulaciones de licencias obligan a aislar determinadas cargas de trabajo. O cuando simplemente quieres colocar los pods de una aplicación en la misma zona de disponibilidad que su base de datos para minimizar la latencia.
Ahí es exactamente donde entra en juego node affinity en Kubernetes: el mecanismo que te permite expresar, con mayor o menor grado de obligatoriedad, en qué nodos quieres que el scheduler coloque tus pods.
Este artículo cubre todo lo que necesitas saber para usar node affinity de forma efectiva: conceptos fundamentales, diferencias con nodeSelector, los dos tipos de reglas, sus trade-offs reales en producción y buenas prácticas para no encontrarte con sorpresas cuando falle un nodo a las 3 de la madrugada.
Qué es Node Affinity en Kubernetes
Node affinity es una funcionalidad del scheduler de Kubernetes que permite definir restricciones sobre en qué nodos pueden programarse los pods, basándose en las etiquetas (labels) de esos nodos.
En la práctica, es la respuesta de Kubernetes a una pregunta muy concreta: «¿cómo le digo al scheduler que este pod solo debe ir a nodos con esta característica?» La respuesta, antes de node affinity, era nodeSelector. Y aunque nodeSelector sigue funcionando y es perfectamente válido para casos simples, node affinity expande notablemente esa capacidad.
Node Affinity vs nodeSelector: cuál es la diferencia
nodeSelector es el mecanismo más básico de scheduling por etiquetas. Añades un campo nodeSelector al spec del pod con los labels que debe tener el nodo, y el scheduler filtra en consecuencia. Funciona. Es directo. Pero tiene limitaciones importantes:
- Solo permite igualdad exacta (
key: value). No puedes expresar «cualquier valor de esta lista» o «que NO tenga este label». - No distingue entre restricciones obligatorias y preferentes. O se cumple, o el pod no se programa.
- La semántica de error no es especialmente informativa cuando el scheduling falla.
Node affinity resuelve estas limitaciones con una API más expresiva:
- Soporta operadores como
In,NotIn,Exists,DoesNotExist,GtyLt. - Distingue entre reglas required (obligatorias) y preferred (preferentes).
- Se integra mejor con las demás restricciones del scheduler, como pod affinity, pod anti-affinity y taints/tolerations.
La recomendación general es usar nodeSelector para casos muy simples donde la igualdad directa es suficiente, y migrar a node affinity en cuanto necesitas más expresividad o cuando el comportamiento ante fallos importa.
Los dos tipos de reglas de Node Affinity
Node affinity se configura en el campo affinity.nodeAffinity del spec del pod, y distingue dos modos fundamentales:
1. requiredDuringSchedulingIgnoredDuringExecution
Esta es la regla obligatoria (hard constraint). El scheduler solo colocará el pod en nodos que cumplan exactamente los criterios definidos. Si ningún nodo disponible satisface la regla, el pod permanece en estado Pending indefinidamente.
El nombre completo merece atención: IgnoredDuringExecution significa que, si un nodo cambia sus labels después de que el pod ya está ejecutándose, Kubernetes no lo desaloja. El pod sigue corriendo. La regla solo se aplica en el momento del scheduling inicial.
Ejemplo de configuración:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-type
operator: In
values:
- database
- storage-optimized
Con esta configuración, el pod solo se programará en nodos que tengan el label node-type con valor database o storage-optimized. En cualquier otro nodo, no se programará.
2. preferredDuringSchedulingIgnoredDuringExecution
Esta es la regla preferente (soft constraint). El scheduler intentará colocar el pod en nodos que cumplan los criterios, pero si no hay ninguno disponible, lo programará en cualquier otro nodo elegible.
Cada regla preferente tiene un campo weight (1-100) que permite ponderar su importancia relativa cuando hay varias reglas preferentes simultáneas. El scheduler suma los pesos de las reglas satisfechas por cada nodo y elige el nodo con mayor puntuación.
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- eu-west-1a
- weight: 20
preference:
matchExpressions:
- key: node-type
operator: In
values:
- compute-optimized
En este ejemplo, el scheduler intentará colocar el pod en nodos de la zona eu-west-1a (peso 80) y preferentemente de tipo compute-optimized (peso 20), pero si no existen nodos que cumplan ninguna de las dos condiciones, el pod se programará igualmente en otro nodo.
Pueden combinarse
Nada impide usar ambas reglas simultáneamente. Un caso habitual: la regla required asegura que el pod solo va a nodos de un tipo determinado, y la regla preferred optimiza dentro de ese subconjunto por zona de disponibilidad o tier de hardware.
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: workload-class
operator: In
values:
- gpu
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: gpu-model
operator: In
values:
- nvidia-a100
Trade-offs reales: Required vs Preferred en producción
Aquí está el núcleo del artículo, y la parte que mucha documentación glosa demasiado rápido. Porque node affinity no es simplemente «elige la opción que más se adapte a tu caso de uso». Cada elección tiene implicaciones directas sobre la disponibilidad, la operabilidad y el comportamiento ante fallos.
Para ilustrarlo, consideremos un escenario concreto: una base de datos distribuida —etcd, ZooKeeper o Cassandra— desplegada con tres réplicas para garantizar quórum y tolerancia a fallos. El equipo ha designado tres nodos específicos del clúster para alojar estos workloads y ha configurado los labels correspondientes. La pregunta es: ¿required o preferred?
El problema con required en este escenario
Si defines la regla como required, el scheduler solo programará las réplicas en los tres nodos etiquetados. Mientras todo funciona, perfecto: cada réplica está exactamente donde debería estar.
Pero imagina que uno de esos tres nodos cae por un fallo de hardware o una actualización. El pod que corría en ese nodo necesita ser reprogramado. ¿Dónde puede ir? Solo a otro de los nodos etiquetados. Si esos dos nodos ya tienen sus réplicas respectivas, y además tienes configurado pod anti-affinity para garantizar que cada réplica esté en un host diferente, el pod queda en Pending.
El clúster de base de datos está ahora con dos de tres réplicas activas. Si tienes quórum con dos, sigues funcionando. Pero si el nodo caído no vuelve pronto y otro falla, pierdes quórum. Y el pod sigue en Pending porque no puede ir a ningún nodo no etiquetado.
La combinación de required node affinity + pod anti-affinity puede ser más restrictiva de lo que parece a primera vista, y puede dejarte sin capacidad de recuperación automática en situaciones donde más la necesitas.
El problema con preferred en este escenario
Ahora supón que optas por preferred. El pod siempre se programará, incluso si ningún nodo etiquetado está disponible. Resuelves el problema de disponibilidad, ¿verdad?
En parte, sí. Pero introduces otro problema: la opacidad del estado del clúster. Si uno de los nodos etiquetados tiene problemas esporádicos de recursos, el scheduler podría empezar a colocar pods en nodos no etiquetados silenciosamente. Al cabo de un tiempo, tienes réplicas de tu base de datos crítica repartidas entre nodos etiquetados y no etiquetados, sin que ningún mecanismo te haya avisado explícitamente.
Administrar esa situación es mucho más difícil. No puedes garantizar que el workload está donde tiene que estar. Las asunciones sobre aislamiento de recursos o rendimiento del hardware se rompen.
El escenario más peligroso: preferred + taints
El caso más problemático aparece cuando combinas preferred node affinity con nodos que tienen taints. Considera este setup:
- Los tres nodos etiquetados para tu base de datos tienen un taint
dedicated=database:NoSchedulepara evitar que otros workloads los invadan. - Los pods de la base de datos tienen la toleration correspondiente.
- La regla de node affinity es
preferred.
Si los nodos etiquetados están saturados, el scheduler intentará programar los pods en otros nodos. Pero esos otros pods —los que no son la base de datos— no tienen toleration para los nodos etiquetados con taint, y si los nodos sin etiquetar también se están llenando, el scheduler puede quedarse sin opciones.
Resultado: los pods de la base de datos acaban en nodos aleatorios que no son los designados, y otros workloads no pueden usar los nodos designados porque están tainteados. Tienes un clúster desorganizado donde ni el aislamiento ni la distribución están funcionando como esperabas. Y lo que es peor: todo esto puede estar pasando silenciosamente, sin alarmas obvias.
Prepararse para los fallos inesperados
La conclusión práctica de todo lo anterior es sencilla pero importante: las decisiones de scheduling que tomas en tiempo de diseño se manifiestan en tiempo de fallo. Mientras el clúster funciona con normalidad, tanto required como preferred se comportan igual de bien. La diferencia emerge exactamente cuando las cosas van mal.
Esto significa que antes de definir una regla de node affinity en producción, deberías hacerte estas preguntas:
¿Qué ocurre si uno de los nodos etiquetados falla?
– Con required: ¿hay suficientes nodos etiquetados para absorber la carga? ¿El pod anti-affinity no va a bloquearlo?
– Con preferred: ¿es aceptable que el workload acabe en nodos no designados? ¿Qué impacto tiene en rendimiento o compliance?
¿Cuántos nodos etiquetados tengo y cuántos necesito para tolerar fallos?
Si tienes tres réplicas y tres nodos etiquetados, un fallo deja el sistema al límite. Si tienes cuatro nodos etiquetados, tienes margen de maniobra con required.
¿Tengo taints en estos nodos? ¿Qué tolerations tienen los pods?
Tener claro el mapa completo de taints/tolerations antes de definir affinity rules evita los escenarios de deadlock descritos arriba.
¿Cómo monitorizo si los pods están donde se supone que deben estar?
Con preferred especialmente, necesitas alertas o dashboards que confirmen que el workload está realmente en los nodos designados, no solo que está programado y corriendo.
Node Affinity e interacción con Taints y Tolerations
Node affinity y el sistema de taints/tolerations son mecanismos complementarios, pero operan en fases distintas del proceso de scheduling.
El scheduler aplica primero las reglas de node affinity para filtrar el conjunto de nodos candidatos. Una vez tiene ese subconjunto, aplica el filtro de taints y tolerations: solo los nodos cuyos taints el pod puede tolerar son elegibles. Un pod solo acaba en un nodo que supera ambos filtros.
Entender este orden es clave. Si un nodo tiene el label correcto para tu affinity rule pero también tiene un taint que el pod no tolera, el pod no va a ese nodo. No importa cuánto peso le hayas dado a la preferred rule.
La tabla siguiente resume la interacción:
| Pod tiene affinity rule | Nodo tiene label correcto | Nodo tiene taint | Pod tolera el taint | Resultado |
|---|---|---|---|---|
| Required | Sí | No | N/A | Programado |
| Required | Sí | Sí | Sí | Programado |
| Required | Sí | Sí | No | No programado |
| Required | No | N/A | N/A | No programado (Pending) |
| Preferred | Sí | No | N/A | Programado (nodo preferido) |
| Preferred | No | No | N/A | Programado (nodo no preferido) |
| Preferred | Sí | Sí | No | No programado en ese nodo; busca alternativa |
La recomendación es diseñar siempre affinity rules y taints/tolerations como un sistema coherente, no como capas independientes. Mapea explícitamente: qué nodos tienen qué labels, qué nodos tienen qué taints, y qué pods tienen qué tolerations y qué affinity rules. Un diagrama simple de esto puede evitar horas de debugging cuando el scheduler no hace lo que esperas.
Buenas prácticas para definir Node Affinity en producción
Usa labels descriptivos y consistentes
Los labels son el contrato entre tus workloads y tus nodos. Defínelos con cuidado:
- Prefiere los well-known labels de Kubernetes cuando existan:
topology.kubernetes.io/zone,topology.kubernetes.io/region,kubernetes.io/arch,node.kubernetes.io/instance-type. Son más portables y compatibles con herramientas del ecosistema. - Para labels personalizados, usa un prefijo de dominio propio:
workload.mycompany.com/class: databasees mejor que simplementeclass: database. Evita colisiones con otros sistemas. - Documenta el propósito de cada label custom en tu wiki o en anotaciones del propio nodo. Los labels sin documentar se convierten en deuda técnica.
Dimensiona los nodos etiquetados pensando en fallos
Si vas a usar required node affinity para un workload con N réplicas, necesitas al menos N+1 nodos etiquetados para que el sistema pueda recuperarse ante un fallo sin quedarse en Pending. Idealmente N+2 si quieres tolerancia a dos fallos simultáneos.
No etiquetes el mínimo indispensable. Etiqueta con margen.
Combina con pod anti-affinity con cuidado
La combinación de required node affinity + pod anti-affinity es especialmente poderosa para garantizar distribución, pero también especialmente peligrosa si no se dimensiona bien. Cada regla adicional reduce el espacio de nodos elegibles para el scheduler. Haz el cálculo explícito: con X nodos etiquetados, Y réplicas, y anti-affinity de host, ¿cuántos fallos puedo absorber antes de que el scheduler se bloquee?
Prefiere preferred para optimización, required para compliance
Una heurística útil: usa required cuando el incumplimiento de la regla es inaceptable (restricciones de licencia, compliance regulatorio, hardware imprescindible como GPU). Usa preferred cuando es una optimización (latencia, coste, rendimiento) que puede sacrificarse temporalmente si la disponibilidad lo requiere.
Simula fallos antes de ir a producción
No te fíes únicamente del razonamiento teórico. Con herramientas como kubectl cordon o kubectl drain puedes simular la indisponibilidad de un nodo en staging y observar exactamente qué hace el scheduler. ¿Los pods quedan en Pending? ¿Se van a nodos no esperados? ¿Cuánto tardan en reprogramarse?
Esta validación debería ser parte del proceso de revisión antes de mergear cambios en la configuración de scheduling.
Monitoriza el estado real del placement
Especialmente con preferred rules, implementa alertas que verifiquen que los pods están en los nodos esperados, no solo que están en estado Running. Un simple script que compare los labels del nodo donde corre cada pod contra los labels esperados, ejecutado periódicamente, puede darte esa visibilidad. Herramientas como Prometheus + kube-state-metrics facilitan este tipo de queries.
Ejemplo completo: base de datos distribuida con node affinity
Para cerrar con algo concreto, aquí está un ejemplo de configuración para el escenario de la base de datos distribuida con un enfoque razonado:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: distributed-db
spec:
replicas: 3
template:
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: workload.mycompany.com/class
operator: In
values:
- database
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- eu-west-1a
- eu-west-1b
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- distributed-db
topologyKey: kubernetes.io/hostname
tolerations:
- key: "dedicated"
operator: "Equal"
value: "database"
effect: "NoSchedule"
Observa las decisiones deliberadas aquí:
- La node affinity required garantiza que los pods solo van a nodos etiquetados como
database. Es una restricción de compliance/aislamiento que no queremos que se rompa. - La preferencia de zona es
preferred, norequired, porque si una zona tiene problemas, preferimos que el pod se programe en otra zona a que quede en Pending. - El pod anti-affinity también es
preferred(norequired), lo que permite al scheduler romper la restricción de un-pod-por-host si es necesario para garantizar disponibilidad. Puede que dos réplicas acaben en el mismo host en un escenario de fallo extremo, pero al menos estarán corriendo. - Los nodos de base de datos tienen un taint
dedicated=database:NoScheduley el pod tiene la toleration correspondiente.
Este diseño prioriza disponibilidad sobre aislamiento perfecto, que es la elección correcta para la mayoría de workloads en producción.
Preguntas frecuentes sobre Node Affinity en Kubernetes
¿Cuál es la diferencia entre nodeSelector y node affinity?
nodeSelector es un campo simple que requiere que el nodo tenga exactamente los labels especificados. Node affinity es una API más expresiva que soporta operadores como In, NotIn, Exists y DoesNotExist, y distingue entre restricciones hard (required) y soft (preferred). Usa nodeSelector para necesidades básicas de igualdad directa; usa node affinity para cualquier lógica de scheduling más compleja.
¿Cuándo usar required y cuándo preferred?
Usa required cuando el incumplimiento es inaceptable: hardware específico (GPU, FPGA), restricciones de licencia, compliance regulatorio, segmentación de red. Usa preferred cuando la regla es una optimización que puede sacrificarse: colocación en la misma zona de disponibilidad para reducir latencia, preferencia por instancias de un determinado tipo. Ten en cuenta que required puede dejar pods en Pending ante fallos de nodo, mientras que preferred puede no garantizar el placement esperado.
¿Qué riesgos tiene usar required node affinity?
El riesgo principal es el bloqueo de scheduling. Si ningún nodo satisface las reglas required (por fallo, mantenimiento o cambio de labels), el pod queda en Pending indefinidamente. Este riesgo se amplifica cuando se combina con pod anti-affinity, que reduce aún más el conjunto de nodos elegibles. La solución es dimensionar correctamente los nodos etiquetados para tener capacidad de sobra ante fallos.
¿Cómo interactúan node affinity y taints/tolerations?
Operan en fases distintas del scheduling. Primero, el scheduler filtra nodos según las affinity rules. Después, del conjunto resultante, filtra los nodos cuyos taints el pod no puede tolerar. Un pod solo se programa en un nodo que pasa ambos filtros. Diseña siempre ambos mecanismos como un sistema coherente.
¿Qué buenas prácticas debo seguir al definir labels para node affinity?
Usa well-known labels de Kubernetes cuando existan (topology.kubernetes.io/zone, node.kubernetes.io/instance-type). Para labels propios, usa un prefijo de dominio para evitar colisiones. Documenta el propósito de cada label. Dimensiona los nodos etiquetados con margen para tolerar fallos. Valida el comportamiento del scheduler simulando indisponibilidad de nodos antes de pasar a producción.
¿Puedo combinar required y preferred en la misma definición?
Sí. Es un patrón habitual: la regla required establece un filtro duro (solo nodos de un determinado tipo), y las reglas preferred optimizan dentro de ese subconjunto (preferir la zona más cercana, el hardware más rápido, etc.). El scheduler siempre respeta primero las reglas required y después puntúa los candidatos según las preferred.
Conclusión
Node affinity en Kubernetes es una herramienta poderosa para controlar el scheduling de workloads con precisión. Pero esa precisión tiene un coste: introduce fragilidad si no se diseña pensando en los escenarios de fallo.
La clave está en entender que la elección entre required y preferred no es solo una decisión de «qué quiero», sino de «qué pasa cuando las cosas fallan». Required te da certeza a cambio de disponibilidad. Preferred te da disponibilidad a cambio de certeza. Ninguna de las dos es incorrecta: depende del contrato de tu workload con el negocio.
Antes de definir cualquier regla de node affinity en producción, mapea explícitamente el conjunto de nodos etiquetados, sus taints, las tolerations de los pods, y simula al menos un escenario de fallo. Ese análisis previo vale más que cualquier configuración perfecta en papel que nadie ha probado bajo presión.
Este artículo forma parte de la guía completa de patrones de arquitectura Kubernetes en alexandre-vazquez.com.