Kubernetes scheduler simulator

The scheduler places one pod at a time: it throws out the nodes that cannot run the pod, scores the ones that can, and binds the pod to the best. Describe your nodes and pods in a few lines, then step through each decision with the same filters, scores and Pending messages as kube-scheduler.

Scheduled
0/0
Pending
0
Examples
Resource score

Node: name zone=a cpu=4 mem=16Gi, optionally taint=key=value:NoSchedule, label=disk=ssd, cordon. Pod: name x3 app=web cpu=500m mem=512Mi, optionally tolerate=key=value, select=disk=ssd, anti=host or anti=zone, spread=1 (maxSkew across zones).

Cluster · 0 of 6 pods processed
node-1zone a
CPU0 / 4
Memory0 / 16Gi
no pods
node-2zone a
CPU0 / 4
Memory0 / 16Gi
no pods
node-3zone b
CPU0 / 4
Memory0 / 16Gi
no pods

Press Schedule next pod to place the first pod in the queue.

Events
  1. No pods scheduled yet.

Scores are NodeResourcesFit plus NodeResourcesBalancedAllocation, each with weight 1, computed as in kube-scheduler 1.36. The other default score plugins rate every node the same here, because none of these pods sets a preference.

How to use the simulator

Write one node per line and one pod (or x3 replicas of it) per line, using Kubernetes quantities such as 500m CPU and 512Mi memory, or pick an example. Pods are scheduled in the order listed. Schedule next pod places one at a time: the table shows every node's filter result and scores for that pod, and the event list shows what kubectl describe pod would report.

How the scheduler picks a node

kube-scheduler watches for pods that have no node, takes them from a queue one at a time, and for each one runs a scheduling cycle built from plugins. Filter plugins each check one rule and remove the nodes that break it; the nodes that remain are feasible. Score plugins then rate each feasible node from 0 to 100; each rating is multiplied by the plugin's weight and the results are added. The pod is bound to the node with the highest total. If no node is feasible, the pod stays Pending and the scheduler records a FailedScheduling event saying why.

In the default configuration of Kubernetes 1.36 the score weights are 3 for TaintToleration, 2 each for NodeAffinity, PodTopologySpread and InterPodAffinity, and 1 each for NodeResourcesFit, NodeResourcesBalancedAllocation and ImageLocality. The first four only differ between nodes when a pod or node states a preference (a PreferNoSchedule taint, a preferred affinity, a ScheduleAnyway spread constraint); the required versions of the same rules are filters.

Filters and the Pending message

Filters run in plugin order and a node is out at the first one that rejects it, so each node contributes one reason (NodeResourcesFit can name both CPU and memory). The FailedScheduling message counts the reasons and lists them in alphabetical order: in the Pending pod example, a pod requesting 3 CPUs and 6 GiB gets

0/4 nodes are available: 1 Insufficient cpu, 1 Insufficient memory, 1 node(s) had untolerated taint(s), 1 node(s) were unschedulable.

One node has too little CPU, one too little memory, one a GPU taint the pod does not tolerate, and one is cordoned. The real event continues with a preemption: part saying whether evicting lower-priority pods would help. The reasons map to fixes:

  • Insufficient cpu / memory: lower the request, free capacity, or add nodes (the cluster autoscaler adds them when pods are Pending for this reason).
  • untolerated taint(s): the node is reserved; add a toleration if the pod belongs there.
  • didn't match Pod's node affinity/selector: no node has the labels the pod asks for.
  • didn't match pod anti-affinity rules or topology spread constraints: the placement rules leave no room; add nodes in other zones or relax the rule.
  • were unschedulable: the node is cordoned, usually for maintenance.

Scores: spreading and packing

The NodeResourcesFit score has two built-in strategies. LeastAllocated, the default, scores each resource as (allocatable − requested) × 100 / allocatable, counting the new pod, and averages CPU and memory; empty nodes score highest, so pods spread out. MostAllocated scores requested × 100 / allocatable, so the fullest node that still fits wins and pods pack together. A 1 CPU, 2 GiB pod on an empty node with 4 CPUs and 16 GiB scores 81 with LeastAllocated and 18 with MostAllocated.

NodeResourcesBalancedAllocation prefers nodes where CPU and memory end up used in similar proportions, so that one resource is not exhausted while the other sits idle. It takes the standard deviation of the requested fractions (for two resources, half their difference), scores (1 − deviation) × 100 with and without the new pod, and since Kubernetes 1.36 rewards the change: 50 plus half of (50 + score with − score without). The pod above gets 71, for a total of 152.

In the Spread or pack example, LeastAllocated puts the six pods on 3 nodes (50%, 75%, 75% of CPU requested) and MostAllocated on 2 (100%, 100%, 0%), leaving a node empty. Spreading limits the damage when a node fails; packing leaves room for large pods and lets the cluster autoscaler remove idle nodes. The strategy is set in the scheduler configuration, not per pod.

