Replicas, standbys and failover
How to keep a copy of every tenant on another node, fail a node over by hand or on a lease, and run a second router that follows the first. The tutorial fails one node over by hand; these are the other ways.
Replicas of tenants
Pair a node with a standby node, and every tenant on it is copied there as it is written. When the node is lost, one call promotes its tenants on the standby.
fenec-server --dir /data/n1 --http 127.0.0.1:8081 --admin-token a1 \
--replication-token r1 --sync always
fenec-server --dir /data/n2 --http 127.0.0.1:8082 --admin-token a2 \
--replication-token r1 --replica-of http://127.0.0.1:8081
curl -X PUT localhost:8090/_shard/nodes/n1 \
-d '{"addr":"127.0.0.1:8081","token":"a1","standby":"n2"}'- Failover:
POST /_shard/nodes/<n>/failover. - Use
--sync alwayson the primary so a failover loses no write.
How a standby follows every tenant
Every tenant file has a feed of its own, so a node's tenants are followed
by another node's — the standby — tenant by tenant, through
/t/<tenant>/_replication.
The router creates a tenant on both, so the replica has a file to follow
into, and deletes it from both; a move takes the replica with it. Recording a
pair gives the standby a following copy of every tenant already on the node.
A replica node opens every tenant it holds at start and keeps them open — a
follower that is not running is a replica falling behind — so
--idle-close does not apply to them. No tenant is placed on a
standby or moved to one: it would open there as a replica of a tenant the
other node does not have.
A tenant's role is its file's, not the node's. On a node started with
--replica-of, a replica's file follows and a new one follows,
but a primary's file stays a primary — a tenant a failover promoted there
included, whatever the node restarts with. Were every file on such a node
made to follow, a restart with the same flags would undo the failover, and
the old primary's copy would wipe the writes taken since.
When a node is gone, POST /_shard/nodes/<n>/failover
promotes its tenants on the standby, one at a time, and points the directory
at it. They are separate databases: there is nothing to make atomic between
them, so the answer lists what was promoted and what was not. A copy that
is not its tenant as the directory knows it — of another lineage, or an old
primary's copy that never heard from the new one — is refused by its node
and listed as not promoted, the tenant left where its rows are; with
-d '{"force": true}' it is promoted whatever it holds, which
the audit log records. Under --auto-failover the router grants
each node the tenants went to its lease over them before it answers, so
they take writes as soon as the answer comes: left to the next round, a
promoted copy refused them for up to a third of a lease.
curl -X POST localhost:8090/_shard/nodes/n1/failover
{"node":"n1","to":"n2","promoted":["acme","beta"],"failed":[]}Measured with make shard-bench, node, standby and router in
one process on loopback: a write through the router is answered in 2.93 ms
(--sync always on the primary, which is an fsync a write) and is
on the standby 0.089 ms later, 0.448 ms at p99 — timed by reading the
standby's own copy. A failover of 20 tenants took 63.5 to 79 ms, the
medians of three rounds of four to twelve runs, 3 to 4 ms each, and the
first write on the promoted node 0.18 ms.
A promoted tenant's history forks where the replica stood, so what the
primary had not sent is gone — the same trade a database's failover makes,
and --sync always on the primary is what makes it none. A
failover that moved every tenant ends the pair; one that left a tenant behind
keeps it, so a second call can take it. The old node rejoins as the standby of
the new one: point its --replica-of there and record the pair the
other way round. The router then has each of its tenants follow
(POST /_admin/tenants/<t>/follow) — its own files are a
primary's, and follow only when told to — and the writes it took that were
never sent are gone.
fenec-server --dir /data/n1 --http 127.0.0.1:8081 --admin-token a1 \
--replication-token r1 --replica-of http://127.0.0.1:8082
curl -X PUT localhost:8090/_shard/nodes/n2 \
-d '{"addr":"127.0.0.1:8082","token":"a2","standby":"n1"}'
{"node":"n2","unreplicated":[]}A tenant file that still follows on a node that follows nobody — a failover
that left it behind, and the node restarted without --replica-of
— refuses writes, says so in the log, and is promoted where it stands with
POST /_admin/tenants/<t>/promote (with
-d '{"force": true}' where it holds no write and has heard
nothing from its primary since it opened).
Replicas of their own
Instead of an idle standby, fenec-shard --replicas gives each
tenant its own replica on another node, so every node holds both primaries
and replicas and a lost node's tenants spread over the rest.
fenec-server --dir /data/n1 --http 127.0.0.1:8081 --admin-token a1 --replication-token r1 --sync always
fenec-server --dir /data/n2 --http 127.0.0.1:8082 --admin-token a2 --replication-token r1 --sync always
fenec-server --dir /data/n3 --http 127.0.0.1:8083 --admin-token a3 --replication-token r1 --sync always
fenec-shard --replicas
curl -X PUT localhost:8090/_shard/tenants/acme -d '{"node":"n1"}'
{"tenant":"acme","node":"n1","replica_on":"n2"}- After a failover,
POST /_shard/replicasgives every tenant without a replica a new one. - A replica that did not take — its node refused, or answered after the router stopped waiting — is tried again on its own, a few times over the next four minutes.
fenec_router_tenants_unreplicatedcounts the tenants with no copy to fail over to, by region (Monitoring).- A file the router may have left on a node is reported, never
deleted: every repair lists it under
left, andfenec_router_files_leftcounts it.
How --replicas places a copy for each tenant
A standby waits idle, and takes every tenant of its node at once when the
node goes. With --replicas the router gives each tenant a
replica of its own instead, on another node in no pair: the one holding the
fewest replicas of that tenant's node, then the fewest tenants and replicas
altogether. Each node's tenants then follow on all the others, and every
node holds primaries and replicas both.
The replica's node is told which node to follow
(POST /_admin/tenants/acme/follow with
{"from": "http://127.0.0.1:8081"}) and keeps it in
acme.follows beside the file, so the tenant follows again when
the node starts over. The replication token is the nodes', one for the
cluster. A move takes the replica along: the copy goes on following the
tenant on its new node, since the image carried its history, and when the
tenant moves onto the node holding its replica, that copy goes and another
is placed. A delete removes both.
A failover of a node in no pair promotes each of its tenants on its own
replica, one at a time, and routes it there. The tenants whose replicas it
held are named as unreplicated. POST /_shard/replicas gives every
tenant without a replica one, on a node holding a copy of it first: a node
back from a failover has its old primaries follow the nodes that took them
over, and nothing of theirs is left lying there.
curl -X POST localhost:8090/_shard/nodes/n1/failover
{"node":"n1","promoted":[{"tenant":"acme","to":"n2"},{"tenant":"beta","to":"n3"}],"failed":[],"unreplicated":["gamma"]}
curl -X POST localhost:8090/_shard/replicas
{"replicated":[{"tenant":"acme","replica":"n1"},{"tenant":"beta","replica":"n1"},{"tenant":"gamma","replica":"n1"}],"left":[],"failed":[]}A replica that did not take is tried again. A create, a move or a repair
that gives a tenant its replica makes the file on the other node and tells
it to follow the primary; where that node refuses, or takes the follow past
the router's --upstream-timeout — a rename on a node under load
took 13 s, longer than the deploy kit's 10 s — the answer names the tenant
unreplicated and a repair runs again 2 s later, then 4, 8 and
so on up to a minute apart, eight times, about four minutes in all. It asks
again whether the tenant still needs one, so it is placed once, and it
adopts a copy whose follow landed after the router stopped waiting: the
node already holding the copy comes first among those of the zone the rule
picks, so no second copy is made. A node that did not answer the repair's
look is given no replica. A tenant whose region has no other node is not
tried again: add a node, then POST /_shard/replicas.
A file the router made on a node, or meant to take off one, that no
placement names — a create taken back (its schema refused, say) whose
DELETE the node did not take, a create whose PUT
went unanswered, a move undone whose target kept the copy, a move's source
that would not go, a copy a failed try made or found, a deleted tenant's
copy that stayed, an old primary on a node that was silent as its tenant's
replica went elsewhere — is noted in the directory with why, and reported:
every repair lists it under left
({"tenant","node","why"}), fenec_router_files_left
counts it, and FenecFilesLeftOnNodes alerts after half an
hour. The router never deletes one. Neither it nor the node can tell the
file it left from a later one at the same place: the tenant moved back
onto that node, or made there again, or a file put there by hand. A create
of the name on that node is answered 409 while the file is there. A
repair's look forgets a file it finds gone, or one the directory now
places, so an operator removes it by hand after looking at it, then runs a
repair (deploy/RUNBOOK.md, Files left on nodes). Only a
tenant left without its replica makes a repair answer 502. Tenants left
without a copy are counted by
fenec_router_tenants_unreplicated{region}, which
FenecTenantsWithoutReplica in deploy/alerts/
reads.
Measured with make shard-bench, three nodes and the router in
one process, 30 tenants: each is created with its replica in 33 ms, the
replicas 10 a node, and a write is on its replica 0.179 ms after it was
answered, 0.421 ms at p99. A failover of one node's 10 tenants took 57 ms, 5
onto each of the other two, and the repair after it gave 20 tenants a
replica in 176 ms, all on the returning node's copies. Placed by the fewest
altogether alone, every tenant of one node followed on the same other: a
tie went by name each time, and its failover put them all there.
Automatic failover
With --auto-failover the router fails a node over by itself.
It works on leases: a node stops writing when its lease runs out, and only
after that does the router promote its tenants elsewhere, so a tenant never
has two primaries.
fenec-server --dir /data/n1 --http 127.0.0.1:8081 --admin-token a1 \
--replication-token r1 --sync always --lease
fenec-shard --listen 127.0.0.1:8090 --directory shard.fenec --replicas --auto-failover 5- Nodes need
--lease; a node without it is never failed over automatically. - With a 5-second lease, a lost node's tenants cannot be written for 5.5 to 7.2 s. A node cut off from the router while its clients still reach it stops writing two thirds of a lease after its last renewal at the most: its lease runs from the grant before that one.
- What was out to the lost node is answered at its failover, not
at
--upstream-timeout: a read or a keyed write goes to the new primary, any other write is a502that it may have run (Requests out to a node failed over).
How a lease holds, and what leases do not cover, are under Leases in the design.
A standby router
A second router can follow the first one's directory and forward requests too. Only the primary router makes changes; promote the standby when the primary is gone.
fenec-shard --listen 127.0.0.1:8090 --directory shard.fenec --replication-token r1
fenec-shard --listen 127.0.0.1:8091 --directory standby.fenec --replication-token r1 \
--replica-of http://127.0.0.1:8090- Promote:
POST /_replication/promoteon the standby.
How a standby router follows the directory
The directory is a fenecdb file, so it replicates the way any other does. A second router follows it and forwards from the same placements; the primary is the one that changes it.
Both forward /t/<tenant>/...; the standby's
/_shard/ answers reads and refuses changes with 409, since its
directory takes no write of its own. When the primary is gone,
POST /_replication/promote on the standby forks its directory's
history and it starts taking them — the same promotion a replica of a
database gets, and the same rule after it: the old primary rejoins with
--replica-of pointed at the new one, or its file is refused.
A standby is behind by whatever the last directory change has not reached
it yet, which is one write and a sync: placements change when someone places,
moves or deletes a tenant, not when clients read and write. Nothing of the
tenants' own data goes through the router's directory, so a standby that is a
second behind still forwards every request correctly. One whose directory has
not arrived at all — just started, its primary not yet reached — answers
503 with Retry-After rather than 404:
it knows of no tenant, which is not knowing there is none.
Each change the primary makes reaches the standby's maps through the change ring, as a subscription's rows do: the placements it put or deleted since the last one, 4 µs at 100 000 tenants, where reading every map again took 38 ms under the router's write lock. An image adopted, or more changes than the ring holds, and the maps are read again whole.