Fins ara hem decidit què s'executa i com es compon cada pod, però mai on. El kube-scheduler ha anat col·locant els nostres pods als nodes que li ha semblat i no ens hem queixat, perquè en un clúster homogeni de proves qualsevol lloc serveix.

En producció no serveix qualsevol lloc. postgres-reserves necessita el node amb disc SSD, perquè en un disc mecànic les consultes d'ocupació triguen cinc vegades més. Les sis rèpliques d'api-reserves no haurien d'estar totes al mateix node: si aquell node es reinicia, l'API sencera desapareix justament quan algú està pagant un bitllet. I les càrregues d'anàlisi pesades no haurien de competir per CPU amb la venda de bitllets en un pont festiu.

Tot això s'expressa amb tres mecanismes que estudiarem aquí: afinitat (el pod tria node), antiafinitat (el pod evita companyia) i taints amb toleracions (el node rebutja pods). Al final tindrem el pla de planificació complet de Rutas Norte i sabrem diagnosticar el pod que es queda Pending per sempre.

Contingut

  1. Com decideix el kube-scheduler: filtratge i puntuació
  2. nodeName i nodeSelector: els mecanismes bàsics
  3. Afinitat de node: required davant de preferred
  4. Afinitat i antiafinitat entre pods: topologyKey
  5. Taints i toleracions: el node rebutja
  6. Taints automàtics de Kubernetes
  7. PriorityClass i preemption
  8. topologySpreadConstraints: menció i remissió
  9. El pla de planificació de Rutas Norte
  10. Diagnòstic del pod eternament Pending

  1. Com decideix el kube-scheduler: filtratge i puntuació

A la lliçó 01-02 vam dir que el scheduler "assigna pods a nodes". Ara veiem com.

Quan un pod es crea, el seu camp spec.nodeName és buit. El scheduler observa aquests pods sense assignar i, per a cadascun, executa un cicle de dues fases.

graph LR
  P[Pod sense nodeName] --> F[Fase 1: FILTRATGE<br/>quins nodes són viables?]
  F -->|8 nodes → 3 viables| S[Fase 2: PUNTUACIÓ<br/>quin és el millor?]
  S -->|node-5: 78 punts| B[Binding:<br/>escriure nodeName]
  F -->|0 viables| PE[Pod en Pending<br/>+ esdeveniment FailedScheduling]

Fase 1: filtratge

Es descarten els nodes que no poden allotjar el pod. Cada comprovació és un veto absolut: n'hi ha prou amb una per eliminar el node.

Filtre Què comprova
NodeResourcesFit Queden CPU i memòria lliures per a les requests del pod?
NodeAffinity Compleix el node l'afinitat required del pod?
NodeName Si el pod va fixar nodeName, és aquest aquell node?
TaintToleration Tolera el pod els taints NoSchedule del node?
NodePorts Si el pod demana un hostPort, està lliure al node?
VolumeBinding Pot el node accedir als volums del pod (zona, topologia)?
PodTopologySpread Respecta les restriccions de distribució topològica?
InterPodAffinity Compleix l'afinitat i l'antiafinitat required entre pods?

Un detall que connecta amb el mòdul 5: el filtre VolumeBinding és el que fa funcionar volumeBindingMode: WaitForFirstConsumer. Un PVC ja vinculat a un volum de la zona eu-west-1a elimina del filtratge tots els nodes de les altres zones.

I un altre que connecta amb el mòdul 3: NodeResourcesFit mira requests, no l'ús real. Un node amb 8 CPU on els pods han reservat 7,8 CPU però només n'usen 0,5 es considera ple. Per això uns requests inflats malbaraten clúster.

Fase 2: puntuació

Dels nodes viables, cada complement de puntuació atorga entre 0 i 100 punts, i se'n fa una mitjana ponderada.

Complement Premia
NodeResourcesBalancedAllocation Nodes on l'ús de CPU i memòria quedaria equilibrat
NodeResourcesFit (LeastAllocated) Nodes amb més recursos lliures: reparteix la càrrega
ImageLocality Nodes que ja tenen descarregada la imatge: arrencada més ràpida
InterPodAffinity Nodes que satisfan l'afinitat preferred entre pods
NodeAffinity Nodes que satisfan l'afinitat preferred de node, amb el seu weight
TaintToleration Nodes sense taints PreferNoSchedule

Guanya el de més puntuació; si empaten, se n'escull un a l'atzar entre els empatats. El scheduler escriu llavors el nodeName i el kubelet d'aquell node recull el pod.

Amb això ja s'entén la diferència essencial entre els dos tipus de regla que veurem: les required actuen en el filtratge (si no es compleixen, el pod no hi va), i les preferred actuen en la puntuació (si no es compleixen, el pod hi va igualment però a un node pitjor puntuat).

  1. nodeName i nodeSelector: els mecanismes bàsics

nodeName: la força bruta

spec:
  nodeName: rutas-norte-m02

Se salta el scheduler completament: el kubelet d'aquell node veu un pod amb el seu nom i l'arrenca sense més.

No el facis servir mai en producció. Si el node no existeix, està ple o està caigut, el pod es queda penjat sense cap esdeveniment útil; si el node desapareix, no hi ha recol·locació. El seu únic ús legítim és la depuració puntual: forçar un pod a un node concret per investigar un problema local.

nodeSelector: el filtre simple

spec:
  nodeSelector:
    disc: ssd
    entorn-node: produccio

El pod només es programa en nodes que tinguin totes aquestes etiquetes amb aquests valors exactes. És una conjunció d'igualtats, res més.

Les etiquetes es posen sobre els nodes com sobre qualsevol objecte:

kubectl label node rutas-norte-m02 disc=ssd
kubectl label node rutas-norte-m03 disc=hdd
kubectl get nodes --show-labels | head -2

Kubernetes hi afegeix a més unes quantes etiquetes estàndard que convé conèixer:

Etiqueta Contingut
kubernetes.io/hostname Nom del node
kubernetes.io/os linux, windows
kubernetes.io/arch amd64, arm64
topology.kubernetes.io/zone Zona de disponibilitat
topology.kubernetes.io/region Regió
node.kubernetes.io/instance-type Tipus d'instància del proveïdor

Els límits de nodeSelector són tres, i motiven tot el que ve després:

  1. Només igualtat exacta. No es pot dir "SSD o NVMe", ni "qualsevol node que no sigui d'analítica".
  2. És obligatori. Si cap node no hi casa, el pod es queda Pending per sempre. No hi ha manera d'expressar una preferència.
  3. No diu res sobre altres pods. No permet "no em posis amb els meus germans".

  1. Afinitat de node: required davant de preferred

