Notes
The problem
One server has two problems. A ceiling: at some point the CPU, memory, or network maxes out, and responses slow down or time out. And a single point of failure: if it crashes, or you deploy new code, everyone sees an error.
As we saw last episode, the fix is to scale out: run several copies of a stateless server. But now users need one address, and something has to decide which copy handles each request.
The idea
That's a load balancer. It sits in front, with one public address. Every request comes to it, and it forwards each one to one of the servers behind it, then passes the response back.
Its jobs: spread the load so no server is overwhelmed.
Skip servers that are broken.
And let you add, remove, or update servers without users noticing. Next, how does it choose?
Algorithm 1
Algorithm one: round robin. Take turns: A, then B, then C, then back to A. Simple and fair, when every request costs about the same and every server is the same size.
- A → B → C → A → B → C …
- simple and fair
- when requests and servers
- are all alike
Algorithm 2
Algorithm two: weighted round robin. If server A is three times bigger, give it a weight of three, and it gets three requests for every one the others get.
- A A A → B → C → A A A …
- for servers of different sizes
Algorithm 3
Algorithm three: least connections. Send the next request to whichever server is least busy right now. That's better when requests vary a lot, some taking five milliseconds and some five seconds.
- send to the server with
- the fewest open connections
- good when requests vary:
- some take 5 ms, some 5 s
Algorithm 4
Algorithm four: hashing. Hash something about the request, like the user id or IP address, and the same user always lands on the same server. With consistent hashing, adding a server only moves a small share of users. It's the base of most caches and sharded databases.
server = hash(user_id) % N same user → same server consistent hashing: adding a server only moves a small share of keys
Health checks
How does it know a server is broken? Health checks. Every few seconds it calls something like /health on each server. If a server fails several checks in a row, it's taken out of rotation. When it recovers, it's added back.
L4 vs L7
Load balancers come in two kinds. Layer four balancers work with TCP: they see addresses and ports, and they're extremely fast. Layer seven balancers understand HTTP: they can read the URL, headers and cookies, so they can send /api to one group of servers and /images to another.
- L4 (TCP/UDP): sees IPs and ports
- very fast, can’t read the request
- L7 (HTTP): reads URL, headers, cookies
- /api → API servers
- /images → image servers
Sticky sessions
Sometimes a server keeps session data in memory, so a user must keep returning to it. Load balancers can do that, with sticky sessions. It works, but it fights the balancing, and if that server dies the session is lost. The better fix is stateless servers, with sessions in a shared store.
- send a user back to the same server
- (cookie or IP based)
- needed only for stateful servers
- better: keep sessions in Redis
Who balances the balancer?
A sharp interviewer will ask: isn't the load balancer itself a single point of failure? Good question.
The answer: run two. An active one and a standby that watches it with heartbeats, and takes over its address if it fails. Cloud load balancers do this for you, spread across zones. And DNS can point to balancers in several regions.
In practice
In practice you'll meet software balancers like Nginx, HAProxy and Envoy, cloud ones like Amazon's Application and Network Load Balancers, and in Kubernetes, services and ingress controllers. Same ideas everywhere.
- hardware: F5
- software: Nginx, HAProxy, Envoy
- cloud: AWS ELB / ALB / NLB,
- Google Cloud Load Balancing
- Kubernetes: Services, Ingress