Skip to main content

Node Selector y Node Affinity

Node Selector

El nodeSelector utiliza Labels para especificar qué Nodos queremos dirigir. Podemos agrupar Nodos también usando Labels y definir Pods para ser implantados apenas en los Nodos que posean esas Labels correspondientes.

Alt text

Para colocar una Label en un Nodo.

kubectl label nodes k3d-k3d-cluster-agent-1 size=large
node/k3d-k3d-cluster-agent-1 labeled

kubectl describe node k3d-k3d-cluster-agent-1
Name: k3d-k3d-cluster-agent-1
Roles: <none>
Labels: beta.kubernetes.io/arch=amd64
beta.kubernetes.io/instance-type=k3s
beta.kubernetes.io/os=linux
kubernetes.io/arch=amd64
kubernetes.io/hostname=k3d-k3d-cluster-agent-1
kubernetes.io/os=linux
node.kubernetes.io/instance-type=k3s
size=large

kubectl get node k3d-k3d-cluster-agent-1 --show-labels
NAME STATUS ROLES AGE VERSION LABELS
k3d-k3d-cluster-agent-1 Ready <none> 70d v1.27.4+k3s1 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=k3s,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k3d-k3d-cluster-agent-1,kubernetes.io/os=linux,node.kubernetes.io/instance-type=k3s,size=large

kubectl label nodes k3d-k3d-cluster-agent-1 size-
node/k3d-k3d-cluster-agent-1 unlabeled

Pero no siempre el nodeSelector es suficiente para resolver nuestros problemas cuando las exigencias son más complejas

Node Affinity

En ese contexto, nos encontramos con una limitación del nodeSelector, que no consigue lidiar con complejidades como la necesidad de seleccionar Nodos de determinado tamaño, como large o medium, pero evitar los de tamaño small. Con el nodeSelector, no es posible crear expresiones del tipo 'uno u otro' o incluso negar una condición específica.

Con grandes poderes vienen grandes complejidades.

¡Este nodeSelector y este affinity hacen lo mismo!

Alt text

Toda expresión de affinity viene bajo la key affinity:

Existen affinity para Nodos y Pods, entonces tenemos los posibles iniciales:

Estos valores no están definidos, pero pueden ser configurados.

apiVersion: v1
...
spec:
containers:
....
affinity:
nodeAffinity: null
podAffinity: null
podAntiAffinity: null

El nodeAffinity definirá una afinidad para que el Pod sea programado para un Nodo.

El podAffinity y el podAntiAffinity definen la afinidad entre los Pods. Por ejemplo, podemos configurar para que un Pod sea colocado donde hay otro Pod específico (podAffinity) o evite ser colocado donde hay Pods de un tipo específico (podAntiAffinity). Un ejemplo práctico sería garantizar que los Pods A y B estén siempre en el mismo Nodo, no apenas en el mismo grupo de Nodos con la misma Label, para minimizar el tráfico de red y mejorar el desempeño.

Dentro de esos tipos de afinidad podemos tener estas expresiones:

  • requiredDuringSchedulingIgnoredDuringExecution: Define reglas OBLIGATORIAS para el Scheduler. Si un Nodo no atiende a esas reglas, el Pod no será programado en él, pero si ya fue programado en él no necesita remover del Nodo si una Label fue removida por ejemplo.

  • preferredDuringSchedulingIgnoredDuringExecution: Igual a la regla anterior pero después de intentar al máximo si no consigue y hay cómo ser programado para otro, puede dejar, pues no es obligatorio.

  • requiredDuringSchedulingRequiredDuringExecution: La diferencia de no ignorar es que si las condiciones cambian, por ejemplo si una Label es removida, también removerá el Pod. NO ES POSIBLE TENER ESTA EXPRESIÓN JUNTO CON LAS OTRAS

    • TODAVÍA NO FUNCIONA CON EL nodeAffinity

  • preferredDuringSchedulingRequiredDuringExecution: Esta expresión también es posible. NO ES POSIBLE TENER ESTA EXPRESIÓN JUNTO CON LAS OTRAS

    • TODAVÍA NO FUNCIONA CON EL nodeAffinity

Esto sería posible.