L'afinitat de node resol els dos primers límits: sintaxi expressiva i la possibilitat de preferir en comptes d'exigir.

spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: disc
                operator: In
                values: ["ssd", "nvme"]
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 80
          preference:
            matchExpressions:
              - key: topology.kubernetes.io/zone
                operator: In
                values: ["eu-west-1a"]
        - weight: 20
          preference:
            matchExpressions:
              - key: gamma
                operator: In
                values: ["alta"]

Els noms llargs, desxifrats

Es llegeixen en dues meitats:

  • requiredDuringScheduling / preferredDuringScheduling: si la regla és obligatòria (filtratge) o desitjable (puntuació).
  • IgnoredDuringExecution: què passa si l'etiqueta del node canvia mentre el pod ja està corrent. La resposta és: res. El pod continua on és.

Aquest IgnoredDuringExecution és més important del que sembla. Si programes postgres-reserves en un node amb disc=ssd i algú canvia aquesta etiqueta a disc=hdd, el pod no es mou. L'afinitat s'avalua en el moment de programar i mai més. Existia un RequiredDuringExecution planificat que desallotjaria pods en deixar-se de complir la regla, però no es va implementar mai. Per a desallotjament per condicions del node, el mecanisme és l'efecte NoExecute dels taints (apartat 5).

Estructura i operadors

  • nodeSelectorTerms és una llista amb semàntica OR: n'hi ha prou que es compleixi un terme.
  • matchExpressions dins d'un terme és una llista amb semàntica AND: s'han de complir totes.
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          # Terme A: node amb SSD I amb almenys 16 nuclis
          - matchExpressions:
              - key: disc
                operator: In
                values: ["ssd"]
              - key: nuclis
                operator: Gt
                values: ["15"]
          # ...O BÉ Terme B: qualsevol node NVMe
          - matchExpressions:
              - key: disc
                operator: In
                values: ["nvme"]

Operadors disponibles:

Operador Significat Exemple
In El valor és a la llista disc In [ssd, nvme]
NotIn El valor no és a la llista gamma NotIn [economica]
Exists L'etiqueta existeix, sense importar el valor disc Exists
DoesNotExist L'etiqueta no existeix node-role.kubernetes.io/analitica DoesNotExist
Gt Més gran que (valor enter, a values amb un sol element) nuclis Gt 15
Lt Més petit que carrega-mitjana Lt 50

NotIn i DoesNotExist donen l'antiafinitat de node: no hi ha un objecte separat per a això, s'expressa negant.

Detall sobre Gt i Lt: comparen com a enters, la llista values ha de tenir exactament un element, i el valor de l'etiqueta del node ha de parsejar com a enter. nuclis Gt "15" significa estrictament més gran que 15, és a dir, 16 o més.

Els pesos de preferred

      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 80        # d'1 a 100
          preference:
            matchExpressions: [...]

Cada regla preferred que es compleixi suma el seu weight a la puntuació del node. Amb dues regles de pes 80 i 20, un node que compleixi totes dues puntua 100 en aquest complement; un que només compleixi la primera, 80. Els pesos són relatius entre si: el que importa és la proporció, no el valor absolut.

