Load balancers and proxies
One address, many servers. Watch round robin feed a crashed backend, watch least connections make it worse, then fix it with health checks - and learn why only an L7 proxy can route by path.
A popular site isn’t one server. It’s one address in front of many servers, with a load balancer or reverse proxy deciding which server gets each request. Clients never see the decision. That’s the point, and it’s also why a bad decision is so hard to spot from outside.
A balancer makes two choices for every request: which servers are allowed to take it, and which one of those gets it.
Which one: the algorithm
Round robin takes turns: 1, 2, 3, 1, 2, 3. It’s NGINX’s default. Least connections sends each request “to the server with the least number of active connections” (NGINX docs). That sounds smarter, because busy servers get a rest.
Here’s the catch. A crashed server that refuses connections instantly never has an active connection, so least connections thinks it’s the least busy server in the pool and keeps choosing it.
Which are allowed: health checks
A health check probes each backend on a timer and takes failing ones out of rotation until they pass again. Without one, the balancer keeps sending a dead server its share of requests. Real balancers let you tune how many failures it takes: NGINX’s passive check marks a server unavailable after max_fails=1 failure within fail_timeout=10s by default. This simulation pulls a backend after two failed checks and lets it back after two good ones.
NGINX’s defaults go further: a failed connection is retried on the next server (proxy_next_upstream error timeout), and one failure takes a server out for 10 seconds - which would hide a refusing backend from customers. The simulator turns both off, the way a plain layer 4 balancer or a pool with no checks behaves, so you can see what the checks protect you from.
What a balancer can see
- A request for /api/orders leaves the client. Which server can answer it depends on the path.
- An L4 balancer forwards the TCP connection. The path is inside bytes it never reads - and inside TLS, it could not read them - so it picks a backend blind.
- An L7 proxy ends the connection and reads the HTTP request: the Host header and the path. Now /api can go to the API and / to the web servers.
That’s why routing by hostname or path, as in “grafana.localhost goes to Grafana” or “/api goes to the API”, is a layer 7 job. In Kubernetes, the Gateway API’s HTTPRoute describes exactly those rules: hostnames, path matches and the backends they go to.
One of three backends crashed and refuses connections. Which change stops customers seeing errors?
Least connections favours the crashed backend, because it never holds a connection. A fourth backend only dilutes the errors to one in four. Health checks remove the dead member, which is the only change that gets errors to zero while it stays broken.
You need /api to reach a different service from /. What must the balancer be able to do?
The path is part of the HTTP request. A layer 4 balancer passes the connection's bytes along without reading them, so a path rule means nothing to it. Only a layer 7 proxy that parses the request can match on the path.
Everything above ran in your browser. Now do it on a real cluster with tilt lab 02-route-a-hostname (profile minimal). You need Docker, kind and Tilt, about 15 GB of free disk and 8 GB of spare RAM. Setup guide · Can my machine run it?
The tilt platform’s front door is an L7 proxy: an Istio-managed Gateway listening for *.localhost (HTTPS on 443, plus port 80 for the redirect), and an HTTPRoute per app that matches on hostname. Read the rules that route grafana.localhost and prometheus.localhost to different backends:
kubectl get gateway -n istio-system
kubectl get httproute -A
kubectl get httproute grafana -n monitoring -o yaml # hostnames, matches, backendRefsLab 02 then has you route a second hostname yourself.
Sources
Missions and quizzes above earn their own XP. This marks the lesson done.