KubernetesLXXI · SchedulerScheduler
Scoring — least allocated, balanced, topology, custom
What you'll learn
- Describe the score plugins and what they optimise
- Walk a Pod's score evaluation across feasible nodes
- Reason about tied scores and order of evaluation
- Identify the cause of unwanted placements
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
After filter establishes the feasible nodes, score ranks them. Each score plugin evaluates the node and returns a score (0-100). The total score is a weighted sum of the plugin scores; the highest-score node is the chosen one. This lesson walks the score plugins and how their weights affect placement.
The score concept
flowchart LR
F[Feasible nodes] -->|LeastAllocated| LA[Score LA]
LA -->|BalancedAllocation| BA[Score BA]
BA -->|NodeAffinity preferred| NA[Score NA]
NA -->|TaintToleration| TT[Score TT]
TT -->|TopologySpread| TS[Score TS]
TS -->|Sum weighted| R[Total score]
R -->|Highest| N[Chosen node]
Each score plugin contributes to the total. The default plugin set is tuned for typical workloads; production clusters often add or remove plugins based on their optimisation goal.
The default score plugins
LeastAllocated
Score = (1 - (requested + this / capacity)) * weight
The score favors nodes with low utilisation. A node with 2 of 16 CPUs used scores higher than a node with 14 of 16.
| Plugin weight | Default 1 | | Effect | Spread Pods across the cluster |
MostAllocated (alternative)
Score = (requested + this / capacity) * weight
The opposite of LeastAllocated; favors nodes with high utilisation. Used for bin-packing.
BalancedAllocation
Score = 1 - |cpu_fraction - memory_fraction|
Favors nodes where CPU and memory utilisation are similar. A node with 50% CPU and 50% memory scores higher than a node with 80% CPU and 20% memory.
NodeAffinity (preferred)
Score = matchedExpressionsCount / totalExpressionsCount
A Pod with preferredDuringSchedulingIgnoredDuringExecution
NodeAffinity gets a score based on how many of its
preferences match the node.
TaintToleration
Score = len(tolerations_matching_taints) / len(tolerations)
A Pod tolerates a taint; the node gets a small score bonus, encouraging scheduling on nodes with the tolerated taints.
TopologySpread
Score = (max_skew - actual_skew) / max_skew
The plugin tries to spread Pods evenly across the topology domain. A node that brings the cluster closer to even spread scores higher.
ImageLocality
Score = image_already_on_node * weight
If the Pod’s image is already on the node (cached), the node scores higher. Reduces image pull latency.
Weights
The total score is a weighted sum of plugin scores:
apiVersion: kubescheduler.config.k8s.io/v1beta2
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
pluginConfig:
- name: LeastAllocated
weight: 1
- name: BalancedAllocation
weight: 1
Default weights (kubescheduler.config.k8s.io/v1beta2 in 1.34):
| Plugin | Default weight |
|---|---|
| LeastAllocated | 1 |
| BalancedAllocation | 1 |
| NodeAffinity | 2 |
| TaintToleration | 1 |
| ImageLocality | 1 |
| InterPodAffinity | 2 |
| NodeResourcesFit | 1 |
| NodePorts | 1 |
| VolumeBinding | 1 |
| TopologySpread | 2 |
A weight of 0 disables the plugin’s contribution.
Tied scores
When two nodes have the same total score, the scheduler uses a deterministic tie-breaker:
- Round-robin pick among tied nodes.
- Hash of (Pod UID, Node ID) for stability.
A Pod that produces tied scores may not always land on the same node across runs. Custom plugins can break ties deterministically.
Custom score plugins
Custom plugins extend the scoring with their own logic:
func (pl *NetworkLocality) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
score := pl.calculateNetworkDistance(pod, nodeName)
return score, nil
}
The custom plugin’s score is summed with the default plugins’ weighted scores. The weight is set in the plugin configuration.
The score impact on placement
Consider a 3-node cluster where the scheduling goal is “spread CPU usage evenly”:
| Node | CPU free | Score LA | Score BA | Total |
|---|---|---|---|---|
| cp-1 | 12 / 16 | 75 | 60 | 135 |
| cp-2 | 8 / 16 | 50 | 50 | 100 |
| cp-3 | 14 / 16 | 87 | 70 | 157 |
The Pod lands on cp-3 (highest total).
For memory-constrained Pods in a CPU-rich cluster:
| Node | CPU free | Memory free | Score |
|---|---|---|---|
| cp-1 | 14 / 16 | 2 / 16 | depends on weight |
The memory score dominates; cp-1 is filtered if memory is insufficient, then the highest-memory score node wins.
Plugin extensions and overrides
The scheduler supports profiles, which bundle a set of plugin configurations:
apiVersion: kubescheduler.config.k8s.io/v1beta2
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
plugins:
score:
enabled:
- name: LeastAllocated
weight: 2
- name: TopologySpread
weight: 3
disabled:
- name: ImageLocality
- schedulerName: low-latency-scheduler
plugins:
score:
enabled:
- name: LeastAllocated
weight: 1
- name: ImageLocality
weight: 5
A Pod can opt into a profile via
spec.schedulerName. The default-scheduler is the
default; custom profiles target specialised workloads.
Post-filter effects
After the chosen node is selected, the scheduler:
- Reserve the resources on the node (cache is updated).
- Permit (webhooks / framework plugins).
- Bind to the node.
The score’s output is just the chosen node; the binding is what publishes the decision.
Common placement anomalies
“All Pods on one node”
The cluster has multiple nodes but Pods land on one. The score plugins are disabling spread (e.g., all schedulable nodes have a taint except one).
Investigate by:
kubectl describe node <node>to see unschedulable.kubectl get pods -A -o wide | grep <node>to count.
“Spread ignored”
Topology spread is configured but Pods cluster. Check
the topologySpreadConstraints for maxSkew or
whenUnsatisfiable settings.
“Hot node”
One node is consistently at high utilisation. Check whether LeastAllocated is balanced or whether affinity is funneling Pods.
$ kubectl describe nodes | grep -A 5 'Allocated resources'...Quiz
Knowledge check · 4 questions
Q1. Which score plugin prefers the least-utilised node?
Q2. All default score plugins have weight 1.
Q3. Topology spread is configured but Pods cluster on one node. Diagnose.
Pod spec has `topologySpreadConstraints: maxSkew=1, whenUnsatisfiable=DoNotSchedule`. The Pods land on one node. The cluster has 3 nodes; two are in zone-a, one is in zone-b.
Q4. Why is the ImageLocality score plugin useful, and why might a team disable it?
Passing score: 75%. Answers are checked in this browser.
Production discipline
- Default weights are sensible. Tuning is rarely needed unless the cluster has a specific optimisation goal.
- Monitor score distribution. A
scheduler_attempts_total{result="scheduled"}flat-line with high CPU is a custom-plugin overhead concern. - Spread by topology when HA matters. TopologySpread with weight 2 is the default for a reason.
- Disable plugins with care. Disabling LeastAllocated causes packing; disabling NodeAffinity breaks affinity constraints.
- Custom plugins require profiling. A poorly-written plugin is the most common cause of scheduler regressions.
Scoring is the cycle phase that ranks feasible nodes. Operating it well is operating the cluster’s placement.