gw-09 — The Hitchhiker's Guide to Kubernetes Networking Internals
Companion to CONCEPTS.md, with runnable simulations in
src/go/k8snet/. The gateway fleet runs on Kubernetes (Netflix's Titus→K8s journey); these are the two networking behaviors that actually page you.
You can't spin up a real cluster in a unit test, and reading the
Kubernetes docs doesn't build the reflex you need on call. So this lab
simulates the two highest-impact behaviors deterministically: the
drain / EndpointSlice propagation race and conntrack exhaustion.
Run bash scripts/verify.sh:
drain / EndpointSlice race (3 kube-proxies; 10 req/proxy/tick):
WRONG (exit first): 100 requests dropped
RIGHT (fail readiness first): 0 requests dropped
RIGHT but grace too short: 400 requests dropped (SIGKILL mid-propagation)
conntrack exhaustion (table size 100, 1000 requests):
churn (new connection each): 900 packets dropped
keep-alive (reuse 1 flow): 0 packets dropped
Those two blocks are the most common self-inflicted gateway outages on Kubernetes. Internalize them and you can debug "every deploy drops a few requests" and "the node randomly drops connections under load" — two incidents that otherwise eat a day each.
1. The drain race (drain.go)
A Service is fronted by many kube-proxies, each with its own local view of the endpoints, updated from EndpointSlices. Removal is eventually consistent: when a pod leaves, each proxy stops routing to it only after it observes the update — and they don't all observe it at the same instant.
SimulateDrain models three kube-proxies with propagation delays
[2,3,5] ticks:
-
WRONG (
readinessFirst=false) — the pod exits immediately, but the proxies keep routing to it for their propagation delays. Result:(2+3+5)×10 = 100requests dropped (TestDrainRaceDropsWithoutReadiness). This is the "every deploy drops a few requests" bug, and it's why a naivesleep 30 && exitdoesn't fix anything — sleeping doesn't make the LB stop sending you traffic. -
RIGHT (
readinessFirst=true) — readiness fails first, so removal starts propagating while the pod keeps serving; it only exits after every proxy has stopped routing to it (within the grace period). Result: 0 dropped (TestDrainNoDropWithReadinessFirst). The ordering is always: fail readiness → drain → exit. This is the Kubernetes form of the gw-01 drain ordering, made precise. -
RIGHT but grace too short —
TestDrainGraceTooShortadds a proxy that takes 50 ticks to update with only a 10-tick grace: the pod is SIGKILLed mid-propagation and the slow proxy's traffic drops. This is the gw-05 problem: a high-density WebSocket node needs a grace period (terminationGracePeriodSeconds) of minutes; the default 30s would SIGKILL 200k connections mid-migration. Same model, different timeout.
In a real pod, "fail readiness first" is a preStop hook that flips the
readiness probe (and triggers your app drain — gw-01/gw-05), with
terminationGracePeriodSeconds sized to cover propagation +
in-flight-request completion.
EndpointSlice/Shard model the other scalability detail: endpoints are
sharded (~100 per slice) so a 1000-pod Service doesn't ship one giant
object on every change (TestEndpointSliceSharding). Those slices are
the membership the control plane turns into EDS (gw-08) and the gw-04
subset ring consumes.
2. conntrack exhaustion (conntrack.go)
On nodes that do netfilter/NAT (most Kubernetes setups), every connection
consumes a slot in the kernel's nf_conntrack table. Conntrack models
it: Track(flow) reuses an existing flow for free but consumes a slot
for a new flow, dropping when full.
SimulateConntrack(100, 1000, ...):
- churn (a new connection per request) → 1000 new flows into a
100-slot table → 900 drops (
TestConntrackExhaustionUnderChurn). The symptom is "the node randomly drops connections under load" with a crypticnf_conntrack: table fullin dmesg — easy to misdiagnose as a network problem. - keep-alive (reuse one flow) → 0 drops
(
TestConntrackOkWithKeepAlive).
The punchline ties the phase together: connection reuse (gw-04) is not
just a CPU/latency optimization — it's also what keeps you under the
conntrack ceiling. The durable fix for conntrack exhaustion is fewer
connections (pool + keep-alive + h2 multiplexing); raising
nf_conntrack_max is the stopgap.
3. The rest of the model (read CONCEPTS for depth)
The simulations cover the two incident-grade behaviors; CONCEPTS.md covers the full picture you must be able to narrate:
- The packet path: Service DNS → ClusterIP → kube-proxy DNAT (iptables / IPVS / eBPF) → pod IP → CNI (veth, overlay vs native routing) → container.
- kube-proxy is L4 only — connection-level DNAT, not request/latency aware; this is why h2/gRPC behind a bare ClusterIP hits the multiplexing trap (gw-02) and why you still need real LB (gw-06).
- CNI wiring (veth pairs, IPAM, overlay vs native), NetworkPolicy,
externalTrafficPolicy(Cluster vs Local — even spread + SNAT vs client-IP preservation), and NRI/OCI hooks for per-workload runtime customization (the Netflix talk).
docs/analysis.md lists the hands-on cluster exercises (trace a packet
with nsenter, watch EndpointSlices during a rollout, reproduce the
drain race and conntrack exhaustion on a kind cluster) to do once you
have the simulated reflexes.
4. Exercises
- Reproduce both on a real
kindcluster: run awrkloop against a Service while deleting a pod with vs without apreStopthat fails readiness; then lowernf_conntrack_maxand blast short connections. Confirm the simulated numbers match reality qualitatively. - Trace a packet:
nsenterinto a pod's netns, find its veth peer, dump theiptables/ipvsadmrules for the ClusterIP, follow the DNAT to a pod IP. - Size a grace period: given EndpointSlice propagation P and
in-flight request duration D, derive
terminationGracePeriodSecondsfor an L7 node and for a 200k-connection WebSocket node (gw-05). - Model
externalTrafficPolicy: extend the drain sim to compareCluster(even spread, SNAT, loses client IP) vsLocal(client-IP preserved, node-local only) and the load-imbalance trade-off. - Wire to gw-08: turn the sharded EndpointSlices into EDS resources and feed the gw-04 subset ring; debounce re-subsetting under pod flapping so the fix doesn't cause churn.