Skip to main content
RunBook Academy

KubernetesLXXI · SchedulerScheduler

Scoring — least allocated, balanced, topology, custom

Advanced⏱ ~16 minkubectl

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

Not yet marked complete on this device.

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):

PluginDefault weight
LeastAllocated1
BalancedAllocation1
NodeAffinity2
TaintToleration1
ImageLocality1
InterPodAffinity2
NodeResourcesFit1
NodePorts1
VolumeBinding1
TopologySpread2

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”:

NodeCPU freeScore LAScore BATotal
cp-112 / 167560135
cp-28 / 165050100
cp-314 / 168770157

The Pod lands on cp-3 (highest total).

For memory-constrained Pods in a CPU-rich cluster:

NodeCPU freeMemory freeScore
cp-114 / 162 / 16depends 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:

  1. Reserve the resources on the node (cache is updated).
  2. Permit (webhooks / framework plugins).
  3. 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.

Read-only / Safe
$ kubectl describe nodes | grep -A 5 'Allocated resources'
...

Quiz

Knowledge check · 4 questions

  1. Q1. Which score plugin prefers the least-utilised node?

  2. Q2. All default score plugins have weight 1.

  3. 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.

  4. 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.