Combinació típica i molt recomanable: required per al que és un requisit real (arquitectura de CPU, presència d'un disc local) i preferred per al que és una optimització (zona concreta, gamma de màquina). Posar en required el que en realitat era una preferència és la primera causa de pods Pending innecessaris.

  1. Afinitat i antiafinitat entre pods: topologyKey

L'afinitat de node mira etiquetes del node. L'afinitat entre pods mira quins altres pods hi ha ja en aquell node o en aquella zona. És el que permet dir "vull estar a prop de la memòria cau" o "no vull estar amb les meves germanes".

spec:
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchLabels:
              app: api-reserves
              entorn: pro
          topologyKey: kubernetes.io/hostname

Llegeix-ho així: "no em col·loquis en un node (kubernetes.io/hostname) on ja hi hagi un pod amb app=api-reserves i entorn=pro".

topologyKey: la peça central

topologyKey és una etiqueta del node que defineix l'àmbit en què s'aplica la regla. El scheduler agrupa els nodes pel valor d'aquesta etiqueta, i cada grup és un "domini topològic".

topologyKey Domini Efecte d'una antiafinitat
kubernetes.io/hostname Un node Com a molt un pod del grup per node
topology.kubernetes.io/zone Una zona de disponibilitat Com a molt un per zona
topology.kubernetes.io/region Una regió Com a molt un per regió
Etiqueta pròpia, p. ex. bastidor Un bastidor físic Com a molt un per bastidor

Triar bé el topologyKey és triar contra quina fallada et protegeixes:

  • hostname protegeix contra el reinici o la caiguda d'un node.
  • zone protegeix contra la caiguda d'un centre de dades sencer.
  • region protegeix contra una catàstrofe regional, a costa de latència entre rèpliques.

Compte amb required i hostname: si exigeixes com a molt una rèplica per node i tens 3 nodes, no podràs passar de 3 rèpliques. La quarta es quedarà Pending indefinidament. Aquest error apareix sovint combinat amb un autoescalat que intenta pujar a 10.

La solució habitual és fer servir preferred, que reparteix però no bloqueja:

    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            labelSelector:
              matchLabels:
                app: api-reserves
                entorn: pro
            topologyKey: kubernetes.io/hostname

Amb weight: 100, el scheduler penalitza molt els nodes que ja allotgen una rèplica, així que reparteix mentre pot; quan no queda alternativa, en col·loca dues juntes en comptes de deixar el pod Pending. Per a api-reserves, la disponibilitat de la qual importa més que la perfecció del repartiment, és l'elecció correcta.

Fixa't en el canvi de sintaxi entre les dues variants: a required la llista conté directament els termes; a preferred cada element és un objecte amb weight i podAffinityTerm.

podAffinity: atraure en comptes de repel·lir

El cas simètric: worker-notificacions consulta molt redis-cache i li convé estar al mateix node, on el trànsit no travessa la xarxa física.

    podAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 50
          podAffinityTerm:
            labelSelector:
              matchLabels:
                app: redis-cache
                entorn: pro
            topologyKey: kubernetes.io/hostname

Fes servir podAffinity amb moderació. Concentrar pods relacionats al mateix node millora la latència i empitjora la disponibilitat: aquell node es converteix en un punt únic de fallada per a dos components alhora. A Rutas Norte el deixem en preferred amb pes mitjà i no l'apliquem a res crític.

El cost de còmput

Aquest és un advertiment pràctic que la documentació oficial subratlla. Per avaluar afinitat entre pods, el scheduler ha d'examinar, per a cada node candidat, els pods de tots els nodes del seu domini topològic. La complexitat creix amb el producte de nodes per pods, no linealment.

En un clúster de 10 nodes és imperceptible. En un de diversos centenars, amb regles d'afinitat entre pods en moltes càrregues de treball, el temps de decisió per pod pot passar de mil·lisegons a segons, i en un desplegament massiu això es nota. La documentació desaconsella expressament aquestes regles en clústers de més de diversos centenars de nodes.

Mitigacions:

  • Fes servir topologyKey: kubernetes.io/hostname (dominis petits) abans que zone (dominis grans).
  • Acota l'àmbit amb namespaceSelector per no avaluar pods d'altres namespaces.
  • Per al cas concret de "repartir rèpliques de manera equilibrada", topologySpreadConstraints és més eficient; ho veiem a l'apartat 8.

  1. Taints i toleracions: el node rebutja

Afinitat i antiafinitat són mecanismes del pod: el pod expressa on vol anar. Els taints són el mecanisme invers, del node: el node declara que rebutja pods llevat que estiguin expressament autoritzats.

La diferència importa perquè resol un problema que l'afinitat no pot resoldre: reservar un grup de nodes. Amb afinitat, perquè només les càrregues d'anàlisi anessin als nodes d'anàlisi, caldria afegir una regla "no hi vagis" a tots els altres pods del clúster, inclosos els que desplegui algú demà. Amb un taint, el node es protegeix sol.

# Posar un taint
kubectl taint nodes rutas-norte-m04 dedicat=analitica:NoSchedule

# Treure'l (fixa't en el guionet final)
kubectl taint nodes rutas-norte-m04 dedicat=analitica:NoSchedule-

# Consultar
kubectl describe node rutas-norte-m04 | grep -A3 Taints

Un taint té la forma clau=valor:efecte.

Els tres efectes

Efecte En programar Als pods que ja són al node
NoSchedule Rebutja els pods que no el toleren No els toca
PreferNoSchedule Els evita si pot, però els accepta si no hi ha alternativa No els toca
NoExecute Rebutja els pods que no el toleren Els desallotja

NoExecute és l'únic dels tres que actua sobre pods en execució. És el mecanisme que sí que fa el que RequiredDuringExecution hauria fet a l'afinitat.

La sintaxi d'una toleració

spec:
  tolerations:
    # Forma 1: amb operator Equal (per defecte): clau, valor i efecte exactes
    - key: dedicat
      operator: Equal
      value: analitica
      effect: NoSchedule

    # Forma 2: amb operator Exists: la clau existeix, el valor tant se val
    - key: dedicat
      operator: Exists
      effect: NoSchedule

    # Forma 3: tolerar TOT (fer servir amb molt de compte)
    - operator: Exists

Regles de la sintaxi:

  • operator: Equal (el valor per defecte) exigeix que coincideixin key, value i effect.
  • operator: Exists exigeix que coincideixin key i effect; no ha de portar value.
  • Ometre effect significa "qualsevol efecte per a aquella clau".
  • Ometre key amb operator: Exists significa "tots els taints". És el que fan servir els DaemonSets de CNI que han de córrer passi el que passi (06-02).

tolerationSeconds

Només té sentit amb NoExecute, i expressa quant de temps aguanta el pod al node abans de ser desallotjat:

    - key: node.kubernetes.io/unreachable
      operator: Exists
      effect: NoExecute
      tolerationSeconds: 300

"Si el node es torna inabastable, queda-t'hi 5 minuts per si torna; si no, desallotja'm." És exactament el que Kubernetes afegeix per defecte a tots els pods, com veurem ara.

L'advertiment fonamental

Una toleració no atrau el pod: només evita que el rebutgin.

Un pod amb la toleració de dedicat=analitica pot anar als nodes d'analítica, però també a qualsevol altre node del clúster, i probablement acabi en un altre. Per reservar nodes de veritat calen les dues peces:

    spec:
      # 1. El taint del node impedeix que hi entrin els altres
      tolerations:
        - key: dedicat
          operator: Equal
          value: analitica
          effect: NoSchedule
      # 2. L'afinitat fa que aquest pod sí que hi vagi
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: dedicat
                    operator: In
                    values: ["analitica"]

Taint + toleració = exclusivitat. Taint + toleració + afinitat = dedicació.

  1. Taints automàtics de Kubernetes

Kubernetes posa taints pel seu compte quan un node té problemes. Conèixer-los explica molts comportaments que altrament semblen màgics.

Taint Efecte Quan el posa el sistema
node.kubernetes.io/not-ready NoExecute El node no està Ready
node.kubernetes.io/unreachable NoExecute El controlador de nodes va perdre contacte amb el kubelet
node.kubernetes.io/memory-pressure NoSchedule Al node li queda poca memòria
node.kubernetes.io/disk-pressure NoSchedule Al node li queda poc disc
node.kubernetes.io/pid-pressure NoSchedule Al node li queden pocs PID
node.kubernetes.io/network-unavailable NoSchedule La xarxa del node no està configurada
node.kubernetes.io/unschedulable NoSchedule Algú va executar kubectl cordon
node.kubernetes.io/out-of-service NoExecute Un administrador el marca com a fora de servei

I el més conegut, que no és automàtic sinó que el posa kubeadm en crear el clúster:

node-role.kubernetes.io/control-plane:NoSchedule

És el que va obligar el DaemonSet de logs de 06-02 a portar una toleració explícita.

El desallotjament per node caigut

Aquí hi ha el mecanisme real darrere de "si un node cau, els pods es recreen en un altre lloc":

  1. El kubelet deixa d'enviar batecs.
  2. Passats 40 segons, el controlador de nodes marca el node NotReady i li afegeix el taint node.kubernetes.io/unreachable:NoExecute.
  3. Els pods del node no es desallotgen immediatament, perquè Kubernetes els va afegir automàticament aquesta toleració en crear-los:
  tolerations:
    - key: node.kubernetes.io/not-ready
      operator: Exists
      effect: NoExecute
      tolerationSeconds: 300
    - key: node.kubernetes.io/unreachable
      operator: Exists
      effect: NoExecute
      tolerationSeconds: 300
  1. Als 300 segons (5 minuts) s'esgota la tolerància i els pods es marquen per esborrar. Els seus controladors creen substituts en altres nodes.

Això explica el retard d'uns cinc minuts entre que un node cau i que els seus pods reapareixen en un altre lloc. Es pot ajustar per pod:

      # api-reserves és sensible a la latència de recuperació: 30 s en comptes de 300
      tolerations:
        - key: node.kubernetes.io/unreachable
          operator: Exists
          effect: NoExecute
          tolerationSeconds: 30
        - key: node.kubernetes.io/not-ready
          operator: Exists
          effect: NoExecute
          tolerationSeconds: 30

Abaixar-lo molt té el seu risc: un tall de xarxa transitori de 40 segons provocaria un desallotjament massiu i una recreació innecessària. Per a postgres-reserves convé el contrari: un valor alt, perquè recrear la base de dades en un altre node implica desmuntar i tornar a muntar un volum, i és millor esperar que el node torni.

Un avís: els pods de DaemonSet no reben aquestes toleracions automàtiques amb tolerationSeconds; en el seu lloc toleren aquests taints indefinidament, perquè els agents continuïn funcionant en un node amb problemes.

  1. PriorityClass i preemption

Quan el clúster és ple, què passa si arriba un pod important? Sense prioritats, es queda Pending com qualsevol altre. Amb prioritats, pot desallotjar pods menys importants per fer-se lloc.

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: rutasnorte-critica
value: 100000
globalDefault: false
preemptionPolicy: PreemptLowerPriority
description: "Components sense els quals Rutas Norte no pot vendre bitllets"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: rutasnorte-normal
value: 10000
globalDefault: true
description: "Prioritat per defecte de les càrregues de Rutas Norte"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: rutasnorte-lot
value: 100
globalDefault: false
preemptionPolicy: Never
description: "Treballs per lots que poden esperar"

Ús en un pod:

spec:
  template:
    spec:
      priorityClassName: rutasnorte-critica

Les PriorityClass són objectes de clúster, no de namespace. Kubernetes en porta dues de predefinides per a components del sistema, system-cluster-critical (2000000000) i system-node-critical (2000001000); aquesta última és la que fem servir al DaemonSet de logs de 06-02.

Què fa la prioritat

Actua en dos moments:

  1. A la cua del scheduler: els pods pendents s'ordenen per prioritat descendent. Un pod crític s'avalua abans que un de lot, encara que portés menys temps esperant.
  2. A la preemption: si un pod d'alta prioritat no cap en cap node, el scheduler busca un node on, desallotjant pods de menor prioritat, sí que hi cabria. Si el troba, marca aquests pods per esborrar (amb el seu SIGTERM i el seu període de gràcia de 02-01) i programa el pod important al seu lloc.

preemptionPolicy: Never fa que un pod es beneficiï de la prioritat a la cua però no desallotgi mai ningú. És el correcte per a treballs per lots: que es programin aviat si hi ha lloc, però que no tirin res avall.

El perill d'abusar de les prioritats

És un mecanisme que es degrada sol si es fa servir malament:

  • La inflació de prioritats. Cada equip posa la seva càrrega en "crítica". Al final tot és crític i la prioritat deixa de discriminar. Defineix poques classes (tres n'hi ha prou) i documenta què justifica cadascuna.
  • La preemption en cascada. El pod A desallotja el B; el controlador de B el recrea en un altre node; allà desallotja el C… Un clúster al límit amb prioritats mal repartides pot passar minuts movent pods sense estabilitzar-se.
  • Els desallotjaments no respecten PodDisruptionBudget de manera absoluta. El scheduler intenta respectar-los, però si no troba una altra opció, desallotja igualment. Els PDB són tema de 09-05.
  • Els pods amb estat pateixen molt. Desallotjar postgres-reserves implica tancar connexions, desmuntar el volum i tornar-lo a muntar en un altre node. Posa-li prioritat alta i no li posis preemptionPolicy: PreemptLowerPriority esperant que es recol·loqui sol: el que vols és que no el toquin.

Regla pràctica: fes servir prioritats per protegir el que és important de ser desallotjat, no per aconseguir que el que és important desallotgi agressivament.

  1. topologySpreadConstraints: menció i remissió

Existeix un tercer mecanisme, més modern i més eficient que l'antiafinitat per al cas concret de repartir rèpliques de manera equilibrada:

spec:
  topologySpreadConstraints:
    - maxSkew: 1
      topologyKey: topology.kubernetes.io/zone
      whenUnsatisfiable: ScheduleAnyway
      labelSelector:
        matchLabels:
          app: api-reserves
          entorn: pro

En una frase: "la diferència entre la zona amb més rèpliques d'api-reserves i la que en té menys no ha de passar d'1; si no es pot complir, programa'l igualment".

Avantatges davant de podAntiAffinity:

podAntiAffinity topologySpreadConstraints
Expressa "cap de costat d'un altre" "repartits amb desviació màxima N"
Granularitat Binària Numèrica, mitjançant maxSkew
Comportament si no hi cap Pending (amb required) ScheduleAnyway o DoNotSchedule, a triar
Cost de còmput Alt en clústers grans Menor

La seva aplicació completa a l'alta disponibilitat —com combinar-lo amb PodDisruptionBudgets per sobreviure a la pèrdua d'una zona— és contingut de la lliçó 09-05. Aquí n'hi ha prou amb saber que existeix i que, per al cas "reparteix les meves rèpliques", sol ser millor eina que l'antiafinitat.

  1. El pla de planificació de Rutas Norte

Ho ajuntem tot en un pla coherent per a rutas-norte-pro.

Etiquetatge i taints dels nodes

# Nodes amb SSD per a la base de dades
kubectl label node rutas-norte-m02 disc=ssd gamma=alta
kubectl label node rutas-norte-m03 disc=ssd gamma=alta

# Nodes estàndard per a la web i l'API
kubectl label node rutas-norte-m04 disc=hdd gamma=estandar
kubectl label node rutas-norte-m05 disc=hdd gamma=estandar

# Node dedicat a càrregues d'anàlisi, protegit amb taint
kubectl label node rutas-norte-m06 dedicat=analitica
kubectl taint node rutas-norte-m06 dedicat=analitica:NoSchedule
graph TB
  subgraph SSD[Nodes disc=ssd]
    M2[m02] --> PG[postgres-reserves-0]
    M3[m03]
  end
  subgraph EST[Nodes gamma=estandar]
    M4[m04] --> A1[api-reserves]
    M4 --> T1[botiga-web]
    M5[m05] --> A2[api-reserves]
    M5 --> T2[botiga-web]
  end
  subgraph ANA[Node dedicat=analitica<br/>taint NoSchedule]
    M6[m06] --> IO[informes-ocupacio]
  end
  DS[DaemonSet recollector-logs<br/>tolera tot] -.-> M2
  DS -.-> M4
  DS -.-> M6

postgres-reserves: fixat a nodes SSD

# k8s/entorns/pro/postgres-reserves-planificacio.yaml (fragment del StatefulSet)
spec:
  template:
    spec:
      priorityClassName: rutasnorte-critica
      affinity:
        nodeAffinity:
          # Requisit real: sense SSD les consultes d'ocupació no compleixen el seu SLA
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: disc
                    operator: In
                    values: ["ssd", "nvme"]
          # Preferència: millor a la zona on és la resta de la plataforma
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 60
              preference:
                matchExpressions:
                  - key: topology.kubernetes.io/zone
                    operator: In
                    values: ["eu-west-1a"]
        # Mai dues rèpliques de PostgreSQL al mateix node
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: postgres-reserves
                  entorn: pro
              topologyKey: kubernetes.io/hostname
      # Davant d'un node inabastable, esperar més del normal abans de desallotjar:
      # remuntar el volum en un altre node és car
      tolerations:
        - key: node.kubernetes.io/unreachable
          operator: Exists
          effect: NoExecute
          tolerationSeconds: 600

Aquí required a l'antiafinitat que és correcte: dues instàncies de PostgreSQL al mateix node no aporten res i sí que introdueixen contenció de disc. I com que el StatefulSet té una rèplica, no hi ha risc de bloqueig.

api-reserves i botiga-web: repartiment entre nodes

# k8s/entorns/pro/api-reserves-planificacio.yaml (fragment del Deployment)
spec:
  replicas: 6
  template:
    spec:
      priorityClassName: rutasnorte-critica
      affinity:
        nodeAffinity:
          # Fora dels nodes d'analítica i dels de base de dades
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: dedicat
                    operator: DoesNotExist
                  - key: gamma
                    operator: In
                    values: ["estandar", "alta"]
        podAntiAffinity:
          # preferred, no required: amb 6 rèpliques i pocs nodes, required bloquejaria
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchLabels:
                    app: api-reserves
                    entorn: pro
                topologyKey: kubernetes.io/hostname
            - weight: 50
              podAffinityTerm:
                labelSelector:
                  matchLabels:
                    app: api-reserves
                    entorn: pro
                topologyKey: topology.kubernetes.io/zone
      tolerations:
        - key: node.kubernetes.io/unreachable
          operator: Exists
          effect: NoExecute
          tolerationSeconds: 60

Dues regles amb pesos diferents: repartir entre nodes importa el doble que repartir entre zones. El resultat en un desplegament típic:

kubectl get pods -n rutas-norte-pro -l app=api-reserves \
  -o custom-columns=POD:.metadata.name,NODE:.spec.nodeName --sort-by=.spec.nodeName
POD                             NODE
api-reserves-6c8f9d4b7-2m5kx    rutas-norte-m02
api-reserves-6c8f9d4b7-jm2xq    rutas-norte-m03
api-reserves-6c8f9d4b7-p8t4v    rutas-norte-m04
api-reserves-6c8f9d4b7-q1w7z    rutas-norte-m04
api-reserves-6c8f9d4b7-r9k3b    rutas-norte-m05
api-reserves-6c8f9d4b7-x4n6c    rutas-norte-m05

Sis rèpliques en quatre nodes, tan repartides com sigui possible. Amb required la sisena s'hauria quedat Pending. botiga-web porta una configuració equivalent.

informes-ocupacio: al node d'anàlisi

El CronJob de 06-03 fa consultes agregades pesades. Que corri al node dedicat evita que competeixi amb la venda de bitllets:

# k8s/entorns/pro/informes-ocupacio-planificacio.yaml (fragment del jobTemplate)
        spec:
          priorityClassName: rutasnorte-lot      # preemptionPolicy: Never
          tolerations:
            - key: dedicat
              operator: Equal
              value: analitica
              effect: NoSchedule
          affinity:
            nodeAffinity:
              requiredDuringSchedulingIgnoredDuringExecution:
                nodeSelectorTerms:
                  - matchExpressions:
                      - key: dedicat
                        operator: In
                        values: ["analitica"]

Les dues peces juntes, com vam explicar a l'apartat 5: la toleració li permet entrar on ningú més no pot, i l'afinitat l'obliga a anar-hi. La prioritat rutasnorte-lot amb preemptionPolicy: Never garanteix que un informe nocturn no desallotjarà mai res.

El DaemonSet de logs

Ja el vam escriure a 06-02, i ara s'entén del tot:

      tolerations:
        - key: node-role.kubernetes.io/control-plane
          operator: Exists
          effect: NoSchedule
        - key: dedicat
          operator: Equal
          value: analitica
          effect: NoSchedule       # també al node d'anàlisi

Sense la segona toleració, el node m06 no tindria recol·lector i els logs dels informes nocturns no arribarien enlloc: exactament els logs que voldries consultar quan un informe falla a les tres de la matinada.

Resum del pla

Component Afinitat de node Antiafinitat Toleracions Prioritat
postgres-reserves required: disc in [ssd,nvme] required per hostname unreachable 600 s crítica
api-reserves required: no analítica preferred per hostname i zone unreachable 60 s crítica
botiga-web required: no analítica preferred per hostname per defecte normal
redis-cache cap preferred per hostname per defecte normal
worker-notificacions required: no analítica preferred per hostname per defecte normal
informes-ocupacio required: analítica cap dedicat=analitica lot (sense preemption)
recollector-logs (DS) cap no aplica control-plane + analítica system-node-critical

  1. Diagnòstic del pod eternament Pending

Un pod Pending no ha trobat node. El scheduler et diu exactament per què, i aprendre a llegir aquest missatge és l'habilitat més rendible d'aquesta lliçó.

kubectl get pods -n rutas-norte-pro
NAME                            READY   STATUS    RESTARTS   AGE
informes-ocupacio-29178180-p2   0/1     Pending   0          6m
kubectl describe pod informes-ocupacio-29178180-p2 -n rutas-norte-pro | tail -8
Events:
  Type     Reason            Age    From               Message
  ----     ------            ----   ----               -------
  Warning  FailedScheduling  6m12s  default-scheduler  0/6 nodes are available:
           1 node(s) had untolerated taint {dedicat: analitica},
           2 node(s) didn't match Pod's node affinity/selector,
           3 Insufficient cpu.
           preemption: 0/6 nodes are available:
           1 Preemption is not helpful for scheduling,
           5 No preemption victims found for incoming pod.

Com es llegeix aquest missatge

La primera línia dona el recompte: de 6 nodes, 0 disponibles. Després, el desglossament per motiu, i la suma dels números ha de donar 6:

  • 1 node(s) had untolerated taint {dedicat: analitica} → el node m06 va rebutjar el pod perquè no porta la toleració. Si el pod era informes-ocupacio, hi falta la toleració.
  • 2 node(s) didn't match Pod's node affinity/selector → dos nodes no compleixen l'afinitat required.
  • 3 Insufficient cpu → tres nodes no tenen prou CPU lliure per a les requests.

El bloc preemption: explica per què la prioritat tampoc no va ajudar: No preemption victims found significa que no hi ha pods de menor prioritat que, desallotjats, deixarien lloc.

Taula de motius i què fer

Missatge Causa Solució
Insufficient cpu / Insufficient memory No hi ha recursos lliures per a les requests Abaixar requests, afegir nodes, escalar el clúster (09-03)
had untolerated taint {clau: valor} El node té un taint que el pod no tolera Afegir la toleració o treure el taint
didn't match Pod's node affinity/selector L'afinitat required o el nodeSelector no hi casen Revisar les etiquetes del node amb kubectl get nodes --show-labels
didn't match pod anti-affinity rules Ja hi ha un pod del grup a cada domini Canviar a preferred, ampliar topologyKey o afegir nodes
node(s) had volume node affinity conflict El PV és en una altra zona que el node Revisar volumeBindingMode de la StorageClass (05-04)
node(s) were unschedulable Node amb cordon kubectl uncordon <node>
node(s) had no available disk Pressió de disc Alliberar espai al node
exceeded quota (a FailedCreate, no Pending) ResourceQuota del namespace esgotada Ajustar la quota (03-04)

Rutina de diagnòstic

# 1. L'esdeveniment del scheduler: el 90 % de les vegades n'hi ha prou amb això
kubectl describe pod <pod> -n <ns> | grep -A15 Events

# 2. Quant demana el pod?
kubectl get pod <pod> -n <ns> \
  -o jsonpath='{.spec.containers[*].resources.requests}{"\n"}'

# 3. Quant queda lliure a cada node? (mira "Allocated resources")
kubectl describe nodes | grep -A6 "Allocated resources"

# 4. Quines etiquetes tenen els nodes?
kubectl get nodes --show-labels

# 5. Quins taints tenen?
kubectl get nodes -o custom-columns=NODE:.metadata.name,TAINTS:.spec.taints

# 6. Esdeveniments recents del namespace, ordenats
kubectl get events -n <ns> --sort-by=.lastTimestamp | tail -20

Consell de mètode: comença sempre per l'esdeveniment del scheduler. És l'únic lloc on el sistema et diu, amb recompte per motiu, què va descartar cada node. Inspeccionar nodes a ull abans de llegir-lo és perdre el temps.

Errors Comuns i Consells

Confondre toleració amb afinitat. És el malentès més estès del tema. Una toleració només evita el rebuig; no porta el pod enlloc. Per dedicar nodes calen les dues coses.

required on n'hi havia prou amb preferred. Cada regla required és un filtre absolut que pot deixar un pod Pending per sempre. Abans d'escriure-la, pregunta't: "si no es compleix, prefereixo que el pod no s'executi?". Si la resposta és no, va a preferred.

Antiafinitat required amb topologyKey: hostname i moltes rèpliques. Limita les rèpliques al nombre de nodes. Combinada amb un HPA que intenta pujar a 15 en un pic de pont, produeix pods Pending justament quan més falta fan.

Esperar que un canvi d'etiqueta mogui pods. IgnoredDuringExecution significa exactament el que diu. Per moure un pod cal esborrar-lo, o fer servir un taint NoExecute.

Fer servir nodeName en manifests de producció. Se salta el scheduler i amb ell tota la resiliència. Si el node cau, el pod no es recol·loca.

Posar operator: Exists amb value. L'API ho rebutja. Amb Exists no s'especifica valor.

Oblidar el guionet en treure un taint. kubectl taint node m06 dedicat=analitica:NoSchedule afegeix; amb el guionet final treu. Sense el guionet s'acaba amb taints duplicats.

Inflació de PriorityClass. Si tot és crític, res no ho és. Tres classes ben definides i documentades basten per a gairebé qualsevol plataforma.

Preemption sobre càrregues amb estat. Desallotjar postgres-reserves implica desmuntar i remuntar un volum en un altre node. Protegeix-lo amb prioritat alta, però no comptis que la preemption el recol·loqui netament.

Consell: kubectl get pods -o wide --sort-by=.spec.nodeName és l'ordre per verificar d'un cop d'ull que el repartiment és el que esperaves.

Consell: comprova una regla abans d'aplicar-la en producció. Aplica el manifest a rutas-norte-dev, escala a la xifra que faries servir en producció i comprova que cap rèplica no queda Pending. Una antiafinitat required mal calibrada només es manifesta quan el nombre de rèpliques supera el de nodes.

Consell: documenta per què existeix cada regla. Un podAntiAffinity sense comentari és indistingible d'un copiar i enganxar, i ningú no s'atrevirà a tocar-lo d'aquí a un any. Una línia de comentari amb el motiu estalvia molt.

Exercicis

Exercici 1: afinitat de node obligatòria i preferida

Al teu minikube (perfil rutas-norte, diversos nodes), etiqueta un node amb disc=ssd i un altre amb disc=hdd. Crea a rutas-norte-dev un Deployment bd-demo de 2 rèpliques de nginx:1.27.2-alpine que:

  • Exigeixi (required) córrer en nodes amb disc a [ssd, nvme].
  • Prefereixi (preferred, pes 70) nodes amb gamma=alta.

Comprova on es col·loquen. Després escala a 4 rèpliques i observa què passa.

Exercici 2: antiafinitat required i el seu límit

Crea un Deployment api-demo de 2 rèpliques amb antiafinitat required per kubernetes.io/hostname sobre la seva pròpia etiqueta app. Verifica que les dues rèpliques cauen en nodes diferents. Escala a un nombre més gran que el de nodes disponibles i diagnostica el pod Pending llegint l'esdeveniment del scheduler. Corregeix el problema passant a preferred.

Exercici 3: node dedicat amb taint, toleració i afinitat

Tria un node, etiqueta'l dedicat=analitica i posa-li el taint dedicat=analitica:NoSchedule. Després:

  1. Comprova que un pod normal no s'hi col·loca.
  2. Crea un Job analitica-demo que que hi corri, amb toleració i afinitat.
  3. Demostra que només la toleració no n'hi ha prou: crea un pod amb la toleració però sense afinitat i comprova on acaba.
  4. Neteja el taint i les etiquetes.

Solucions

Solució 1

kubectl get nodes
kubectl label node rutas-norte-m02 disc=ssd gamma=alta --overwrite
kubectl label node rutas-norte-m03 disc=hdd gamma=estandar --overwrite
# /tmp/bd-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: bd-demo
  namespace: rutas-norte-dev
  labels:
    app: bd-demo
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
spec:
  replicas: 2
  selector:
    matchLabels:
      app: bd-demo
      entorn: dev
  template:
    metadata:
      labels:
        app: bd-demo
        app.kubernetes.io/part-of: rutas-norte
        entorn: dev
    spec:
      automountServiceAccountToken: false
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: disc
                    operator: In
                    values: ["ssd", "nvme"]
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 70
              preference:
                matchExpressions:
                  - key: gamma
                    operator: In
                    values: ["alta"]
      containers:
        - name: nginx
          image: nginx:1.27.2-alpine
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 200m
              memory: 128Mi
kubectl apply -f /tmp/bd-demo.yaml
kubectl get pods -n rutas-norte-dev -l app=bd-demo -o wide
NAME                       READY   STATUS    RESTARTS   AGE   NODE
bd-demo-7f9c4d8b6-h2m4x    1/1     Running   0          18s   rutas-norte-m02
bd-demo-7f9c4d8b6-t5k7p    1/1     Running   0          18s   rutas-norte-m02

Les dues rèpliques van a m02, l'únic node amb disc=ssd. Que a més tingui gamma=alta li dona 70 punts extra, però era irrellevant: el filtratge ja havia deixat un sol candidat.

kubectl scale deployment bd-demo -n rutas-norte-dev --replicas=4
kubectl get pods -n rutas-norte-dev -l app=bd-demo -o wide
NAME                       READY   STATUS    RESTARTS   AGE   NODE
bd-demo-7f9c4d8b6-h2m4x    1/1     Running   0          2m    rutas-norte-m02
bd-demo-7f9c4d8b6-t5k7p    1/1     Running   0          2m    rutas-norte-m02
bd-demo-7f9c4d8b6-v8n3q    1/1     Running   0          12s   rutas-norte-m02
bd-demo-7f9c4d8b6-w1x6z    1/1     Running   0          12s   rutas-norte-m02

Les quatre al mateix node. L'afinitat de node diu on pot anar un pod, però no diu res sobre repartir. Per a això cal antiafinitat, que és l'exercici 2. Si m02 es reinicia, cau bd-demo sencer.

Solució 2

# /tmp/api-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-demo
  namespace: rutas-norte-dev
  labels:
    app: api-demo
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api-demo
      entorn: dev
  template:
    metadata:
      labels:
        app: api-demo
        app.kubernetes.io/part-of: rutas-norte
        entorn: dev
    spec:
      automountServiceAccountToken: false
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: api-demo
                  entorn: dev
              topologyKey: kubernetes.io/hostname
      containers:
        - name: nginx
          image: nginx:1.27.2-alpine
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 200m
              memory: 128Mi
kubectl apply -f /tmp/api-demo.yaml
kubectl get pods -n rutas-norte-dev -l app=api-demo -o wide
NAME                       READY   STATUS    RESTARTS   AGE   NODE
api-demo-8b5d7c9f4-k3m2x   1/1     Running   0          15s   rutas-norte-m02
api-demo-8b5d7c9f4-r7t9p   1/1     Running   0          15s   rutas-norte-m03

Una per node, tal com es demanava.

kubectl scale deployment api-demo -n rutas-norte-dev --replicas=4
kubectl get pods -n rutas-norte-dev -l app=api-demo -o wide
NAME                       READY   STATUS    RESTARTS   AGE   NODE
api-demo-8b5d7c9f4-k3m2x   1/1     Running   0          2m    rutas-norte-m02
api-demo-8b5d7c9f4-r7t9p   1/1     Running   0          2m    rutas-norte-m03
api-demo-8b5d7c9f4-w4n8q   0/1     Pending   0          20s   <none>
api-demo-8b5d7c9f4-z2v5c   0/1     Pending   0          20s   <none>
kubectl describe pod -n rutas-norte-dev \
  $(kubectl get pod -n rutas-norte-dev -l app=api-demo \
    --field-selector=status.phase=Pending -o jsonpath='{.items[0].metadata.name}') | tail -5
Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  25s   default-scheduler  0/3 nodes are available:
           1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: },
           2 node(s) didn't match pod anti-affinity rules.

El missatge és explícit: dos nodes ja tenen una rèplica i la regla required prohibeix una segona; el tercer és el pla de control, amb el seu taint. Amb antiafinitat required per hostname, el màxim de rèpliques és el nombre de nodes elegibles.

Correcció:

kubectl patch deployment api-demo -n rutas-norte-dev --type=json -p='[
  {"op": "remove", "path": "/spec/template/spec/affinity/podAntiAffinity/requiredDuringSchedulingIgnoredDuringExecution"},
  {"op": "add", "path": "/spec/template/spec/affinity/podAntiAffinity/preferredDuringSchedulingIgnoredDuringExecution",
   "value": [{"weight": 100, "podAffinityTerm": {
     "labelSelector": {"matchLabels": {"app": "api-demo", "entorn": "dev"}},
     "topologyKey": "kubernetes.io/hostname"}}]}
]'

