6.5 KiB
Multi-Node Predastore Configuration
Overview
Predastore uses Reed-Solomon erasure coding RS(2,1) -- 2 data shards and 1 parity shard -- requiring a minimum of 3 distinct nodes. Each shard is placed on a separate node via a consistent hash ring (GetClosestN(hash, 3)), which means every node must know the full cluster topology at startup to make identical placement decisions.
This is fundamentally different from NATS, which uses gossip-based discovery and can add members dynamically. Predastore's hash ring is static -- if nodes disagree about membership, they'll compute different shard placements and data becomes unreachable.
How It Works
Init (leader node)
hive admin init --predastore-nodes "10.11.12.1,10.11.12.2,10.11.12.3" --bind 10.11.12.1 ...
- The comma-separated IPs are parsed and validated. Each gets a sequential node ID (1, 2, 3).
- The leader's
--bindIP is matched against the list to determine its own node ID. If the bind IP isn't in the list, init fails. GenerateMultiNodePredastoreConfigrenders a singlepredastore.tomlcontaining the full cluster topology -- all DB entries (port 6660), all shard entries (port 9991), RS config, auth, and buckets. Node ID 1 is marked as the Raft bootstrap leader.- The predastore.toml is written to
config/predastore/predastore.toml. - The leader's node ID is written into
hive.tomlasnode_id = 1under the[predastore]section.
Join (follower nodes)
hive admin join --leader https://10.11.12.1:7777 --bind 10.11.12.2 ...
- The follower contacts the leader's
/joinHTTP endpoint. - The leader reads its
predastore.tomlfrom disk and includes the full content in the join response (PredastoreConfigfield). - The follower writes this verbatim to its own
config/predastore/predastore.toml. Now all nodes have an identical copy. ParsePredastoreNodeIDFromConfigunmarshals the TOML, extracts the[[db]]array, and matches the follower's--bindIP to find its node ID. If the IP isn't found, join fails with an error.- The detected node ID is written into the follower's
hive.toml(e.g.,node_id = 2).
Startup
When start-dev.sh runs, it reads node_id from hive.toml using awk and exports it as HIVE_PREDASTORE_NODE_ID. Predastore reads this environment variable to know which DB and shard entries in the shared config belong to it.
Key Design Decisions
Shared config, per-node identity
Every node gets the same predastore.toml (the full cluster topology). The only difference per node is node_id in hive.toml. This separation exists because:
- Predastore needs all members known at startup for consistent hash ring agreement.
- The
predastore.tomldescribes the cluster. Thenode_idinhive.tomlsays "I am this member." - This makes config distribution simple: the leader just sends one file, and the follower figures out its own identity by matching its bind IP.
Why IPs must be specified at init time
Unlike NATS (gossip discovery), Predastore requires static membership. The hash ring maps GetClosestN(hash, 3) to determine which 3 nodes store each object's shards. If a node joins without being in the ring, existing nodes won't route data to it. If a node is in the ring but missing, data placed on it becomes unavailable.
This means all Predastore node IPs must be known when the leader is initialized. The --predastore-nodes flag captures this.
Node ID 1 is the Raft bootstrap leader
The generated config marks leader = true on the [[db]] entry with id = 1. This is the node that bootstraps the Raft cluster. It's always the first IP in the --predastore-nodes list, which is also the --bind IP of the init command (the leader node).
Config distribution via join protocol
Rather than requiring operators to manually copy predastore.toml to each node, the existing HTTP join protocol distributes it. The leader's daemon reads its predastore.toml and includes it in the join response. This is the same pattern used for CA certificates.
The leader currently sends its predastore.toml unconditionally (even in single-node setups). The follower treats any non-empty PredastoreConfig in the response as "leader provided a multi-node config" and skips generating its own template-based config.
Node ID detection uses proper TOML parsing
The follower determines its node ID by parsing the received predastore.toml with go-toml/v2 and calling FindNodeIDByIP against the [[db]] entries. If parsing fails or the bind IP isn't found, join exits with an error. This mirrors the init path, which also exits on failure.
Backward Compatibility
When --predastore-nodes is omitted from hive admin init:
- No multi-node predastore.toml is generated.
PredastoreNodeIDstays at 0, sonode_idis not emitted in hive.toml.- The default single-node predastore.toml template is used instead.
start-dev.shfinds nonode_idin hive.toml and leavesHIVE_PREDASTORE_NODE_IDempty.- Predastore runs in its default single-node mode.
Existing single-node deployments are unaffected.
Files Involved
| File | Role |
|---|---|
cmd/hive/cmd/admin.go |
CLI flag parsing, init/join orchestration |
hive/admin/admin.go |
GenerateMultiNodePredastoreConfig, FindNodeIDByIP, ParsePredastoreNodeIDFromConfig, BaseConfigFiles |
hive/config/config.go |
PredastoreConfig.NodeID field, NodeJoinResponse.PredastoreConfig field |
hive/daemon/daemon.go |
/join endpoint reads and sends predastore.toml |
cmd/hive/cmd/templates/hive.toml |
Conditional node_id rendering |
scripts/start-dev.sh |
Auto-detects NODE_ID from hive.toml at startup |
Generated Config Example
For --predastore-nodes "10.11.12.1,10.11.12.2,10.11.12.3":
version = "1.0"
region = "ap-southeast-2"
host = "0.0.0.0"
port = 8443
[rs]
data = 2
parity = 1
[[db]]
id = 1
host = "10.11.12.1"
port = 6660
path = "distributed/db/node-1/"
leader = true
[[db]]
id = 2
host = "10.11.12.2"
port = 6660
path = "distributed/db/node-2/"
[[db]]
id = 3
host = "10.11.12.3"
port = 6660
path = "distributed/db/node-3/"
[[nodes]]
id = 1
host = "10.11.12.1"
port = 9991
path = "distributed/nodes/node-1/"
[[nodes]]
id = 2
host = "10.11.12.2"
port = 9991
path = "distributed/nodes/node-2/"
[[nodes]]
id = 3
host = "10.11.12.3"
port = 9991
path = "distributed/nodes/node-3/"
[[buckets]]
name = "predastore"
region = "ap-southeast-2"
type = "distributed"
[[auth]]
access_key_id = "..."
secret_access_key = "..."
Each node receives this identical file. Node 1's hive.toml has node_id = 1, node 2 has node_id = 2, etc.