Summary in my words:
Multiple L7 load
balancers talk to multiple backend servers. Backend servers don't have to worry
about path MTU discovery, TCP congestion control algorithms, avoidance of the
TIME-WAIT state and various other low-level details. But how do you load balance
the L7 LBs?
L3 load balancing
can help. All L7 LBs can advertise the same IP to their nearby routers. With a
combination of ECMP and BGP protocols, routers can load balance the packets to
L7 LBs. But adding/removing an L7 LB in this setup breaks existing connections(which
is problematic for long connections like downloads) since routers don't use
consistent hashing. Also, if one router becomes unavailable, other routers may
forward the packets to a different LB since every router decides
individually.
To mitigate this, use L4 LBs between L3 and L7. The purpose of this tier is to
ensure that all members take same scheduling decision for an incoming packet
based on destination IP and port. Consistent hashing is used to route packets
to L7 LBs so removal/addition of an L7 LB isn't problematic. Additionally L7
LBs return response directly to L3 routers(DSR) to improve perf.
So, routers use ECMP + BGP to route packets to L4 LBs(all of which advertise
the same IP). L4 LBs use consistent hashing scheme to talk to L7 LBs. And L7
LBs return the response directly to L3.
Actual Snippets from the article:
A combination
of:
- ECMP routing
- Stateless L4 load-balancing
- Stateful L7 load-balancing
L7 (Last Tier):
Working in the highest layers of the OSI model, it can also offer additional
services, like TLS-termination, HTTP routing, header rewriting, rate-limiting
of unauthenticated users, and so on. Being stateful, it can leverage complex
load-balancing algorithm. Being the first point of contact with backend
servers, it should ease maintenances and minimize impact during daily changes.
It also terminates
client TCP connections. This introduces some loose coupling between the
load-balancing components and the backend servers with the following benefits:
- connections to servers can be
kept open for lower resource use and latency,
- requests can be retried
transparently in case of failure,
- clients can use a different
IP protocol than servers,
- and servers do not have to
care about path MTU discovery, TCP congestion control algorithms,
avoidance of the TIME-WAIT state and various other low-level details.
As is, this solution
is not complete. We have just moved the availability and scalability problem
somewhere else. How do we load-balance the requests between the load-balancers?
First tier: ECMP routing (L3)
On most modern
routed IP networks, redundant paths exist between clients and servers. For each
packet, routers have to choose a path. When the cost associated to each path is
equal, incoming flows are load-balanced among the available destinations. This characteristic
can be used to balance connections among available
load-balancers.
If we assume you already have BGP-enabled routers available, ExaBGP is a
flexible solution to let the load-balancers advertise their availability.
Unfortunately, this solution is not resilient when an expected or unexpected
change happens. Notably, when adding or removing a load-balancer, the number of
available routes for a destination changes. The hashing algorithm used by
routers is not consistent and flows are reshuffled among the available
load-balancers, breaking existing connections.
Moreover, each
router may choose its own routes. When a router becomes unavailable, the second
one may route the same flows differently which again breaks existing
connections.
If you think this is
not an acceptable outcome, notably if you need to handle long connections like
file downloads, video streaming or websocket connections, you need an
additional tier. Keep reading!
Second tier: L4 load-balancing
The second tier is
the glue between the stateless world of IP routers and the stateful land of L7
load-balancing. It is implemented with L4 load-balancing. The terminology can
be a bit confusing here: this tier routes IP datagrams (no TCP termination) but
the scheduler uses both destination IP and port to choose an available L7
load-balancer. The purpose of this tier is to ensure all members take the same
scheduling decision for an incoming packet. There are two options:
- stateful L4 load-balancing
with state synchronization across the members,
- or stateless L4
load-balancing with consistent hashing.
The first option
increases complexity and limits scalability. We won’t use it. The second option
is less resilient during some changes but can be enhanced with a hybrid
approach using a local state.
We use IPVS, a
performant L4 load-balancer running inside the Linux kernel, with Keepalived, a
frontend to IPVS with a set of healthcheckers to kick out an unhealthy
component. IPVS is configured to use the Maglev scheduler, a consistent hashing
algorithm from Google. Among its family, this is a great algorithm because it
spreads connections fairly, minimizes disruptions during changes and is quite
fast at building its lookup table. Finally, to improve performance, we let the
last tier—the L7 load-balancers—sends back answers directly to the clients
without involving the second tier—the L4 load-balancers. This is referred to as
direct server return (DSR) or direct routing (DR).
With such a setup, we expect packets from a flow to be able to move freely
between the components of the first two tiers while sticking to the same L7
load-balancer.
Tier 0: DNS load-balancing
Optionally, you can
add DNS load-balancing to the mix. This is useful either if your setup is
spanned across multiple datacenters, or multiple cloud regions, or if you want
to break a large load-balancing cluster into smaller ones. It is not intended
to replace the first tier as it doesn’t share the same characteristics:
load-balancing is unfair (it is not flow-based) and recovery from a failure is
slow.