Skip to main content

Spread pods with topology constraints

Inspect 1.36

Topology spread constraints keep a workload's replicas apart, across nodes and across zones, so a single node replacement takes at most one of them. On a platform where node replacement is routine, this is the difference between a drain that costs one replica and a drain that costs all of them at once.

maxSkew, and the default you are overriding#

maxSkew is how uneven the spread may get: the busiest topology domain may hold at most maxSkew more matching pods than the emptiest. A pod that carries no spread rule of its own still gets one, because the cluster applies a default. But that default is deliberately loose:

Axis Label Default maxSkew On unsatisfiable
Nodes kubernetes.io/hostname 3 ScheduleAnyway
Zones topology.kubernetes.io/zone 5 ScheduleAnyway

Read the top row honestly: maxSkew: 3 across nodes means three replicas can pile onto one node, and losing that node takes all three. For anything that has to survive a single node replacement, write your own tighter rule:

yaml
		spec:
  topologySpreadConstraints:
    - maxSkew: 1
      topologyKey: kubernetes.io/hostname
      whenUnsatisfiable: DoNotSchedule
      labelSelector:
        matchLabels:
          app: web
	

maxSkew: 1 keeps any two nodes within one pod of each other, so replicas land on separate nodes. One caveat that trips people up: your own constraint replaces the loose default for that pod. It does not stack on top of it.

DoNotSchedule or ScheduleAnyway#

whenUnsatisfiable is where you decide what matters more: the placement, or the pod running at all.

  • DoNotSchedule holds a pod Pending until it can spread. This is strict high availability: an imbalanced placement is not allowed to happen.
  • ScheduleAnyway places the pod even when it cannot spread evenly. Use it when a running-but-lopsided pod beats no pod.

Zones on bare metal#

The CCM gives every node a zone label, bare metal included. The values differ in detail: cloud nodes get a datacenter like fsn1-dc14, Robot nodes get a location like fsn1. So a pool of Robot servers in one location is a single zone.

A single zone does not block scheduling. The skew is the gap between the fullest and the emptiest zone, and with only one zone that gap is always 0. A DoNotSchedule zone constraint is therefore satisfied straight away, and every replica lands.

What does block scheduling is a topologyKey that no node carries. Under DoNotSchedule those nodes are skipped entirely and every replica stays Pending. On cloud, minDomains goes the other way, requiring a minimum number of zones to actually get used before the constraint counts as satisfied.

Verify it survived a drain#

Scheduling three replicas onto three nodes is the easy half. Whether the spread holds when a node goes away, the exact event the page opened on, is the half that matters. Start with three replicas across worker-1, worker-2, and worker-3, then drain the middle one:

		$ kubectl drain worker-2 --ignore-daemonsets --delete-emptydir-data
	

Watch where the evicted replica lands. Under maxSkew: 1 it must reschedule onto a node that does not already hold a matching pod, never doubling up on worker-1 or worker-3. If instead it sits Pending, the spread is working exactly as told: there is no node left that satisfies the rule, and your choices are to add a node or relax that one constraint to ScheduleAnyway.

Spread decides where pods land; it does not cap how many are evicted at once. Pair it with a to keep enough replicas running through the drain itself.