kube-scheduler watches for Pods with no spec.nodeName and picks a node in two phases. Filtering drops nodes that cannot run the Pod (too little unreserved CPU or memory, an untolerated taint, a failed affinity rule). Scoring ranks the rest with weighted plugins, and the choice is written back as a binding. The scheduler counts requests, not real usage, so a Pod asking for more than any node has left stays Pending:
kubectl create deployment hungry --image=localhost:33500/booknest-web:1.3 --replicas=2 \
--dry-run=client -o yaml | kubectl set resources -f - --local --requests=cpu=3 -o yaml \
| kubectl apply -f -
sleep 10
kubectl get pods -l app=hungry \
-o custom-columns=NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName
kubectl get events --field-selector reason=FailedScheduling -o jsonpath='{.items[-1:].message}' \
| fold -s -w 90deployment.apps/hungry created NAME STATUS NODE hungry-66f6c7c98-2m6pm Pending <none> hungry-66f6c7c98-ccl6l Running l3-booknest-worker 0/2 nodes are available: 1 Insufficient cpu, 1 node(s) had untolerated taint(s). preemption: 0/2 nodes are available: 1 No preemption victims found for incoming pod, 1 Preemption is not helpful for scheduling.
That is the filtering verdict node by node: the worker lacks CPU, and the control-plane node carries a NoSchedule taint. By default the NodeResourcesFit plugin scores with LeastAllocated, spreading Pods onto the emptiest nodes; configure MostAllocated to bin-pack instead, so the cluster autoscaler (Cluster Autoscaler) can remove idle nodes.