kubectl rollout status deployment/api-demo -n rutas-norte-dev
kubectl get pods -n rutas-norte-dev -l app=api-demo -o wide
NAME                        READY   STATUS    RESTARTS   AGE   NODE
api-demo-6d4f8a7c2-b1n5k    1/1     Running   0          40s   rutas-norte-m02
api-demo-6d4f8a7c2-h9m3t    1/1     Running   0          38s   rutas-norte-m03
api-demo-6d4f8a7c2-p6x2w    1/1     Running   0          35s   rutas-norte-m02
api-demo-6d4f8a7c2-y8k4r    1/1     Running   0          33s   rutas-norte-m03

Quatre rèpliques repartides dues i dues: la preferència va repartir tan bé com va poder i, esgotades les alternatives, va permetre duplicar en comptes de bloquejar.

Solució 3

kubectl label node rutas-norte-m03 dedicat=analitica --overwrite
kubectl taint node rutas-norte-m03 dedicat=analitica:NoSchedule
kubectl get nodes -o custom-columns=NODE:.metadata.name,TAINTS:.spec.taints
NODE              TAINTS
rutas-norte       [map[effect:NoSchedule key:node-role.kubernetes.io/control-plane]]
rutas-norte-m02   <none>
rutas-norte-m03   [map[effect:NoSchedule key:dedicat value:analitica]]

