Skip to content

h30s/RaftKV

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

RaftKV

Built a distributed key-value store from scratch to actually understand how Raft works. It's a 3-node cluster running in Docker, talking over gRPC.

The Setup

Clients use consistent hashing locally to pick a node. If they accidentally hit a follower, the follower just hands back a redirect to the current leader.

Client → consistent-hash ring → pick node → gRPC (Get/Put/Delete)
                                     │
                              ┌──────┴──────┐
                              │   KV Server │
                              └──────┬──────┘
                                     │
                              ┌──────┴──────┐
                              │  Raft Node  │ ◄──── AppendEntries / RequestVote
                              └──────┬──────┘
                                     │ ApplyChannel
                              ┌──────┴──────┐
                              │   KV Store  │
                              └─────────────┘

Running it

You just need Docker, Compose, and Go 1.22+.

1. Boot the cluster

docker-compose up --build

Nodes find each other via Docker DNS. Leader election happens in about 300ms.

2. Play with the CLI

go build -o client ./cmd/client
./client put mykey myvalue
./client get mykey
./client delete mykey

Breaking it (Failover)

The best way to see Raft working is to break things.

# Find out who the leader is
docker-compose logs | grep "became leader"

# Kill that container (say it's node1)
docker kill raftkv-node1-1

# Watch the other two panic for a split second, then elect a new leader
docker-compose logs -f node2 node3

# Writes still work perfectly
./client put testkey survived_the_crash

# Turn the old leader back on. It wakes up as a follower and syncs the missing logs.
docker start raftkv-node1-1

Tests & Benchmarks

# Fast in-memory unit tests (no Docker needed)
go test ./test/... -v -timeout 30s

# Hardcore fault injection (needs the Docker cluster running)
go test ./test/... -run TestFault -v -timeout 120s

# Benchmarks
go test ./bench/... -bench=. -benchtime=30s -v

On my 4-core laptop, hitting the 3-node cluster with 8 parallel writers gets me about 6,400 ops/sec. Failover takes around 240ms.

Under the Hood

I tried to stick to the Raft paper as much as possible, but took a few shortcuts to keep the code readable.

What I actually implemented:

  • Standard leader election with randomized timeouts (150-300ms).
  • Strict log matching (prevLogIndex/prevLogTerm) and truncation.
  • New-leader no-op entries.
  • ReadIndex for linearizable reads without hitting the disk every time.
  • Snapshots, log compaction, and accelerated backtracking for conflicts.

Where I cheated:

  • I share one gRPC port for both client traffic and internal Raft chatter. It just makes Docker networking way easier for a demo.
  • Logs are JSON and I only fsync on snapshots. Fsyncing every single append is mathematically correct but brutally slow for a side project.
  • Static membership. No joint consensus or adding/removing nodes on the fly. That's a massive headache for another day.

About

A distributed key-value store built on a from-scratch Raft consensus implementation. Three nodes, one Raft group, gRPC API, Docker cluster.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages