Agendamento de Pods no Kubernetes: Affinity, Taints, Tolerations e Topology Spread
Guia técnico do scheduler do Kubernetes: nodeSelector, affinity e anti-affinity, taints e tolerations, topology spread e as armadilhas que deixam Pods Pending.
Neste artigo
Quando você cria um Pod no Kubernetes, uma pergunta silenciosa precisa ser respondida antes de qualquer coisa acontecer: em qual nó esse Pod vai rodar? Na maioria das vezes você nem pensa nisso, porque o scheduler do Kubernetes decide sozinho, colocando o Pod num nó qualquer que tenha recursos disponíveis. E para muitas cargas isso basta. Mas basta até o dia em que a colocação importa. Você tem três réplicas da sua API e o scheduler, sem saber melhor, empilhou todas no mesmo nó — aí esse nó cai e sua "alta disponibilidade" some inteira de uma vez. Ou você tem nós com GPU caríssima e um monte de Pod comum se instalou neles, ocupando o espaço que deveria ser dos treinos de machine learning. Ou você precisa que um cache fique perto da aplicação que o consome, e eles acabaram em zonas de disponibilidade opostas, pagando latência à toa.
Esses são problemas de agendamento, e o Kubernetes oferece um conjunto rico de mecanismos para você influenciar as decisões do scheduler sem microgerenciá-las. Entender essas ferramentas — nodeSelector, affinity, taints, tolerations e topology spread — é o que separa quem sofre com colocações ruins de quem projeta a distribuição dos Pods deliberadamente. Este guia explica como o scheduler pensa e como cada mecanismo o guia, com atenção especial às armadilhas que deixam Pods eternamente Pending, o sintoma mais comum de agendamento mal configurado.
Como o scheduler pensa
Antes das ferramentas, o modelo mental. O scheduler do Kubernetes, para cada Pod que precisa de nó, roda um processo de duas fases. Primeiro, o filtering: ele elimina todos os nós que não podem rodar o Pod — os que não têm recursos suficientes, os que não batem com as restrições declaradas, os que estão marcados como indisponíveis. Sobra um conjunto de nós viáveis. Segundo, o scoring: ele pontua os nós viáveis segundo várias heurísticas (espalhamento, uso de recursos, afinidades) e escolhe o de maior pontuação. O Pod é então vinculado (bound) àquele nó.
Toda ferramenta de agendamento age numa dessas duas fases, e essa distinção é a chave para entender tudo o que vem a seguir. Algumas restrições são rígidas (hard): elas atuam no filtering, eliminando nós — se nenhum nó sobreviver, o Pod fica Pending, sem lugar para ir. Outras são suaves (soft): elas atuam no scoring, apenas preferindo certos nós — se a preferência não puder ser atendida, o Pod ainda roda, só que num nó menos ideal. Confundir rígido com suave é a origem de metade dos problemas de agendamento: você quer uma preferência e escreve uma exigência, e de repente o Pod não sobe.
nodeSelector: o começo simples
A forma mais elementar de dizer "coloque este Pod nesse tipo de nó" é o nodeSelector. Nós têm labels — disktype=ssd, zone=sa-east-1a, gpu=true —, e o nodeSelector diz "só rode em nós que tenham estes labels". É uma restrição rígida e binária.
```yaml apiVersion: v1 kind: Pod metadata: name: app spec: nodeSelector: disktype: ssd containers:
- name: app
image: registry.exemplo.io/app:1.0 ```
Esse Pod só roda em nós rotulados disktype=ssd. Se não houver nenhum, ele fica Pending. O nodeSelector é ótimo pela simplicidade, mas é limitado: só faz igualdade de label, só faz restrição rígida, e não expressa preferências nem lógicas mais ricas. Para tudo além do trivial, você sobe para affinity.
Node affinity: mais expressivo, com preferências
A nodeAffinity é o nodeSelector turbinado. Ela permite operadores além da igualdade (In, NotIn, Exists, Gt, Lt), e — o mais importante — distingue explicitamente exigências de preferências, com dois modos de nome intimidador mas lógica simples:
requiredDuringSchedulingIgnoredDuringExecution— a parte "required" é a exigência rígida: o nó precisa satisfazer, senão o Pod não é agendado ali. É o equivalente ao nodeSelector, com mais poder de expressão.preferredDuringSchedulingIgnoredDuringExecution— a parte "preferred" é a preferência suave: o scheduler tenta atender, com um peso que você define, mas se não conseguir, agenda o Pod de qualquer jeito.
O sufixo IgnoredDuringExecution, comum a ambos, significa que essas regras valem no momento do agendamento, mas não são reavaliadas depois — se o label do nó mudar com o Pod já rodando, o Pod não é expulso.
```yaml spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms:
- matchExpressions:
- key: disktype
operator: In values: [ssd, nvme] preferredDuringSchedulingIgnoredDuringExecution:
- weight: 50
preference: matchExpressions:
- key: zone
operator: In values: [sa-east-1a] ```
Aqui o Pod exige um nó com disco ssd ou nvme, e prefere (mas não exige) a zona sa-east-1a. É a combinação típica: uma exigência dura no que é inegociável, uma preferência macia no que seria bom ter.
Pod affinity e anti-affinity: a relação entre Pods
Até agora falamos de relacionar Pods com nós. Mas muitas decisões dependem de relacionar Pods com outros Pods. É isso que podAffinity e podAntiAffinity fazem, e são as ferramentas mais valiosas para resiliência e performance.
A anti-affinity diz "não coloque este Pod perto daquele". O caso de uso matador é espalhar réplicas: você quer que as três réplicas da sua API fiquem em nós diferentes, para que a queda de um nó não leve mais de uma junto. Com anti-affinity entre os Pods da mesma aplicação, o scheduler os empurra para nós distintos.
```yaml spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels: app: minha-api topologyKey: kubernetes.io/hostname ```
O topologyKey é o conceito central: ele define o que significa "perto". Com kubernetes.io/hostname, "perto" quer dizer "no mesmo nó" — então essa regra proíbe dois Pods minha-api no mesmo nó. Se o topologyKey fosse um label de zona, "perto" significaria "na mesma zona", e a regra espalharia as réplicas por zonas. O topologyKey é o que permite raciocinar em qualquer nível de domínio de falha.
A affinity é o inverso: "coloque este Pod perto daquele". Útil quando dois componentes se comunicam intensamente e você quer minimizar latência de rede colocando-os na mesma zona, ou até no mesmo nó. Mas cuidado com o exagero, que veremos nas armadilhas: affinity rígida demais amontoa Pods e derruba a resiliência que a anti-affinity tenta construir.
Taints e tolerations: o nó que repele
Affinity é o Pod dizendo onde quer ir. Taints e tolerations são o mecanismo espelho: o nó dizendo quem ele aceita. Um taint é uma marca no nó que o torna repelente — por padrão, nenhum Pod é agendado ali. Uma toleration é uma permissão no Pod que diz "eu tolero esse taint", habilitando-o a ser agendado no nó marcado.
A lógica é de exclusividade e reserva. Você tem nós com GPU cara e quer que só cargas de GPU rodem neles. Você marca esses nós com um taint (gpu=true:NoSchedule), e agora nenhum Pod comum vai parar ali, porque não tolera o taint. Só os Pods que você deliberadamente equipou com a toleration correspondente conseguem entrar. Assim o nó caro fica reservado para quem deve usá-lo.
```yaml # no nó (via kubectl): # kubectl taint nodes no-gpu-1 gpu=true:NoSchedule
# no Pod que PODE usar o nó de GPU: spec: tolerations:
- key: gpu
operator: Equal value: "true" effect: NoSchedule ```
O effect do taint tem três níveis de dureza. NoSchedule impede agendar novos Pods que não toleram, mas deixa em paz os que já estão lá. PreferNoSchedule é a versão suave: o scheduler evita o nó, mas usa se preciso. NoExecute é o mais duro: além de barrar novos, ele expulsa Pods já rodando que não toleram o taint — é o mecanismo por trás da evacuação automática de um nó marcado como problemático. É essencial notar que taints/tolerations e affinity são complementares e não substituíveis: uma toleration apenas permite que o Pod vá para um nó marcado; ela não força nada. Para reservar nós de verdade — só as cargas certas neles —, você combina taint no nó (para repelir os outros) com affinity no Pod (para atraí-lo para lá).
Topology Spread Constraints: espalhar com precisão
A anti-affinity espalha, mas de forma binária — "não juntos". Muitas vezes você quer algo mais nuançado: "distribua minhas réplicas o mais uniformemente possível pelas zonas, tolerando um pequeno desequilíbrio". É para isso que existe o topologySpreadConstraints, uma ferramenta mais moderna e granular de distribuição.
```yaml spec: topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: minha-api ```
O maxSkew é a diferença máxima tolerada entre a zona mais cheia e a mais vazia. Com maxSkew: 1, o scheduler mantém as réplicas equilibradas, nunca deixando uma zona com mais de um Pod a mais que outra. O whenUnsatisfiable decide o comportamento quando o equilíbrio não é possível: DoNotSchedule torna a regra rígida (não agenda se desequilibraria demais — risco de Pending), e ScheduleAnyway a torna suave (agenda mesmo desequilibrando, apenas preferindo o equilíbrio). O topology spread é hoje a forma preferida de distribuir cargas por domínios de falha, porque expressa grau de espalhamento, não só a proibição de coabitar.
As armadilhas — o Pod que não sai do Pending
A armadilha número um, de longe, é o Pod Pending por restrição rígida impossível. Você escreveu uma exigência que nenhum nó satisfaz — um label que não existe em nó nenhum, uma anti-affinity que exige mais nós distintos do que você tem, um topology spread rígido que não fecha, uma toleration que faltou para um nó com taint. O scheduler filtra todos os nós, sobra zero, e o Pod fica preso. O diagnóstico está sempre nos eventos: kubectl describe pod mostra a mensagem do scheduler explicando por que nenhum nó serviu. Ler essa mensagem é o primeiro reflexo diante de um Pending; ela quase sempre nomeia a restrição que não fechou.
A segunda: confundir rígido com suave. Você queria uma preferência e escreveu um required. Agora, quando a preferência não pode ser atendida, em vez de agendar num nó menos ideal, o Pod não agenda de jeito nenhum. Regra de bolso: use required apenas para o que é genuinamente inegociável (uma carga de GPU precisa de nó com GPU), e preferred/ScheduleAnyway para tudo que é "seria bom, mas não à custa de não subir".
A terceira: anti-affinity rígida com nós insuficientes. Você exige que cada réplica fique num nó distinto (anti-affinity required com topologyKey de hostname) e pede cinco réplicas num cluster de três nós. As três primeiras sobem, uma por nó; as outras duas ficam Pending para sempre, porque não há um quarto e quinto nó distintos. A anti-affinity suave (preferred) ou o topology spread com ScheduleAnyway resolvem, permitindo empilhar quando não há alternativa, ainda preferindo espalhar.
A quarta: affinity que amontoa e mata a resiliência. O oposto do problema anterior. Uma podAffinity mal calibrada, atraindo Pods para perto uns dos outros, pode concentrar réplicas num punhado de nós — exatamente o que você não quer para disponibilidade. Affinity de co-localização é para casos específicos de latência; abusar dela desfaz o espalhamento que a resiliência exige.
A quinta: esquecer taints do sistema. Nós têm taints automáticos em condições anormais (nó não-pronto, sob pressão de memória, sendo drenado). Cargas críticas às vezes precisam tolerar certos taints de sistema para se comportar bem durante manutenção — mas tolerar taints demais, ou o taint errado, faz o Pod insistir em rodar num nó doente que deveria estar evacuando. Toleration é permissão; distribua-a com parcimônia.
Fechando
O scheduler do Kubernetes toma, para cada Pod, uma decisão que na maior parte do tempo é invisível e correta — mas que, quando a colocação importa, precisa ser guiada com intenção. As ferramentas formam um vocabulário coerente uma vez que você as vê pela lente de "rígido versus suave" e "Pod-para-nó versus Pod-para-Pod". O nodeSelector e a node affinity dizem em que tipo de nó o Pod quer rodar; a pod affinity e anti-affinity relacionam Pods entre si, com o topologyKey definindo o que significa "perto"; taints e tolerations invertem a perspectiva e deixam o nó escolher quem aceita, reservando hardware especial; e o topology spread distribui réplicas por domínios de falha com o controle fino do maxSkew. Domine a diferença entre exigência e preferência, combine taint com affinity quando precisar reservar nós de verdade, prefira espalhar com anti-affinity e topology spread para não perder réplicas junto com um nó — e, acima de tudo, quando um Pod ficar preso em Pending, leia o que o scheduler diz nos eventos antes de qualquer outra coisa. É lá que a restrição impossível sempre confessa.