Regions and zones
How to put nodes in regions and zones, keep a tenant and its replica in one region, and give a region a directory of its own, so that it goes on taking writes when it is cut off from the others.
Regions
Give each node a region and a zone — a datacenter in the region. A tenant lives in one region, its node's, and its replica is kept in the same region in another zone, so a failover keeps both its latency and where its data is.
curl -X PUT localhost:8090/_shard/nodes/eu1 \
-d '{"addr":"10.1.0.11:8081","token":"a1","region":"eu","zone":"fra"}'
curl -X PUT localhost:8090/_shard/nodes/eu2 \
-d '{"addr":"10.1.1.11:8081","token":"a2","region":"eu","zone":"ams"}'
curl -X PUT localhost:8090/_shard/tenants/acme -d '{"region":"eu"}'
{"tenant":"acme","node":"eu1","region":"eu","replica_on":"eu2"}- A tenant is placed in the region it names, on the node with the
fewest bytes there. A region with no node is refused with
409, and nothing is created. - It leaves its region only by a move that names the region it
goes to:
{"to":"us1","region":"us"}. - Run one router a region: standby routers follow the primary's directory and forward every tenant to its node, wherever it lives.
How regions affect nodes, tenants, copies, moves, routers and leases
Nodes. PUT /_shard/nodes/<n> takes
region and zone beside addr and
token, names of [a-z0-9_-] as a tenant's are. One
left out is the one recorded, so a node given a new token stays where it is.
A node's region is fixed while it holds a tenant, a tenant's replica, or a
place in a pair — its tenants' residency would change under them — so it
changes once they are moved off; its zone may change at any time. A node
recorded without a region is in the unnamed region, which is one region like
any other: a directory that names none places, replicates and fails over as
it did before regions, and "" names it in a request.
Tenants. A tenant's region is its node's — kept on the node alone,
so a move or a failover changes it with the node and there is no second
record to drift. PUT /_shard/tenants/<t> takes
region, node or both, which must agree
(409 otherwise). With neither it goes to the region
fenec-shard --default-region names, and without that flag to the
node with the fewest bytes whatever its region, as before. GET
/_shard/tenants/<t> answers its node, state, region and zone, its
copy's node — a replica of its own or its node's standby — and
"same_zone": true where that copy is in its zone; the list
carries each tenant's region, and ?region=eu gives one region's
tenants.
curl localhost:8090/_shard/tenants/acme
{"name":"acme","node":"eu1","state":"active","region":"eu","zone":"fra","replica":"eu2"}Copies. With --replicas a tenant's replica goes to a
node of its region only — the replica is what a failover promotes, and one in
another region would carry the tenant out of its own. Among those, a node of
another zone comes first, before one holding a copy already and before the
fewest replicas: a datacenter lost would take a replica in its primary's zone
with it. A zone not recorded is ranked between, neither known to be another
nor the same. Where every free node of the region is in the primary's zone,
the replica goes there all the same: the router logs it, the tenant answers
"same_zone": true, and
fenec_router_same_zone_copies{region} counts it for as long as it
stays. A region with no other node gives none ("unreplicated": "there is
no other node in region `eu` to keep it") until a repair finds one. A
pair's standby must be in its node's region (409 otherwise), and
one in its zone is logged. So a failover — by hand or on a lease — promotes
a tenant within its region: its copy was never placed anywhere else.
Moves. {"to": "eu2"} moves a tenant to a node of its
region; to a node of another, it is refused naming the region the request
must name. {"region": "us"} picks the node with the fewest bytes
there, {"to": "us1", "region": "us"} names both; either way the
answer carries the region the tenant is in now. Its replica goes with it
where it still fits — in the new region, in no worse a zone than a node free
there — and is otherwise taken off and placed again in the new region: kept,
a failover would take the tenant back out of the region it moved to. A move
across regions copies the tenant's image over the network between them, the
tenant frozen for writes meanwhile.
One router a region. The directory is one file with one primary
router, which takes every change. A standby router in each other region
follows it (A standby router) and forwards every
tenant, from the same placements, to its node wherever it is; its
/_shard/ answers reads, GET /_shard/tenants/<t>
and POST /_shard/query included, and refuses changes with
409 naming the primary — asked again it would refuse again, so
it is no 503. Clients reach the router nearest them, by GeoDNS or
an anycast address in front; a request for a tenant in another region crosses
between regions once, router to node, as it would going there itself. A
standby router added 23.7 µs p50 to a request over the node's own, as the
primary did — 23.0 to 23.8 and 23.1 to 23.9 over five runs, the two asked in
turns: following costs a request nothing (make shard-bench
ARGS=standby, node and routers in one process on loopback).
# eu: the primary router, its directory replicated to the others
fenec-shard --listen 0.0.0.0:8090 --token $ROOT --directory shard.fenec \
--replication-token $DIR --replicas --default-region eu
# us and tr: a standby router each, following it
fenec-shard --listen 0.0.0.0:8090 --token $ROOT --directory standby.fenec \
--replication-token $DIR --replica-of http://router.eu.internal:8090
# every region's nodes, two zones each, recorded through the primary
for n in "eu1 10.1.0.11 eu fra" "eu2 10.1.1.11 eu ams" \
"us1 10.2.0.11 us iad" "us2 10.2.1.11 us pdx" \
"tr1 10.3.0.11 tr ist" "tr2 10.3.1.11 tr ank"; do
set -- $n
curl -X PUT router.eu.internal:8090/_shard/nodes/$1 -H "Authorization: Bearer $ROOT" \
-d "{\"addr\":\"$2:8081\",\"token\":\"$ADMIN\",\"region\":\"$3\",\"zone\":\"$4\"}"
done
curl -X PUT router.eu.internal:8090/_shard/tenants/acme -H "Authorization: Bearer $ROOT" \
-d '{"region":"tr"}'
{"tenant":"acme","node":"tr1","region":"tr","replica_on":"tr2"}A standby router is behind the primary by a directory change on its way,
one network trip between regions; a move deletes the source copy only once
every standby keeping up has been sent the new placement, so a standby in
another region routes the tenant's next request to the target and not to a
node that no longer holds it. A promotion is the same as with one standby:
POST /_replication/promote on the router to take over, the others
pointed at it with --replica-of.
A region's leases. Under --auto-failover the router
that keeps a node's directory leases it. With one directory that is the
primary router, so a region cut off from the primary's would stop taking
writes as its leases lapse, and could not be failed over until the router
reached its nodes again. A region that keeps its own directory is leased and failed over
by its own router, and goes on writing through a partition. A client far
from a tenant's home reads near itself where the tenant has a read replica in its region; no SDK picks a router
by region: a client is given the router nearest it.
The directory's format. nodes took
region and zone fields, which a directory written
before them is given as it opens (an alter); its nodes read the
unnamed region. A router from before them reads the directory as it always
did — it ignores the fields, forwards every tenant where it is, and, promoted,
places without regard to regions.
A region that keeps its own directory
Give a region a router of its own that keeps the region's directory: its nodes and tenants. That router leases the region's nodes and fails them over, so the region goes on taking writes — and fails a node over inside itself — while it is cut off from every other region.
# us: the region's router, its directory in us.fenec, the global one followed from eu
fenec-shard --listen 0.0.0.0:8090 --token $ROOT --directory us.fenec \
--replication-token $DIR --replicas --auto-failover 5 \
--region us --global http://router.eu.internal:8090
# on the global router: us keeps its own directory, kept by that router
curl -X PUT router.eu.internal:8090/_shard/regions/us -H "Authorization: Bearer $ROOT" \
-d '{"routers":["http://router.us.internal:8090"]}'
# us's nodes and tenants, through either router
curl -X PUT router.eu.internal:8090/_shard/nodes/us1 -H "Authorization: Bearer $ROOT" \
-d '{"addr":"10.2.0.11:8081","token":"a3","region":"us","zone":"iad"}'
curl -X PUT router.eu.internal:8090/_shard/tenants/acme -H "Authorization: Bearer $ROOT" \
-d '{"region":"us"}'
{"tenant":"acme","node":"us1","region":"us","replica_on":"us2"}- Inside the region, nothing waits for another: leases, failovers, repairs, moves between its nodes, quotas and its nodes are its router's.
- What needs every region waits for them: a create (the name is
global), a delete and a move across regions go through the global router
and are refused with
503andRetry-Afterwhile a region they touch cannot be reached — nothing is done by half. - Every router reaches every tenant: one asked for a tenant of another region relays the request to that region's router.
Two directory levels, fencing and a partition step by step
Two levels. The global directory — the router started without
--region, and its standbys — holds every tenant's name, the
regions keeping their own directory and their routers (PUT
/_shard/regions/<r> {"routers": [...]}, the one keeping it
first, its standbys after). For each tenant of such a region it holds a home,
the region and no node. The region's router holds the region's nodes and
placements in a directory of its own (--region us), the same
collections, and follows the global one from --global. A region
that keeps no directory of its own — the unnamed one, and every region of a
deployment that names none — is the global directory's, so a deployment
without --region places, leases and fails over exactly as
before, and one may move its regions out one at a time.
Who changes what. A change touching one region's nodes or tenants is that region's router's: a node added, a tenant's quota, a move between two of its nodes, a failover by hand, a repair. A change the global directory takes is the global router's: a create takes the tenant's name there first and then asks the region's router to make it (the name given back if that router refuses or cannot be reached; kept where it says the tenant exists, which takes in a create whose answer was lost), a delete asks the region's router first and then gives the name back. Either router takes either kind: what is not its own it relays, once — a region's router to the global one, the global router to the region's, marked so it goes no further.
A move across regions is the global router's, step by step: the
tenant frozen in the region it leaves and its image taken, installed and
recorded in the region it goes to — leased and given its replica there —
let go of in the region it left, whose directory then places it nowhere, so
no lease there names it; and only then its home moved in the global
directory, one statement, which routes it to its new region. Each step is
undone where the next fails: a region not reached leaves the tenant where it
was, taking writes again, and the move is answered 503. A move
between a region keeping its own directory and one the global directory
keeps works the same way, the global router doing that region's part
itself.
Forwarding. A router asked for a tenant its own directory does not
place looks it up in the global directory — its own, or its copy — and
sends the request to its node, or, for a tenant of a region keeping its own,
to that region's routers, the one that answered last first. They forward it
to the node as the region's last failover left it: nobody outside the
region needs to have heard of the failover, and after a partition every
router reaches the new placement with its first request. The relay costs a
request a hop inside the tenant's region, which a client asking the router
of its own region never pays: 32 to 42 µs p50 on loopback, a router's 24 and
the proxy that stood for the link between the regions (make
shard-bench ARGS=regions). A router relaying names the client beside it,
marked with an HMAC of the routers' shared --token, so a refused
token waits by the client's address, at the first router alone.
Fencing. A lease names its holder: the directory it comes from and
that directory's term, the number of times its history forked. Promoting a
router forks it, so a region's router promoted over the one before — a
standby following its directory, in the region or in another — grants at a
higher term, and every node that hears from it refuses the old one from then
on: its grants (409 naming who holds the node), its promotions
and its re-pointing of replicas. A node that only the old router still
reaches cannot be told; that is why a router is promoted once the one before
it is gone, as with one directory. A node from before terms takes a grant
without checking its term, so a region's router leases it but never fails it
over on its own. List a region's standby routers after its router in
routers: a relay moves to the next one where the first cannot be
reached, and a standby forwards from its copy of the directory and refuses
changes until it is promoted.
A partition, step by step. The global router in eu, us keeping its own directory; the link between the regions goes down.
- Inside us, nothing changes: us's router renews its nodes' leases, and
its clients write as before. A request from eu for a us tenant is
answered
502— a region out of reach, as a node out of reach is — and a client asks again; inside eu, eu's tenants are written as before. - A node of us lost or cut off stops taking writes as its lease lapses,
and us's router promotes its tenants on their replicas in us, a tenth of
a lease later. With a lease of a second, a node cut off stopped writing
0.38 to 0.63 s after the cut and its 10 tenants wrote again on their
replicas 1.41 to 1.70 s after, ten runs; with five seconds, 3.9 to 4.1 s
and 5.5 to 6.7 s — as the global router fails over its own, 1.27 to 1.36
s at a second (
make shard-bench ARGS=regions, the region cut off from the global router throughout, three nodes and both routers in one process). - A create in us, a delete of a us tenant, a move into or out of us are
refused with
503andRetry-After, through any router, and change nothing. - The link comes back: the global router's next relay reaches us's router, which places the failed-over tenants where it put them; the refused changes go through when asked again.
Formats. The global directory took a regions
collection, and a tenant at home in such a region is a row of
tenants whose node is @<region>, which no node
can be named; a region's directory names its region in scope,
and a router opened over the wrong one refuses it. A router from before
regional directories reading the global one forwards no request of such a
tenant (no node @us: 404), refuses every change of
it, takes no name it holds, and leases none of the region's nodes, which it
does not know. A lease grant carries authority,
term and history, and its own number and the one
it acknowledges (grant, acked); a node from before
reads the grant as it did. Not done: a query across tenants (POST
/_shard/query) asks the tenants the router's own directory places, and
names each tenant of another region's directory as failed rather than leave
it out; ask that region's router for its own.