1. Un pod normal no hi va:

kubectl run normal-demo -n rutas-norte-dev --image=nginx:1.27.2-alpine --restart=Never
kubectl wait --for=condition=ready pod/normal-demo -n rutas-norte-dev --timeout=60s
kubectl get pod normal-demo -n rutas-norte-dev -o wide
NAME          READY   STATUS    RESTARTS   AGE   NODE
normal-demo   1/1     Running   0          8s    rutas-norte-m02

Va acabar a m02, l'únic sense taint que el rebutgi.

2. Job amb toleració i afinitat:

# /tmp/analitica-demo.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: analitica-demo
  namespace: rutas-norte-dev
  labels:
    app: informes-ocupacio
    entorn: dev
spec:
  backoffLimit: 1
  ttlSecondsAfterFinished: 3600
  template:
    metadata:
      labels:
        app: informes-ocupacio
        entorn: dev
    spec:
      restartPolicy: Never
      automountServiceAccountToken: false
      tolerations:
        - key: dedicat
          operator: Equal
          value: analitica
          effect: NoSchedule
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: dedicat
                    operator: In
                    values: ["analitica"]
      containers:
        - name: calcul
          image: busybox:1.36
          command: ["sh", "-c", "echo 'calculant ocupació al node dedicat'; sleep 10"]
          resources:
            requests:
              cpu: 20m
              memory: 32Mi
            limits:
              cpu: 100m
              memory: 64Mi
