O que o scheduler faz
Quando você cria um pod, ele nasce no estado Pending. Sem nó atribuído. O scheduler (kube-scheduler) assiste o API Server por pods novos (?watch=true) e decide em qual nó cada um vai executar.
A decisão tem duas fases: predicates (filtros) e priorities (ranqueamento).
Fase 1: Predicates (filtros)
Predicates são regras booleanas. Se o nó não passa em qualquer predicate, é descartado. Os principais:
- NodeUnschedulable: o nó está marcado como unschedulable (
kubectl cordon) - NodeHasSufficientMemory: o nó tem RAM disponível pra soma dos
resources.requestsde todos os containers - NodeHasSufficientCPU: o nó tem CPU disponível
- TaintToleration: o pod tolera os taints do nó
- NodePorts: a porta NodePort que o pod pede não conflita com outro pod
- NodeAffinity: o nó atende os requisitos de affinity do pod
Em clusters de produção, o control plane tipicamente tem o taint node-role.kubernetes.io/control-plane:NoSchedule. Isso impede que workloads comuns sejam agendadas no nó de controle. Neste cluster, o taint foi removido para permitir scheduling em todos os nós.
Fase 2: Priorities (ranqueamento)
Depois de filtrar, o scheduler ranqueia os nós que sobraram. Cada priority function dá uma pontuação de 0 a 10. O nó com maior soma ganha.
Na configuração padrão do Kubernetes, o scheduler usa LeastRequestedPriority + BalancedResourceAllocation. Isso tende a espalhar pods entre os nós disponíveis.
Outras priority functions:
- MostRequestedPriority: prefere nós mais ocupados (empacota, economiza custo)
- ImageLocality: prefere nós que já têm a imagem do container em cache
- NodeAffinityPriority: dá mais pontos pra nós que batem com node affinity
Mão na massa: com e sem scheduler
Vamos criar dois pods e ver a diferença nos eventos.
Pod 1: com nodeName (bypass do scheduler)
Quando você especifica nodeName no YAML, o scheduler é ignorado. O pod vai direto pro nó indicado:
kubectl run scheduler-bypass --image=nginx --restart=Never --overrides='{"spec":{"nodeName":"ip-172-31-34-6"}}'pod/scheduler-bypass createdAgora olha os eventos:
kubectl describe pod scheduler-bypass | grep -A5 Events
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Pulling 15m kubelet Pulling image "nginx"
Normal Pulled 15m kubelet Successfully pulled image "nginx" in 4.319s (4.319s including waiting). Image size: 63120520 bytes.
Normal Created 15m kubelet Created container: scheduler-bypass
Normal Started 15m kubelet Started container scheduler-bypassRepara: não tem evento Scheduled. O kubelet foi direto criar o container. O scheduler nem foi consultado.
Pod 2: sem nodeName (scheduler age)
Agora deixa o scheduler decidir:
kubectl run natural-pod --image=nginx --restart=Never
pod/natural-pod createdEventos:
kubectl describe pod natural-pod | grep -A10 Events
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 15m default-scheduler Successfully assigned default/natural-pod to ip-172-31-34-6
Normal Pulling 15m kubelet Pulling image "nginx"
Normal Pulled 15m kubelet Successfully pulled image "nginx" in 4.779s (4.779s including waiting). Image size: 63120520 bytes.
Normal Created 15m kubelet Created container: natural-pod
Normal Started 15m kubelet Started container natural-podAgora tem Scheduled: default-scheduler atribuiu o pod ao ip-172-31-34-6. Depois o kubelet criou o container. O scheduler não executa nada, ele só escreve o nome do nó no campo .spec.nodeName do pod (via API Server). Quem age é o kubelet.
Os dois pods foram pro mesmo nó (ip-172-31-34-6), mas por caminhos completamente diferentes:
kubectl get pods scheduler-bypass natural-pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
scheduler-bypass 1/1 Running 0 15m 10.0.1.167 ip-172-31-43-16 <none> <none>
natural-pod 1/1 Running 0 15m 10.0.2.232 ip-172-31-34-6 <none> <none>Node Affinity
Você pode influenciar o scheduler com node affinity. Em vez de fixar um nodeName (rígido, sem fallback), você expressa uma preferência ou requisito.
Vamos etiquetar o worker com disktype=ssd:
kubectl label node ip-172-31-34-6 disktype=ssd
node/ip-172-31-34-6 labeledConferindo:
kubectl get nodes --show-labels | grep disktype
ip-172-31-34-6 Ready <none> 20m v1.32.13 ...,disktype=ssd,...
ip-172-31-43-16 Ready <none> 21m v1.32.13 ...,disktype=ssd,...Agora um pod que só agenda em nós com SSD:
apiVersion: v1
kind: Pod
metadata:
name: affinity-pod
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disktype
operator: In
values:
- ssd
containers:
- name: nginx
image: nginxkubectl apply -f affinity-pod.yaml kubectl get pod affinity-pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
affinity-pod 1/1 Running 0 10s 10.0.3.x ip-172-31-34-6 <none> <none>O scheduler respeitou a affinity e colocou o pod no ip-172-31-34-6 (único nó com disktype=ssd).
requiredDuringScheduling: se nenhum nó atender, o pod fica Pending pra sempre. Há também preferredDuringScheduling: "prefira nós com label X, mas se não tiver, pode agendar em qualquer um". O preferred é peso, não requisito.
O scheduler real
kubectl get pod -n kube-system kube-scheduler-ip-172-31-45-35 -o yaml
Argumentos principais:
kube-scheduler
--bind-address=127.0.0.1
--kubeconfig=/etc/kubernetes/scheduler.conf
--leader-elect=true--leader-elect=true: em HA, só o leader toma decisões. Os outros schedulers ficam em standby.--bind-address=127.0.0.1: métricas e healthz só em localhost (porta 10259, HTTPS).
Por que um scheduler separado?
O scheduler poderia ser parte do controller manager. Mas é separado por dois motivos:
- Extensibilidade: você pode trocar o scheduler padrão por um customizado (ex: scheduler que considera preço de spot instances na AWS, ou latência entre nós)
- Separação de responsabilidades: cada componente faz uma coisa só. Scheduler decide onde. Controller manager reconcilia estado. API Server serve a API.