A Tour of Kubernetes Internals: The Thirty-Six Tricks That Make It Scale

Thirty-six source-grounded tours of Kubernetes internals: the scheduler's copy-on-write snapshot, the watch cache's memoized serialization, the informer's unbounded ring buffer, and the small tricks that make a distributed system look effortless.

TL;DR: Kubernetes is a distributed system that pretends to be a declarative API. The pretence is the whole design: a YAML file is a diff against a desired state, and thirty different reconcilers are each responsible for making one part of the world match it. What makes that work at scale is a pile of small, unglamorous decisions - a copy-on-write snapshot so the scheduler never locks, a memoized serializer so one etcd event can feed a thousand watchers, a workqueue built from two sets and a FIFO. This is a tour of thirty-six of them, and where in the source tree each one lives.

kubectl apply -f deployment.yaml is not a request to create a Deployment. It is a request to make the world look like a Deployment, and the interesting question is always how much machinery stands between that sentence and the running pod. The answer is a lot: a hash, a watch cache, an informer, a workqueue, a scheduler snapshot, a cgroup, a conntrack entry.

This is a tour of thirty-six of the decisions inside that path - the ones that are worth knowing about, grouped by the part of the system they live in. Every article quotes the source it describes.

1. The scheduler: sampling, snapshots, and stealing

The scheduler runs one cycle at a time, on a cache that other goroutines are writing to, and has to answer “where should this pod go” in milliseconds without locking anything and without looking at every node.

2. The API server: one event, a thousand watchers

The API server’s job is to turn etcd writes into streams of events, and its hot path is fan-out. Almost every trick here exists because one etcd event can reach thousands of watchers.

3. client-go: the machinery everybody copies

Every controller in the wild is a variation on the same four components: a reflector, a DeltaFIFO, an informer, and a workqueue. They are also the components with the sharpest edges.

4. The kubelet: the part that actually touches the hardware

Everything above runs in a control plane that someone else can reboot. The kubelet is the part that has to be right about cgroups, memory pressure and the OOM killer, because nobody is watching.

5. The controllers: acting on a cache that lags

A controller reads from an informer cache, decides, and writes to the API server. Between those two things is a window in which the world can change underneath it, and the code is full of ways of not caring.

6. Networking: a service is a rule

The service proxy is the oldest part of Kubernetes and the one that has to survive the most constraints: 28-character chain names, a kernel that will happily accept thousands of rules, and a datapath that has to stay connection-safe.

How to read the Kubernetes source

The tree is big, but it is organised by the same boundaries this tour follows:

DirectoryWhat lives there
pkg/scheduler/the scheduling cycle, the framework plugins, the cache
pkg/kubelet/everything that touches the node: cgroups, eviction, the runtime
pkg/controller/the reconcilers, each in its own directory
pkg/proxy/the service proxy, one directory per datapath
pkg/registry/the API server’s storage and validation layer
staging/src/k8s.io/apiserver/the generic API server: watch cache, admission, storage
staging/src/k8s.io/client-go/reflectors, informers, workqueues
staging/src/k8s.io/apimachinery/the type system, serialization, patches

Two habits make the code readable. First, almost every interesting decision is a named constant with a comment explaining the number - minFeasibleNodesToFind = 100, overloadThreshold = 0.2, maxLimit = 10000, blockTimeout = 3 * time.Second. Second, the comments explain why, usually by naming the bug or the incident that caused the code to exist (“this adds ~1s of latency on all transitions from sync to terminating”, “we don’t want to do this on every sync in large clusters”).

Trade-offs

Almost every trick here buys its speed with something. The watch cache trades freshness for throughput: a LIST from the cache can be a few milliseconds behind etcd, and it can be arbitrarily behind if the cache is unhealthy, which is why it eventually returns an error instead of lying. The scheduler trades exactness for latency: it samples nodes and rotates the starting point, so a pod might not land on the theoretically best node, but no node is starved. The informer trades memory for correctness: an unbounded queue means a slow handler eventually takes the process down rather than silently dropping events. The large-cluster mode of kube-proxy trades debuggability for rule count, which is why iptables-save stops being useful past a thousand endpoints.

The pattern is consistent: Kubernetes optimises for the steady state at scale, and pays for it in the corners.

The bottom line

The one thing to remember: the interesting parts of Kubernetes are not the API. They are the places where the code refuses to do the obvious thing - clone two nodes instead of five thousand, serialize once instead of a thousand times, use one heap instead of a thousand timers, and re-read a lease from the server before it kills your node.

The part people get wrong: most of these behaviours are silent. The scheduler does not tell you it sampled 5% of the cluster. The watch cache does not tell you it served you a slightly stale list. kube-proxy does not tell you it stopped writing comments. The defaults are tuned for the cluster size that was current when they were written, and the knobs that would tell you otherwise are often not exposed.

What to go do about it: read the constants. grep -rn "= 100" pkg/scheduler/ and grep -rn "threshold" pkg/kubelet/eviction/ take ten minutes and explain more about how Kubernetes behaves than most of the documentation does. Then watch the metrics that those constants drive - scheduler_pending_pods, apiserver_watch_cache_read_wait_seconds, kubelet_evictions, kube_proxy_iptables_rules_total - and remember that each of them is a window into one of the tricks above.

The thing people get wrong

Kubernetes is often described as a declarative shell over an imperative system, and the phrase makes people look for reconciliation loops in the wrong place. The reconciliation loops are the easy part; they are a Get, a comparison and a Write. The hard part is everything the source does to make those three operations safe at ten thousand nodes per second: the snapshots, the expectations, the caches, the generation stamps, the counters, the frozen hashes, the discarded buffers.

None of it is exotic. It is all a set of small, well-commented decisions about what to do when the world changes underneath you - which, in a distributed system, it always does.

KubernetesInternalsOverviewArchitecturePerformanceSchedulerApiserverKubeletNetworkingClient go