apiVersion: v1
...
spec:
containers:
....
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
bloque
preferredDuringSchedulingIgnoredDuringExecution:
bloque
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
bloque
preferredDuringSchedulingIgnoredDuringExecution:
bloque
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
bloque
preferredDuringSchedulingIgnoredDuringExecution:
bloque

Esto sería posible, pero no funciona todavía.

apiVersion: v1
...
spec:
containers:
....
affinity:
nodeAffinity:
requiredDuringSchedulingRequiredDuringExecution: # En particular no es posible usar esta expresión todavía, es una issue en Kubernetes en desarrollo
bloque
podAffinity:
requiredDuringSchedulingRequiredDuringExecution:
bloque
podAntiAffinity:
requiredDuringSchedulingRequiredDuringExecution:
bloque

Esto todavía sería posible

apiVersion: v1
...
spec:
containers:
....
affinity:
nodeAffinity:
requiredDuringSchedulingRequiredDuringExecution: # En particular no es posible usar esta expresión todavía, es una issue en Kubernetes en desarrollo
bloque
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
bloque
preferredDuringSchedulingIgnoredDuringExecution:
bloque
podAntiAffinity:
requiredDuringSchedulingRequiredDuringExecution:
bloque

Pero esto no es posible

apiVersion: v1
...
spec:
containers:
....
affinity:
nodeAffinity:
requiredDuringSchedulingRequiredDuringExecution: # En particular no es posible usar esta expresión todavía, es una issue en Kubernetes en desarrollo
bloque
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
bloque
requiredDuringSchedulingRequiredDuringExecution: # RequiredDuringExecution no combina con nadie
bloque
podAntiAffinity:
requiredDuringSchedulingRequiredDuringExecution:
bloque

Ahora vamos a concentrarnos en el Bloque:

El bloque puede ser nodeSelectorTerms si se está usando nodeAffinity o podSelectorTerms si se está usando podAffinity o podAntiAffinity

nodeSelectorTerms y podSelectorTerms habilitan el posible OR. O una expresión, o otra expresión, o otra, etc. ¿Podría ser colocado en el bloque directamente un matchExpressions sin estar dentro de los bloques terms? Sí podría. Si lo mantienes tampoco hace mal y es MEJOR.

Entonces vamos al bloque.

apiVersion: v1
kind: Pod
...
spec:
containers:
....
affinity:
nodeAffinity:
requiredDuringSchedulingRequiredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: key1
operator: In
values:
- value1
- value2
# O Regla 2
- matchFields:
- key: metadata.name
operator: In
values:
- node-1

Ahora para entender mejor lo que dije, los dos affinities abajo son lo mismo.

  affinity:
nodeAffinity:
requiredDuringSchedulingRequiredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: key1
operator: In
values:
- value1
- value2

# PERO EN ESTE AFFINITY PERDIÓ LA OPCIÓN DEL OR ENTONCES USA SIEMPRE EL DE ARRIBA
affinity:
nodeAffinity:
requiredDuringSchedulingRequiredDuringExecution:
matchExpressions:
- key: key1
operator: In
values:
- value1
- value2

Ahora vamos a entender los posibles operator que podemos usar.

  • In: Comparará si los valores de las claves son IDÉNTICOS
  • NotIn: Comparará si los valores de las claves son DIFERENTES
  • Exists: Solo quiere saber si la clave existe no importa el valor
  • DoesNotExist: Solo quiere saber si la clave NO EXISTE no importa el valor
  • Gt: Mayor que, igual que el operador in pero comparará números aunque sea string
  • Lt: menor que

En el caso del Exists y del DoesNotExist un ejemplo

  affinity:
nodeAffinity:
requiredDuringSchedulingRequiredDuringExecution:
matchExpressions:
- key: key1
operator: Exists

Si ambas son aplicadas a un Pod nodeSelector tiene precedencia sobre el affinity.

apiVersion: v1
kind: Pod
metadata:
name: example-pod
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: key1
operator: In
values:
- value1
- value2
nodeSelector:
key2: value3
containers:
- name: my-container
image: my-image

Moral de la historia... Si dominas el affinity no necesitas el nodeSelector, pero es bueno conocer.