Part 5 – The Kubernetes Scheduler

How Kubernetes Chooses the Perfect Node for Your Spring Boot Application

In the previous article, we followed Sarah’s deployment from the moment she executed kubectl apply until the Deployment Controller created a ReplicaSet, and the ReplicaSet Controller created three Pod objects.

At that point, the cluster looked something like this.

Deployment
      │
      ▼
ReplicaSet
      │
      ▼
Pod A (Pending)

Pod B (Pending)

Pod C (Pending)

Notice something important.

Although the Pods exist inside Kubernetes, they are not running anywhere.

There is no JVM.

No Docker container.

No Spring Boot application.

No REST endpoints.

The Pods are simply waiting.

Waiting for what?

Waiting for one of the most intelligent components inside Kubernetes to make a decision.

That component is the Scheduler.

Today we’ll step inside the Scheduler and understand how Kubernetes decides exactly where every Spring Boot application should run.


A Common Misconception

Ask ten Java developers what the Scheduler does, and most will answer:

“It selects a worker node.”

Technically, they’re right.

But that’s like saying:

“Google Maps chooses a road.”

It certainly does—but only after analyzing traffic, road closures, speed limits, toll roads, accidents, construction work, estimated arrival time, and dozens of other variables.

Similarly, the Kubernetes Scheduler doesn’t randomly assign Pods to nodes.

Every scheduling decision is based on a surprisingly sophisticated evaluation process.

The Scheduler continuously asks:

“Which node can run this workload most efficiently while keeping the cluster balanced and resilient?”

This is far more than simple placement.

It’s intelligent orchestration.


Our Banking Cluster

Let’s revisit our banking platform.

Assume our production OpenShift cluster consists of six worker nodes spread across three availability zones.

                      Production Cluster

        ┌────────────────────────────────────────────┐

            Zone A          Zone B          Zone C

         ┌──────────┐     ┌──────────┐     ┌──────────┐
         │ Worker 1 │     │ Worker 3 │     │ Worker 5 │
         └──────────┘     └──────────┘     └──────────┘

         ┌──────────┐     ┌──────────┐     ┌──────────┐
         │ Worker 2 │     │ Worker 4 │     │ Worker 6 │
         └──────────┘     └──────────┘     └──────────┘

Sarah’s Payment Service requires three replicas.

spec:
  replicas: 3

The Scheduler now has one responsibility.

Find the best location for each Pod.

Not just a node.

The best node.


Why Can’t Kubernetes Pick Any Node?

Imagine you’re the operations manager of a hotel.

Three new guests arrive.

Would you randomly assign all three guests to Room 101?

Of course not.

You’d consider:

  • Which rooms are available?
  • Which rooms are already occupied?
  • VIP preferences
  • Room capacity
  • Maintenance status
  • Cleaning schedule

Only then would you assign rooms.

Scheduling Pods follows the same principle.

Random placement would lead to:

  • CPU bottlenecks
  • Memory exhaustion
  • Uneven workload distribution
  • Poor fault tolerance
  • Increased downtime during node failures

Kubernetes avoids these problems through intelligent scheduling.


The Scheduler Watches for Pending Pods

Just like the Deployment Controller watches Deployments, the Scheduler continuously watches the API Server for Pods whose status is:

Pending

The moment Sarah’s ReplicaSet creates three Pending Pods, the Scheduler receives an event.

It now begins evaluating each Pod individually.

Notice something subtle.

The Scheduler doesn’t schedule Deployments.

It doesn’t schedule ReplicaSets.

It schedules Pods.


The Scheduling Pipeline

A scheduling decision isn’t a single step.

It’s a pipeline.

Conceptually, it looks like this.

             Pending Pod

                  │

                  ▼

        Retrieve Available Nodes

                  │

                  ▼

          Filter Unsuitable Nodes

                  │

                  ▼

          Score Remaining Nodes

                  │

                  ▼

       Select Highest Scoring Node

                  │

                  ▼

        Bind Pod to Worker Node

Every Pod goes through this process.

For small clusters, this takes milliseconds.

Even in clusters containing thousands of nodes, Kubernetes performs this evaluation remarkably quickly.


Step 1 – Collect Candidate Nodes

The Scheduler first retrieves every worker node capable of running workloads.

Imagine the cluster currently looks like this.

NodeCPU FreeMemory Free
Worker-16 vCPU14 GB
Worker-21 vCPU2 GB
Worker-38 vCPU20 GB
Worker-43 vCPU10 GB
Worker-57 vCPU18 GB
Worker-65 vCPU15 GB

Initially, every node is considered a candidate.

The Scheduler hasn’t rejected anything yet.


Step 2 – Filtering

Now the real evaluation begins.

The Scheduler asks:

“Can this node run the Pod?”

This phase is called Filtering.

Think of it as an elimination round.

