Cluster
Set up and operate a FyroDB cluster — slots, nodes.conf, redirects, migration, and client examples.
FyroDB implements Redis Cluster: 16384 hash slots, MOVED/ASK redirects,
CROSSSLOT detection, hash tags, and a gossip bus for heartbeats and failure
detection. Any cluster-aware Redis client works unchanged.
Quick Start (Docker Compose)
Save this as docker-compose.yml and run docker compose up -d. It brings up
three masters covering all 16384 slots.
services:
cluster-config:
image: busybox
restart: "no"
entrypoint:
- /bin/sh
- -c
- |
cat > /config/nodes.conf <<'EOF'
node-1 127.0.0.1:8000@node-1:18000 master - 1 0 0 connected 0-5461
node-2 127.0.0.1:8001@node-2:18000 master - 1 0 0 connected 5462-10922
node-3 127.0.0.1:8002@node-3:18000 master - 1 0 0 connected 10923-16383
EOF
for i in 1 2 3; do cp /config/nodes.conf /config/nodes-$i.conf; done
volumes:
- cluster-config:/config
node-1:
image: rana718/fyrodb:latest
depends_on:
cluster-config:
condition: service_completed_successfully
environment:
FYRODB_CLUSTER_ENABLED: "true"
FYRODB_BIND: "0.0.0.0"
FYRODB_PORT: "8000"
FYRODB_CLUSTER_PORT: "18000"
FYRODB_NODE_ID: "node-1"
FYRODB_CLUSTER_CONFIG_FILE: "/config/nodes-1.conf"
FYRODB_RDB_PATH: "/data/fyrodb.rdb"
ports:
- "8000:8000"
volumes:
- node-1-data:/data
- cluster-config:/config
restart: unless-stopped
node-2:
image: rana718/fyrodb:latest
depends_on:
cluster-config:
condition: service_completed_successfully
environment:
FYRODB_CLUSTER_ENABLED: "true"
FYRODB_BIND: "0.0.0.0"
FYRODB_PORT: "8000"
FYRODB_CLUSTER_PORT: "18000"
FYRODB_NODE_ID: "node-2"
FYRODB_CLUSTER_CONFIG_FILE: "/config/nodes-2.conf"
FYRODB_RDB_PATH: "/data/fyrodb.rdb"
ports:
- "8001:8000"
volumes:
- node-2-data:/data
- cluster-config:/config
restart: unless-stopped
node-3:
image: rana718/fyrodb:latest
depends_on:
cluster-config:
condition: service_completed_successfully
environment:
FYRODB_CLUSTER_ENABLED: "true"
FYRODB_BIND: "0.0.0.0"
FYRODB_PORT: "8000"
FYRODB_CLUSTER_PORT: "18000"
FYRODB_NODE_ID: "node-3"
FYRODB_CLUSTER_CONFIG_FILE: "/config/nodes-3.conf"
FYRODB_RDB_PATH: "/data/fyrodb.rdb"
ports:
- "8002:8000"
volumes:
- node-3-data:/data
- cluster-config:/config
restart: unless-stopped
volumes:
node-1-data:
node-2-data:
node-3-data:
cluster-config:docker compose up -d
docker compose downThen talk to it with any Redis client:
$ redis-cli -c -p 8000
127.0.0.1:8000> set user:1 alice
-> Redirected to slot [8676] located at 127.0.0.1:8001
OK
127.0.0.1:8001> get user:1
"alice"The -c flag is what makes redis-cli follow redirects. Without it you see the
redirect as an error, which is correct protocol behaviour:
$ redis-cli -p 8000 get user:1
(error) MOVED 8676 127.0.0.1:8001Before you deploy this beyond your laptop, change the client-facing addresses.
The 127.0.0.1:800x above only works for a client on the Docker host — see
nodes.conf below.
Each node uses one worker per CPU by default. Cap it when several nodes share a machine, so they do not oversubscribe it:
FYRODB_WORKERS: "4" # per nodeWithout Docker
Three processes, one config file each:
cat > nodes.conf <<'EOF'
node-1 127.0.0.1:8000@127.0.0.1:18000 master - 1 0 0 connected 0-5461
node-2 127.0.0.1:8001@127.0.0.1:18001 master - 1 0 0 connected 5462-10922
node-3 127.0.0.1:8002@127.0.0.1:18002 master - 1 0 0 connected 10923-16383
EOF
for i in 1 2 3; do cp nodes.conf nodes-$i.conf; done
for i in 1 2 3; do
FYRODB_CLUSTER_ENABLED=true \
FYRODB_BIND=127.0.0.1 \
FYRODB_PORT=$((7999 + i)) \
FYRODB_CLUSTER_PORT=$((17999 + i)) \
FYRODB_NODE_ID=node-$i \
FYRODB_CLUSTER_CONFIG_FILE=nodes-$i.conf \
FYRODB_RDB_PATH=node-$i.rdb \
fyro_db &
doneEnvironment Variables
| Variable | Default | Description |
|---|---|---|
FYRODB_CLUSTER_ENABLED | false | true/1/yes turns on cluster mode |
FYRODB_NODE_ID | auto | Stable node identity; must match nodes.conf |
FYRODB_CLUSTER_PORT | 18000 | Cluster bus port (clients never use it) |
FYRODB_ADVERTISE_ADDR | FYRODB_BIND | Address used when there is no nodes.conf |
FYRODB_CLUSTER_CONFIG_FILE | fyrodb-nodes.conf | Topology file, Redis nodes.conf format |
FYRODB_CLUSTER_HEARTBEAT_MS | 2000 | Ping interval on the bus |
FYRODB_CLUSTER_SUSPECT_MS | 6000 | Silence before a peer is suspect; must exceed heartbeat |
FYRODB_CLUSTER_FAILURE_QUORUM | 2 | Reports needed to confirm a failure |
FYRODB_CLUSTER_QUEUE_CAPACITY | 1024 | Bounded outbound frames per peer |
FYRODB_CLUSTER_MAX_INBOUND | 1024 | Max inbound bus connections |
FYRODB_CLUSTER_AUTH | (none) | Shared secret for the bus handshake |
FYRODB_REPLICATION_LOG_CAPACITY | 100000 | Mutation log entries retained for replicas |
nodes.conf
One line per node, Redis-compatible:
<id> <client-addr>@<bus-addr> <flags> <master> <epoch> 0 0 connected <slots...>
node-1 127.0.0.1:8000@127.0.0.1:18000 master - 1 0 0 connected 0-5461
node-2 127.0.0.1:8001@127.0.0.1:18001 master - 1 0 0 connected 5462-10922
node-3 127.0.0.1:8002@127.0.0.1:18002 master - 1 0 0 connected 10923-16383
The two addresses on either side of @ live in different reachability
domains, and getting this wrong is the most common setup mistake:
- client-addr is returned in every
MOVED/ASKreply, so it must be reachable by your clients, on a port they can connect to. - bus-addr is only ever used node-to-node.
Under Docker Compose that means a client on the host needs
127.0.0.1:<published port> on the left, while the right side can stay on the
Compose network:
node-2 127.0.0.1:8001@node-2:18000 master - 1 0 0 connected 5462-10922
Point the client-facing address at a Compose service name instead if your client runs inside the network. Bus addresses may be hostnames — they are resolved on every connect attempt, so a node that restarts with a new IP recovers on its own.
id is opaque and does not have to look like an address. Node identity is
compared by id, never by address.
Verifying a Cluster
redis-cli -p 8000 cluster info
redis-cli -p 8000 cluster slots
redis-cli -p 8000 cluster nodes
redis-cli -p 8000 cluster keyslot user:1Check both of these — a cluster serves reads correctly with the bus completely dead, so functional tests alone will not catch a broken bus:
redis-cli -p 8000 cluster info | grep cluster_state # expect ok
redis-cli -p 8000 info | grep cluster_peer_healthy # expect peers-1INFO also reports cluster_peer_total, cluster_peer_suspect,
cluster_peer_queue_full_total, cluster_peer_reconnect_total,
cluster_replication_lag_total and cluster_snapshot_attempts.
Hash Tags and CROSSSLOT
Multi-key commands must stay within one slot. Wrap the part you want hashed in
{} to force keys together:
$ redis-cli -c -p 8000 mset '{cart:42}:items' 3 '{cart:42}:total' 99
OK
$ redis-cli -c -p 8000 mget '{cart:42}:items' '{cart:42}:total'
1) "3"
2) "99"
$ redis-cli -p 8000 mget a b
(error) CROSSSLOT Keys in request don't hash to the same slotSlot Migration
Migration follows Redis's handshake. SETSLOT <slot> NODE <target> has to be
sent to every master — only the source has a pending migration record, and a
node that never hears about the new owner will keep redirecting to the old one.
SLOT=$(redis-cli -p 8000 cluster keyslot user:1 | tr -dc 0-9)
redis-cli -p 8001 cluster setslot $SLOT importing node-1 # on the target
redis-cli -p 8000 cluster setslot $SLOT migrating node-2 # on the source
# writes to the slot now ASK-redirect to the target
redis-cli -p 8000 set user:1 v
# (error) ASK 8676 127.0.0.1:8001
for p in 8000 8001 8002; do
redis-cli -p $p cluster setslot $SLOT node node-2
done
redis-cli -p 8001 cluster setslot $SLOT stableThis moves ownership. Key data moves separately.
Cluster Commands
CLUSTER INFO, MYID, SLOTS, SHARDS, NODES, KEYSLOT,
COUNTKEYSINSLOT, GETKEYSINSLOT, MEET, FORGET, ADDSLOTS, DELSLOTS,
SETSLOT, REPLICATE, RESET, SAVECONFIG, plus ASKING.
nodes.conf is rewritten whenever the topology changes, so a restarted node
comes back with the slot map it last agreed on.
Client Examples
Node.js (ioredis)
import { Cluster } from "ioredis";
const cluster = new Cluster([
{ host: "127.0.0.1", port: 8000 },
{ host: "127.0.0.1", port: 8001 },
{ host: "127.0.0.1", port: 8002 },
]);
await cluster.set("user:1", "alice");
console.log(await cluster.get("user:1"));Python (redis-py)
from redis.cluster import RedisCluster
rc = RedisCluster(host="127.0.0.1", port=8000)
rc.set("user:1", "alice")
print(rc.get("user:1"))Go (go-redis)
rdb := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{"127.0.0.1:8000", "127.0.0.1:8001", "127.0.0.1:8002"},
})
rdb.Set(ctx, "user:1", "alice", 0)Benchmarking a Cluster
The benchmark tool lives in the repository, so point it at your running cluster from a clone:
git clone https://github.com/Rana718/FyroDB && cd FyroDB/bench
go run . -cluster 127.0.0.1:8000,127.0.0.1:8001,127.0.0.1:8002Flush between runs so the second run does not start with the first one's data:
for p in 8000 8001 8002; do redis-cli -p $p flushall; doneGive every node the same total core count as whatever you are comparing against. Redis is single-threaded per master, so its master count is its core count — a 6-master Redis Cluster on a 12-thread machine only gets half the box. See Benchmarks for the paired configurations and measured numbers.
Single Node or Cluster?
A single FyroDB node already uses every core, so clustering is about capacity and fault isolation rather than throughput on one box. Measured at matched thread count on a 6-core/12-thread machine, three nodes with four workers each land within a few percent of one node with twelve workers on most workloads, and ahead on single-key contention because sharding across processes reduces cross-core traffic. Reach for a cluster when the dataset outgrows one machine or you want independent failure domains.