58. Walkthrough: Design a Cluster Scheduler / Container Orchestrator (Kubernetes, Borg)
The prompt. "Design Kubernetes", "Design a cluster manager like Borg", or "Design a job scheduler for a fleet of machines." It tests desired-state control loops, scheduling algorithms, failure handling, and the scalability of the control plane. See the K8s guide for operator-level detail.
Step 0 — Clarify the business goal
🎤 Candidate: The business outcomes are utilization: machines are the biggest infrastructure cost, and better bin packing saves real money. Also developer velocity (self-serve deploys) and reliability (automatic recovery). Do we mix latency-sensitive services and batch jobs on the same machines? That's where most of the utilization gain comes from, and most of the complexity.
🧑💼 Interviewer: Yes, both, on ~10,000-machine clusters.
Step 1–2 — Requirements
| Functional | Non-functional |
|---|---|
| Declarative API: "run N replicas of image X with resources R" | Cluster: 10k nodes, ~300k containers |
| Scheduling with constraints: resources, affinity/anti-affinity, zones, priorities | Scheduling throughput: thousands of pods/s during mass deploys |
| Health checks, restarts, rescheduling on node failure | Control-plane outage must not stop running workloads |
| Rolling updates, service discovery, quotas per team | Failover of a failed node's work within minutes |
| Preemption: production evicts batch when needed | High utilization (e.g. > 60% average CPU) without hurting latency SLOs |
Step 5 — High-level architecture
Users / CI ─▶ API server (authN/Z, validation, admission policies) ─▶ state store (etcd: Raft)
▲ watch streams (list + watch; informers cache state in every component)
┌──────────────────┼────────────────────────┬────────────────────────────┐
│ │ │ │
Controllers Scheduler Node agents (kubelet) Autoscalers
(deployments, watch unscheduled pods → watch pods bound to node → (replicas, nodes)
replicasets, FILTER feasible nodes → start/stop containers via
jobs, endpoints) SCORE (bin-pack, spread, runtime; report status;
reconcile desired affinity) → BIND (write enforce cgroups limits;
vs actual state pod.nodeName, optimistic evict on resource pressure
concurrency)
The core idea: level-triggered reconciliation. Every controller loops "observe actual state → compare with desired → act to converge". It doesn't react to individual events ("pod died"). It reacts to the current difference, so missed events, restarts, and duplicate notifications are harmless. That's why the system self-heals.
Step 6 — Deep dives
- Scheduling algorithm: filter (resources, taints, affinity, ports, volumes zone) → score (least-allocated vs most-allocated bin packing, topology spread, image locality) → bind. To scale: sample a subset of feasible nodes on large clusters, use optimistic concurrency on binding (conflicts are retried), and batch. Google's Omega paper (2013) compares monolithic, two-level (Mesos), and shared-state optimistic schedulers; the shared-state idea influenced Kubernetes.
- Priorities, preemption, and quotas: production workloads can evict lower-priority batch work. Quotas per team stop one org from taking the whole cluster. Pod disruption budgets limit voluntary evictions.
- Utilization (the business case): Borg (EuroSys 2015) reported large savings from mixing production and batch on shared cells and reclaiming resources that production jobs reserved but didn't use. The catch is noisy neighbors: CPU throttling (§22.6), memory pressure, and I/O contention need isolation (cgroups, CPU pinning for latency-critical work) and eviction policies.
- Control-plane scalability: list/watch with informer caches so components don't poll the API server; watch bookmarks and resource versions; etcd sized carefully (it's a consensus store with size limits, §19). The upstream Kubernetes scalability thresholds are about 5,000 nodes and 150,000 pods per cluster, so larger fleets use many clusters (cells, §20.4) with multi-cluster tooling. Meta's Twine (OSDI 2020) takes a different approach: one control plane managing very large shared regions.
- Failure handling: node heartbeats stop → mark the node unhealthy after a grace period → evict and reschedule pods. Stateful workloads need fencing (§19.3): a partitioned node may still be running the old pod, so volume attachment and leases must prevent two writers.
Step 7 — Failure modes & corner cases
| Issue | Response |
|---|---|
| Control plane down | Running pods keep running (kubelets don't need the API to keep containers alive); no new scheduling, a static-stability property |
| Node failure → mass rescheduling storm | Rate-limit evictions; spread replicas across zones in advance; pod disruption budgets |
| Image pull storms (1,000 pods pull a 2 GB image at once) | Image pre-pulling and caching, peer-to-peer distribution, smaller images |
| Zone outage | Topology spread constraints place replicas across zones before failure |
| Scheduler starvation (large pods never fit in a fragmented cluster) | Preemption, defragmentation/descheduler, reserved capacity for large jobs |
| etcd growth / slowness | Compaction, defragmentation, event TTLs, keeping large objects out of etcd |
| Controller hot loops (two controllers fighting over a field) | Clear field ownership (server-side apply), backoff on repeated reconciles |
🌍 Real-world references
- Borg, Omega, and Kubernetes: Burns et al., "Borg, Omega, and Kubernetes" (ACM Queue, 2016), lessons across three generations of Google cluster managers.
- Large-scale cluster management at Google with Borg (EuroSys 2015).
- Twine: A Unified Cluster Management System for Shared Infrastructure (Meta, OSDI 2020).
- Azure Service Fabric (EuroSys 2018): Microsoft's distributed platform for stateful microservices, powering many Azure services.