kubectl apply -f /tmp/analitica-demo.yaml
kubectl get pods -n rutas-norte-dev -l job-name=analitica-demo -o wide
NAME                   READY   STATUS    RESTARTS   AGE   NODE
analitica-demo-m4k2x   1/1     Running   0          6s    rutas-norte-m03

3. Només la toleració no n'hi ha prou:

# /tmp/nomes-toleracio.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nomes-toleracio
  namespace: rutas-norte-dev
  labels:
    app: nomes-toleracio
    entorn: dev
spec:
  automountServiceAccountToken: false
  tolerations:
    - key: dedicat
      operator: Equal
      value: analitica
      effect: NoSchedule
  containers:
    - name: nginx
      image: nginx:1.27.2-alpine
      resources:
        requests:
          cpu: 50m
          memory: 64Mi
        limits:
          cpu: 200m
          memory: 128Mi
kubectl apply -f /tmp/nomes-toleracio.yaml
kubectl wait --for=condition=ready pod/nomes-toleracio -n rutas-norte-dev --timeout=60s
kubectl get pod nomes-toleracio -n rutas-norte-dev -o wide
NAME              READY   STATUS    RESTARTS   AGE   NODE
nomes-toleracio   1/1     Running   0          9s    rutas-norte-m02

Podia haver anat a m03 i no hi va anar: la toleració permet però no atrau. El scheduler va triar m02 per la seva puntuació. Això confirma l'advertiment de l'apartat 5: per dedicar nodes calen taint + toleració + afinitat.

