A single server is a single point of failure: it goes down and
the site is down. A load balancer takes requests on one address
and spreads them across several identical copies of a service —
backends (upstream). One copy fails, requests go to
the others, and users do not notice. On Linux the balancer is usually
nginx or haproxy.
Copies and round robin
upstream app {
server 127.0.0.1:8081;
server 127.0.0.1:8082;
}
server {
listen 80;
location / {
proxy_pass http://app;
add_header X-Upstream $upstream_addr always;
}
}
upstream is a group of copies under one name; proxy_pass sends
requests to the group. By default nginx goes round the copies in turn
(round robin): 8081, 8082, 8081… Other methods and the parameters of
the server line are in nginx:
| Parameter |
What it does |
least_conn; |
a request goes to the copy with fewer connections |
ip_hash; |
one client — always the same copy |
weight=3 |
the copy gets three times more requests |
backup |
the copy gets requests only when the others are unavailable |
down |
the copy is switched off by hand |
Who answered shows in the log as $upstream_addr and
$upstream_status, and in the X-Upstream header. If nginx retried
a request on another copy, the fields become comma-separated lists:
… 200 upstream=127.0.0.1:8082, 127.0.0.1:8081 ustatus=502, 200
The first copy did not answer (502 — connection refused), the
second answered 200. The user got 200.
Passive health check
The balancer has to know which copy is sick. nginx judges by real
requests — a passive health check. A failed request to a copy
adds one to a counter. max_fails failures within fail_timeout —
the copy is switched off for fail_timeout. Then nginx tries it again
with one request.
server 127.0.0.1:8082 max_fails=2 fail_timeout=10s;
The defaults are max_fails=1, fail_timeout=10s: after the first
failure the copy rests for 10 seconds. max_fails=0 turns the count
off — the copy is always considered alive. A copy fixed within those
10 seconds gets requests only after them. What counts as a failure is
set by proxy_next_upstream (below).
A passive check has two downsides: a user's request pays for it, and
it notices a sick copy only through traffic.
Retry on another copy
proxy_next_upstream says which outcomes count as a failure and are
retried on the next copy. The default is error timeout: the copy did
not accept the connection or did not answer in time.
| The copy |
What the user sees by default |
does not listen (Connection refused) |
200: nginx retried on another copy |
answers 500 |
500 every other time: the copy's answer is no failure |
| hangs without answering |
an answer after 60 s: it waits proxy_read_timeout |
Error codes from the copy are added to the list explicitly:
proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
A request that may change something (POST) is not resent once nginx
has passed it to a copy: charging money twice is worse than one
error. How many retries and how many seconds for them —
proxy_next_upstream_tries and proxy_next_upstream_timeout.
Timeouts
| Directive |
Waits for |
Default |
proxy_connect_timeout |
the connection to the copy |
60 s |
proxy_read_timeout |
the copy's answer between two reads |
60 s |
proxy_send_timeout |
the copy to accept the request |
60 s |
A copy unreachable over the network (packets vanish) and a hung copy
cost every request a minute. Short timeouts (2–3 s) together with
timeout in proxy_next_upstream turn the minute into a couple of
seconds and a retry on a live copy. A proxy_read_timeout that is too
short cuts slow but healthy answers: heavy reports, exports.
Active health check
With an active health check the balancer polls each copy itself
every few seconds — for example with GET /health. A copy is switched
off after several failures in a row and back on after several
successes. A sick copy drops out before a user reaches it. That is how
haproxy checks (the check parameter of a server line); free
nginx has no active checks, only passive ones.
Floating address
The balancer is a single point of failure too. It is built from two
machines with a floating address (VIP, virtual IP): clients go to
the VIP, which lives on one machine — the MASTER. keepalived on
both machines exchanges VRRP advertisements. When the MASTER's
advertisements stop, the standby (BACKUP) takes the address, in about
3 seconds with advert_int 1. On systemctl stop keepalived the
MASTER announces its departure itself, and the address moves at once.
$ ip -br a show ens2
ens2 UP 10.55.90.11/24 10.55.90.100/24 fe80::5054:ff:fe8c:5836/64
The second address on the interface is the VIP: this machine is the
MASTER now. If the machines do not hear each other's advertisements,
the address ends up on both — split brain, see
keepalived.