Nodes that fail mandatory requirements are removed immediately.

Typical filters include:

  • Insufficient CPU
  • Insufficient memory
  • Node unavailable
  • Node cordoned
  • Taints
  • Node affinity mismatch
  • Unsupported operating system
  • Unsupported architecture
  • Volume constraints
  • Network constraints

After filtering, only eligible nodes remain.


Example – CPU Requirements

Suppose Sarah’s Payment Service specifies:

resources:

  requests:

    cpu: "2"

    memory: "4Gi"

This means Kubernetes must guarantee:

  • 2 CPU cores
  • 4 GB memory

Now consider Worker-2.

Worker-2

CPU Available

1 Core

The Scheduler immediately removes it.

Rejected

Reason

Insufficient CPU

Notice that Kubernetes isn’t checking current CPU utilization.

It checks allocatable requested resources.

This distinction is one of the most misunderstood concepts among developers.


Requests vs Actual Usage

Suppose a node contains:

8 CPU Cores

Applications have reserved:

7 CPU

Current CPU utilization is only:

15%

Can Kubernetes schedule another Pod requesting 2 CPUs?

No.

Why?

Because Kubernetes schedules based on requests, not current utilization.

The Scheduler assumes every application may eventually consume the resources it requested.

This conservative approach prevents future resource starvation.


Memory Filtering

Memory follows exactly the same principle.

Suppose the Payment Service requests:

memory: 8Gi

Worker-4 has:

Available Memory

6 GiB

The Scheduler rejects it immediately.

Unlike CPU, memory cannot be overcommitted safely for most workloads.

Choosing an undersized node would almost certainly result in an OutOfMemoryKilled (OOMKilled) container later.

Kubernetes prevents that risk during scheduling itself.


Taints and Tolerations

Not every worker node is intended for every workload.

Suppose the bank has dedicated nodes for Oracle databases.

Those nodes carry a taint.

database=true:NoSchedule

This tells Kubernetes:

“Do not place normal application Pods here.”

If Sarah’s Payment Service doesn’t declare a matching toleration, the Scheduler removes those nodes from consideration.

This protects critical infrastructure from accidental workload placement.

We’ll explore taints and tolerations in depth later in this series, but it’s important to understand that scheduling decisions aren’t based solely on hardware—they’re also driven by operational policies.


Node Affinity

Sometimes applications prefer specific nodes.

For example:

  • GPU-enabled nodes
  • SSD-backed nodes
  • High-memory nodes
  • Banking-compliant zones
  • Country-specific infrastructure

Sarah may specify:

nodeSelector:

  workload: banking

or use Node Affinity rules.

The Scheduler will only consider nodes matching those labels.

Again, incompatible nodes are eliminated before scoring begins.


After Filtering

Suppose filtering produces the following result.

Worker-1

Eligible

------------------

Worker-3

Eligible

------------------

Worker-5

Eligible

------------------

Worker-6

Eligible

Workers 2 and 4 were rejected.

Only four candidates remain.

Now comes the interesting part.

Which of these four is best?

That is the purpose of the scoring phase.


Step 3 – Scoring

Filtering answers:

Can this node run the Pod?

Scoring answers:

Which eligible node should run the Pod?

Multiple scoring plugins evaluate each remaining node.

Typical considerations include:

  • Resource balance
  • Least allocated resources
  • Image locality
  • Topology spread
  • Inter-pod affinity
  • Preferred node affinity
  • Even distribution across zones

Every node receives a score.

Example:

Worker-1

Score = 84

----------------

Worker-3

Score = 92

----------------

Worker-5

Score = 88

----------------

Worker-6

Score = 79

The Scheduler selects the highest-scoring node.

In this example:

Worker-3

wins.

Notice something important.

The Scheduler didn’t choose the first available node.

It chose the best available node.


Binding the Pod

After selecting Worker-3, the Scheduler performs one final operation.

It updates the Pod specification.

Conceptually:

Before

Node = None

becomes

After

Node = Worker-3

This operation is known as binding.

At this point, the Scheduler’s responsibility is complete.

Its work is finished.

The Pod now has a destination.

But something is still missing.

Even though the Pod has been assigned to Worker-3:

  • No Docker image exists on the node.
  • No container is running.
  • No JVM has started.
  • Spring Boot hasn’t initialized.
  • No HTTP server is listening on port 8080.

Those responsibilities belong to another critical Kubernetes component running on every worker node.

The kubelet has just noticed that a new Pod has been assigned to its node, and it’s about to transform a simple Pod definition into a running Spring Boot application.

In the next article, we’ll move from the Kubernetes control plane to the worker node itself and follow the kubelet as it pulls the Docker image, creates the container, starts the JVM, launches Spring Boot, and prepares the application for production traffic.

Leave a Reply

Your email address will not be published. Required fields are marked *