4. Neteja:

kubectl taint node rutas-norte-m03 dedicat=analitica:NoSchedule-
kubectl label node rutas-norte-m03 dedicat-
kubectl label node rutas-norte-m02 disc- gamma- --overwrite
kubectl label node rutas-norte-m03 disc- gamma- --overwrite
kubectl delete -f /tmp/analitica-demo.yaml -f /tmp/nomes-toleracio.yaml \
                 -f /tmp/api-demo.yaml -f /tmp/bd-demo.yaml
kubectl delete pod normal-demo -n rutas-norte-dev

Conclusió

El kube-scheduler decideix en dues fases: filtra els nodes inviables i puntua els que queden. Aquesta estructura explica tota la lliçó: les regles required actuen en el filtratge i poden deixar un pod Pending; les preferred actuen en la puntuació i només influeixen en l'elecció.

Hem recorregut els mecanismes de menys a més expressiu: nodeName (força bruta, només per depurar), nodeSelector (igualtat simple), afinitat de node amb els seus nodeSelectorTerms en OR, els seus matchExpressions en AND i els seus operadors In, NotIn, Exists, DoesNotExist, Gt i Lt. L'IgnoredDuringExecution dels seus noms llargs significa que un canvi a les etiquetes del node no mourà mai un pod ja programat.

