Skip to main content
RunBook Academy

KubernetesXXII · Scheduling FundamentalsScheduling fundamentals

Score phase — how the scheduler ranks feasible nodes

Advanced⏱ ~17 minkubectlkubeadm

What you'll learn

  • Describe the score phase: ranking feasible nodes
  • Identify the standard scoring plugins and their preferences
  • Explain how scoring weights combine into a final score
  • Configure custom scoring via scheduler profiles

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.

The scheduler’s score phase ranks the nodes that survive the filter phase. Each node gets a score from each scoring plugin; the weighted sum determines the winner. This lesson covers the standard scoring plugins, how they combine, and how to customise them via scheduler profiles.

The score pipeline

flowchart LR
    A[Feasible nodes] --> B["Score plugin 1<br/>LeastAllocated"]
    B --> C["Score plugin 2<br/>BalancedResourceAllocation"]
    C --> D["Score plugin 3<br/>NodeAffinity preferred"]
    D --> E["Score plugin 4<br/>InterPodAffinity"]
    E --> F["Score plugin N<br/>..."]
    F --> G[Weighted sum]
    G --> H["Highest score<br/>selected"]

Each plugin scores a node from 0 to 100 (or 0 to 10 in older versions). The scheduler configures a weight per plugin; the final score is the weighted sum, normalised to 0-100.

Standard scoring plugins

LeastAllocated (default)

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default
  plugins:
    score:
      enabled:
      - name: LeastAllocated
        weight: 1

LeastAllocated favours nodes with the most free resources:

flowchart TB
    A["Node A: 50% used"] --> B["Score: 50<br/>(50% free)"]
    C["Node B: 90% used"] --> D["Score: 10<br/>(10% free)"]

The Pod lands on Node A (more headroom). The score is proportional to the free fraction; a fully-allocated node gets 0.

LeastAllocated is the default scoring rule. It produces even distribution but may not be the right policy for bin-packing (where the goal is high utilisation).

BalancedResourceAllocation

plugins:
  score:
    enabled:
    - name: BalancedResourceAllocation
      weight: 1

BalancedResourceAllocation favours nodes where CPU and memory utilisation are similar:

flowchart TB
    A["Node A: CPU 80%<br/>memory 20%"] --> B["Score: low<br/>(imbalanced)"]
    C["Node B: CPU 50%<br/>memory 50%"] --> D["Score: high<br/>(balanced)"]

A node with high CPU and low memory utilisation is imbalanced; a node with even utilisation is balanced. This produces predictable resource usage patterns.

NodeAffinity (preferred)

affinity:
  nodeAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 50
      preference:
        matchExpressions:
        - key: disk
          operator: In
          values: ["ssd"]

The weight is added to the node’s score if the node matches the preference. A weight of 50 means “50 out of 100 extra points.” Multiple preferences are summed.

InterPodAffinity

affinity:
  podAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 100
      podAffinityTerm:
        topologyKey: kubernetes.io/hostname
        labelSelector:
          matchLabels:
            app: cache

Co-locate with Pods labelled app: cache on the same node. The score increases for nodes that already host such Pods.

TaintToleration

plugins:
  score:
    enabled:
    - name: TaintToleration
      weight: 1

The TaintToleration plugin favours nodes the Pod tolerates without preferring. It is rarely customised; the filter phase handles taints more strictly.

Combining scores

flowchart TB
    A[Node X] --> B["LeastAllocated: 80"]
    A --> C["Balanced: 70"]
    A --> D["NodeAffinity: 50"]
    A --> E["InterPodAffinity: 0"]
    A --> F[Weighted sum]
    F --> G["Score: (80 + 70 + 50 + 0) / 4 = 50"]

The scheduler configures a weight for each plugin; the final score is the weighted sum. The default kube-scheduler configuration uses equal weights for the built-in plugins.

Tiebreakers

When two nodes have the same score, the scheduler uses tiebreakers:

flowchart TB
    A["Two nodes<br/>score 80"] --> B{Tiebreaker 1:<br/>BinPacking?}
    B -->|yes| C["Node with more<br/>existing Pods"]
    B -->|no| D{Tiebreaker 2:<br/>NodeName hash?}
    D -->|yes| E["Hash of node name<br/>deterministic"]
    D -->|no| F[Random selection]

The default behaviour: random selection. Determinism is useful for testing but not for production.

Custom scoring

A scheduler profile can enable, disable, or replace scoring plugins:

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: gpu-scheduler
  plugins:
    score:
      enabled:
      - name: LeastAllocated
        weight: 1
      - name: NodeAffinity
        weight: 2
      - name: InterPodAffinity
        weight: 1
      disabled:
      - name: BalancedResourceAllocation

The gpu-scheduler profile weights NodeAffinity twice as much as LeastAllocated, prefers nodes with the GPU label, and disables BalancedResourceAllocation (a GPU workload does not benefit from balanced scoring).

Real-world patterns

Pattern 1: prefer SSD nodes

affinity:
  nodeAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 100
      preference:
        matchExpressions:
        - key: disk
          operator: In
          values: ["ssd"]

The Pod lands on an SSD node if available; falls back to spinning disk if not.

Pattern 2: spread across nodes

affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 100
      podAffinityTerm:
        topologyKey: kubernetes.io/hostname
        labelSelector:
          matchLabels:
            app: web

The Pod prefers a node with no other app: web Pods. Multiple replicas of the Deployment spread across nodes.

Pattern 3: prefer same node as cache

affinity:
  podAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 100
      podAffinityTerm:
        topologyKey: kubernetes.io/hostname
        labelSelector:
          matchLabels:
            app: cache

The Pod prefers to land on a node already hosting a cache. This co-locates the workload with its dependency.

Inspecting scores

The scheduler does not expose the score per node by default. To debug:

# Enable verbose logging on the scheduler
kubectl logs -n kube-system kube-scheduler-master-0 --v=4
# Look for "ScoreNoderesults" entries

Production operators correlate the scheduling decision with the cluster’s state at the time (kubectl describe nodes, kubectl top nodes).

Quiz

Knowledge check · 4 questions

  1. Q1. Which scoring plugin favours nodes with the most free resources?

  2. Q2. The scheduler's score phase randomly picks among feasible nodes; the scoring plugins have no real effect.

  3. Q3. Your team deploys a Pod with preferredDuringSchedulingIgnoredDuringExecution for SSD nodes. The Pod lands on a non-SSD node. Diagnose.

    Pod web-cache with Node Affinity preferred weight 100 for disk ssd. Cluster has 4 SSD nodes and 4 HDD nodes. The Pod lands on an HDD node.

  4. Q4. Explain how scoring plugins combine into a final score, and how tiebreakers work.

Passing score: 75%. Answers are checked in this browser.

Production discipline

  • Default scoring is a starting point. Production workloads may need different weights (e.g., a latency-sensitive workload that prefers nodes with low CPU).
  • Use scheduler profiles for workload-specific scoring. Different profiles for different workloads (GPU, latency-sensitive, batch) allow tailored scoring.
  • Verify scoring with logs. The scheduler’s debug logging surfaces the per-node scores; use it to validate the configuration.
  • Audit the scheduler’s behaviour. A Pod that lands on an unexpected node may be the result of a scoring plugin the operator did not intend.

The score phase is the difference between a Pod landing on the right node and an arbitrary one. Operators who understand scoring have Pods that land predictably.