Skip to content

Internals

Read from the source at commit 8c64324b. Every claim here points at a file and line.

Code map

PathResponsibility
cmd/Thin entry binaries that delegate to Cobra commands (apiserver, scheduler, controller-manager, kubelet, kubectl)
pkg/schedulerThe scheduler: queue, cache, framework, and the per-Pod scheduling cycle
pkg/kubeletThe node agent that runs assigned Pods through the CRI
pkg/controllerBuilt-in controllers reconciling higher level objects
pkg/controlplane, pkg/kubeapiserverAPI server assembly and storage wiring
staging/src/k8s.io/*Libraries published as standalone modules (k8s.io/api, k8s.io/client-go, k8s.io/apimachinery)

Core data structures

The Scheduler struct holds the scheduling state: the node and Pod cache, the scheduling queue, the per-Pod profiles, and a swappable SchedulePod function (scheduler.go:68). Making SchedulePod a field on the struct (scheduler.go:87) is what lets tests and alternate algorithms replace the core decision.

QueuedPodInfo wraps a PodInfo with queueing parameters and a Pod signature; it is the unit that sits in the scheduling queue (types.go:719). Its Gated() method reports whether a gating plugin is still holding the Pod back (types.go:747).

CycleState is scratch space scoped to a single scheduling cycle. It is backed by a sync.Map and tuned for the write-once-read-many pattern plugins use: write in PreFilter or PreScore, read in Filter or Score (cycle_state.go:28).

A path worth tracing

Scheduling one Pod, all in pkg/scheduler/schedule_one.go. schedulePod is the decision core: it filters nodes to the feasible set, fails with a FitError if none fit, takes the single survivor directly if there is one, and otherwise scores and pops the best (schedule_one.go:564). Filtering itself runs the PreFilter plugins first inside findNodesThatFitPod (schedule_one.go:622, PreFilter call at :632).

text
ScheduleOne (:67)
  -> scheduleOnePod (:93)            resolve profile, new CycleState
    -> schedulingCycle (:169)
      -> UpdateSnapshot (:177)
      -> schedulingAlgorithm -> schedulePod (:564)
           findNodesThatFitPod -> RunPreFilterPlugins (:632) -> Filter
           prioritizeNodes -> pop highest score
      -> prepareForBindingCycle (:196)  assume + Reserve + Permit
  -> go runBindingCycle (:141)          async bind
       -> bind (:1142) -> RunBindPlugins -> DefaultBinder.Bind

The actual cluster mutation happens in the default binder, which constructs a v1.Binding targeting the chosen node and POSTs it to the API server (default_binder.go:52).

Things that surprised me

The scheduler does not evaluate every node in a large cluster. numFeasibleNodesToFind adaptively shrinks the candidate set (schedule_one.go:858). When percentageOfNodesToScore is unset it computes percentage = 50 - numAllNodes/125, clamped to a floor (schedule_one.go:871), and never drops below minFeasibleNodesToFind = 100 (schedule_one.go:57). The bigger the cluster, the smaller the fraction it looks at. Optimal placement is traded for scheduling latency.

The second surprise is the optimistic assume followed by an asynchronous bind (schedule_one.go:141). The Pod is marked placed in the cache before the API server confirms the binding, so the next Pod can be scheduled without waiting on the network round-trip.