Taints and Tolerations#
On a single-node Minikube cluster scheduling is trivial - there’s nowhere else to put a pod. The moment you add nodes (multi-node notes are coming), scheduling decisions become real. Taints mark a node as “keep away”, and tolerations let specific pods opt in anyway:
kubectl taint nodes <node-name> workload=ai:NoScheduleThe three effects behave differently:
NoSchedule: new pods without a matching toleration can’t be scheduled here (running pods are left alone)PreferNoSchedule: soft version - scheduler tries to avoid, but won’t rejectNoExecute: also evicts pods already running there that lack the toleration - the aggressive one
The matching toleration in a pod spec:
tolerations:
- key: "workload"
operator: "Equal"
value: "ai"
effect: "NoSchedule"operator: "Exists" (no value) matches any value for that key. And some taints are well-known - k8s itself taints nodes when things go wrong (node.kubernetes.io/not-ready, .../unreachable, memory-pressure); these auto-expire when the condition clears, which is why pods can’t schedule onto an unhealthy node even though you never taint it.
Özetlersek#
Taint, node’un kapısına “her pod giremez” tabelası asar.
Toleration ise pod’un elindeki “bu yasağı geçebilirim” iznidir.
Örneğin bir GPU node’una taint koyarsan, normal pod’lar oraya schedule edilmez.
Sadece gerekli toleration’a sahip AI / ML workload’ları bu node’u kullanabilir.
Yaygın kullanım alanları:
- GPU node’ları → normal workload’lar gelmesin, sadece GPU kullanan job’lar çalışsın.
- Control-plane node’ları → uygulama pod’ları yanlışlıkla control-plane üzerinde çalışmasın.
kubeadmbunu varsayılan olarak taint ile yapar. - Spot / preemptible node’lar → mümkünse normal node’ları kullan, ama gerekirse spot node’a da geç.
Özellikle PreferNoSchedule şu anlama gelir:
"Mümkünse buraya pod koyma,
ama başka uygun yer yoksa koyabilirsin."Kısaca:
Taint = node'un koyduğu kısıtlama
Toleration = pod'un o kısıtlamayı tolere edebilmesiAma önemli nokta:
toleration != "beni bu node'a gönder"Toleration sadece kapıdaki engeli kaldırır. Pod’un gerçekten o node’a gitmesini istiyorsan ayrıca nodeSelector, nodeAffinity gibi scheduling kuralları kullanırsın.
Affinity#
Taints push pods away from nodes. Affinity pulls them toward - or away from - each other. Three flavors:
nodeSelector - the simple label match:
spec:
nodeSelector:
disktype: ssdnodeAffinity - nodeSelector with boolean expressions and soft variants:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disktype
operator: In
values: ["ssd", "nvme"]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: zone
operator: In
values: ["eu-west"]required...= hard rule; no node matches → pod staysPending(this is a troubleshooting classic)preferred...= soft preference with a weight; used when “best effort” is acceptableIgnoredDuringExecution= if a running pod’s node stops matching, the pod keeps running (only new pods are constrained) - there’s arequiredDuringSchedulingRequiredDuringExecutionbeta feature for “please move me” semantics
podAntiAffinity - spread or co-locate pods relative to each other (the topologyKey decides what counts as “different domain” - hostname = per node, zone = per availability zone).
Topology Spread Constraints#
podAntiAffinity is binary per node. Topology spread expresses the modern, more nuanced goal: skew:
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: synergychat-webmaxSkew: 1 means: across all nodes matching the topologyKey domain, the difference between the node with the most and least matching pods may be at most 1. With 3 replicas over 3 nodes you get exactly one pod per node; with 4 replicas you get 2/1/1 - never 3/1/0. whenUnsatisfiable: DoNotSchedule makes it a hard constraint; ScheduleAnyway is the soft mode.
Why prefer this over podAntiAffinity? Anti-affinity refuses to co-locate any two replicas - at scale (50 replicas, 10 nodes) that’s unsatisfiable and pods pend forever. Spread constraints quantify fairness instead of forbidding co-location.
ResourceQuota and LimitRange#
Taints and affinity decide where pods go. Quotas decide how much a namespace may consume in total:
apiVersion: v1
kind: ResourceQuota
metadata:
name: backend-quota
namespace: backend
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "20"A LimitRange complements it with per-container defaults, so devs don’t have to remember resources on every pod:
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
spec:
limits:
- default:
cpu: "500m"
memory: 512Mi
defaultRequest:
cpu: "100m"
memory: 128Mi
type: ContainerThe quota-LimitRange interaction has a sharp edge: a pod with no resources declared can’t be admitted to a namespace with CPU/memory quota, because it can’t be accounted. With a LimitRange present, undeclared resources get defaults and admission succeeds - that’s why the pair ships together, not alone.
Also worth knowing: quotas can be scoped - scopeSelector with Terminating/NotTerminating or BestEffort/NotBestEffort lets you, say, cap only bursty jobs while leaving long-running services alone.
How to Reserve a Node for Special Workloads#
- Taint the node and verify the taint took:
kubectl taint nodes <node-name> team=backend:NoSchedule
kubectl describe node <node-name> | grep -iA3 taints- Delete a
synergychat-webpod and watch its replacement goPending:
kubectl delete pod <web-pod>
kubectl describe pod <new-pod>Find the event: node(s) had untolerated taint {team: backend} - the scheduler is telling you the only node is off-limits.
- Give the web deployment the toleration (pod template):
tolerations:
- key: "team"
operator: "Equal"
value: "backend"
effect: "NoSchedule"Apply and watch it schedule. Then remove the taint to restore the shared setup:
kubectl taint nodes <node-name> team=backend:NoSchedule-Note the trailing
-on the taint command - that’s kubectl’s “remove” suffix, same as labels. Forgetting it creates a duplicate taint error that reads like a mystery.
How to Spread Replicas Across Nodes#
- Add to the web deployment’s pod spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: synergychat-web- Scale to more replicas than you have nodes, and watch placement:
kubectl get pods -o wide --watch- Delete one pod and watch the replacement avoid clustering on an already-heavy node. With
whenUnsatisfiable: DoNotScheduleon a tight cluster, some pods may stayPendinguntil capacity allows skew 1 - that’s the constraint working, not a bug; switch toScheduleAnywayif you’d rather degrade gracefully.
How to Cap a Namespace with ResourceQuota#
- Create a sandbox namespace with a strict pod budget:
kubectl create ns sandbox
kubectl -n sandbox apply -f - <<EOF
apiVersion: v1
kind: ResourceQuota
metadata:
name: sandbox-quota
spec:
hard:
pods: "3"
requests.memory: 1Gi
EOF- Deploy a 5-replica nginx there:
kubectl -n sandbox create deployment nginx --image=nginx:1.27 --replicas=5
kubectl -n sandbox get deployment nginx3/5 ready, and the rest never come up.
- Find the verdict - quota exhaustion shows up in deployment conditions and events, not pod statuses (there are no pods to describe!):
kubectl -n sandbox describe deployment nginx | tail -n 5
kubectl -n sandbox get resourcequota sandbox-quota -o yamlThe quota’s status.used vs hard tells the story.
- Now add the LimitRange (defaults above) to the same namespace, delete the deployment, redeploy with no resource declarations at all - the pods inherit defaults and count against the quota automatically. That’s the mechanism that keeps “forgot to set requests” developers from bypassing your accounting.
for more pod-priority-disruption