Requests, not usage

Everything above uses the pods' resource requests, not what they actually consume. A node is full to the scheduler when the requests of its pods add up to its allocatable capacity, even if they are idle. This is what makes scheduling predictable, and it is why requests should be close to real needs: too high and nodes look full while idle, too low and pods are placed on nodes that cannot run them. For scoring, a container with no request is counted as 100 millicores of CPU and 200 MiB of memory.

Spreading has a cost of its own: fragmentation. In the Fragmentation example, six 1.25-CPU pods spread over three 4-CPU nodes leave 1.5 CPUs free on each, 4.5 in all, and a 2-CPU pod cannot fit anywhere: 0/3 nodes are available: 3 Insufficient cpu. With MostAllocated the same six pods fill two nodes and the large pod lands on node-3.

Taints, tolerations and selectors

A taint on a node repels pods: with the NoSchedule effect, no new pod is placed there unless it has a matching toleration (NoExecute also evicts running pods that do not tolerate it). Taints keep ordinary workloads off special nodes such as GPU machines or the control plane. A toleration only permits a pod onto a tainted node; to make it go there, the pod also needs a nodeSelector or node affinity on the node's labels, which the NodeAffinity filter enforces.

Anti-affinity and topology spread

Required pod anti-affinity keeps pods that match a label selector out of the same topology domain, such as the same node (kubernetes.io/hostname) or zone. It is all or nothing: with two nodes and three replicas that must not share a node, the third stays Pending with 0/2 nodes are available: 2 node(s) didn't match pod anti-affinity rules. The rule is checked both ways: a new pod is also kept off nodes where an existing pod's anti-affinity rejects it.

Topology spread constraints are the more flexible tool. With maxSkew: 1 over zones and whenUnsatisfiable: DoNotSchedule, a node is filtered out if placing the pod there would leave its zone more than one matching pod ahead of the zone with the fewest. In the Zone spread example the five web pods go node-1, node-3, node-4, node-2, node-3: the third goes to the small node in zone c even though larger nodes score higher, because zone c had none.

About the model

The simulator implements the default filters that depend on what you can describe here (NodeUnschedulable, TaintToleration, NodeAffinity through nodeSelector, NodeResourcesFit, PodTopologySpread with DoNotSchedule over zones, and required InterPodAffinity anti-affinity on the pod's own app label) and the two resource scores with kube-scheduler's integer arithmetic. It leaves out priorities and preemption, volumes, ports, pod-count limits, image locality and preferred rules, and it schedules pods strictly in the order given, where the real queue orders by priority and retries pending pods when the cluster changes.

Kubernetes scheduling questions and answers

How does the Kubernetes scheduler choose a node?

For each pending pod it runs filter plugins that remove nodes that cannot run it (not enough requested CPU or memory, an untolerated taint, a nodeSelector that does not match, affinity or topology spread rules), then score plugins that rate the rest from 0 to 100. Each score is multiplied by its plugin weight and added up; the node with the highest total wins, with ties broken at random.

Why is my pod Pending with "0/3 nodes are available"?

No node passed the filters, and the message counts why each node failed. "Insufficient cpu" means the pod's CPU request does not fit in what is left of a node's allocatable CPU after other pods' requests. In this page's fragmentation example, three nodes each have 1.5 CPUs free, 4.5 in total, but a pod asking for 2 gets "0/3 nodes are available: 3 Insufficient cpu."

What does "node(s) had untolerated taint(s)" mean?

The node has a taint with the NoSchedule or NoExecute effect that the pod has no matching toleration for. Nodes for special hardware, such as GPUs, are often tainted so that only pods that tolerate the taint run there. Add a toleration to the pod, or schedule it elsewhere.

What is the difference between LeastAllocated and MostAllocated?

They are the two built-in strategies of the NodeResourcesFit score. LeastAllocated, the default, prefers nodes with the most free capacity and spreads pods out; MostAllocated prefers the fullest nodes that still fit and packs pods together, leaving whole nodes empty for the cluster autoscaler to remove. Four web pods and two workers end up on 3 nodes with LeastAllocated and 2 with MostAllocated here.

Does the scheduler look at actual CPU usage?

No. It compares the pods' resource requests with the node's allocatable capacity. A node whose pods request all its CPU is full to the scheduler even if they are idle, and a pod with no requests can be placed on a node that is busy. For scoring, a container with no request counts as 100 millicores and 200 MiB.

What is maxSkew in topology spread constraints?

The largest difference allowed between the number of matching pods in any topology domain, such as a zone, and the domain with the fewest. With maxSkew 1 and whenUnsatisfiable: DoNotSchedule, a node is filtered out if placing the pod there would put its zone more than one pod ahead of the emptiest zone.

Further reading

Published Updated