L'afinitat i antiafinitat entre pods miren qui més hi ha al domini que defineix topologyKey: node, zona o regió, segons de quina fallada et vulguis protegir. El seu cost de còmput és real en clústers grans, i per al cas concret de repartir rèpliques convé mirar cap a topologySpreadConstraints, l'ús del qual per a alta disponibilitat és contingut de 09-05.

Els taints inverteixen la relació: és el node qui rebutja, amb tres efectes (NoSchedule, PreferNoSchedule i NoExecute, l'únic que desallotja pods en marxa). Kubernetes els fa servir pel seu compte quan un node falla, i les toleracions automàtiques amb tolerationSeconds: 300 expliquen el retard de cinc minuts abans que els pods d'un node caigut reapareguin en un altre lloc. La regla que cal gravar: una toleració permet, no atrau; per dedicar nodes calen taint, toleració i afinitat.

Les PriorityClass ordenen la cua i habiliten la preemption, amb el risc de la inflació de prioritats i de les cascades de desallotjament. I l'esdeveniment FailedScheduling, amb el seu recompte de nodes descartats per cada motiu, és la primera i gairebé sempre l'única parada del diagnòstic d'un pod Pending.

Amb això, el pla de planificació de Rutas Norte queda complet: postgres-reserves als nodes amb SSD, api-reserves i botiga-web repartides entre nodes, un node d'anàlisi reservat per a informes-ocupacio i un DaemonSet de logs que arriba a tot arreu.

Fins aquí hem fet servir els objectes que Kubernetes porta de fàbrica: Pods, Deployments, StatefulSets, Services, PVC, Jobs. Però al mòdul 4 vam instal·lar cert-manager i vam començar a crear objectes de tipus Certificate i Issuer, i al mòdul 5 vam crear VolumeSnapshot. Cap d'aquests tipus no forma part de Kubernetes: algú els va afegir a l'API, i el clúster els tracta exactament igual que els natius —kubectl get, describe, explain, validació, RBAC—. Com es fa això, i com definiríem el nostre propi tipus RutaProgramada per a Rutas Norte, és el tema de la lliçó següent: les Definicions de Recursos Personalitzats.

Curs de Kubernetes

Mòdul 1: Introducció a Kubernetes

Mòdul 2: Components Principals de Kubernetes

Mòdul 3: Gestió de Configuració i Secrets

Mòdul 4: Xarxes a Kubernetes

Mòdul 5: Emmagatzematge a Kubernetes

Mòdul 6: Conceptes Avançats de Kubernetes

Mòdul 7: Monitoratge i Registre

Mòdul 8: Seguretat a Kubernetes

Mòdul 9: Escalat i Rendiment

Mòdul 10: Ecosistema i Eines de Kubernetes

Mòdul 11: Estudis de Cas i Aplicacions del Món Real

Mòdul 12: Preparació per a la Certificació de Kubernetes

© Copyright 2026. Tots els drets reservats