Architecture
FyroDB's concurrent data path, compact storage, and adaptive memory maintenance.
Overview
FyroDB combines five key architectural decisions:
- Thread-per-core I/O — one epoll loop per CPU core via
mio, withSO_REUSEPORTfor kernel-level connection distribution - Lock-free hash map — custom sharded open-addressing map with atomic pointer swaps and epoch-based reclamation
- Zero-copy data path — RESP parsing directly from read buffer, GET writes stored value straight to TCP buffer
- Batched writes — all epoll events processed before flushing responses, reducing syscall count
- Compact, adaptive storage — inline keys and values, lazy shard growth, compact collection encodings, and background memory reclamation
Data Flow
Client → TCP (SO_REUSEPORT) → Worker Thread (epoll)
→ read(socket) → RESP Parser (zero-copy, SIMD memchr)
→ Command Dispatch (3-tier: inline → first-byte → enum)
→ Store Operation (lock-free lookup, per-entry writer lock, EBR pin/unpin)
→ Response Build (inline bulk headers, batched)
→ write(socket) → Client
Lock-Free Hash Map
Built from scratch using atomic pointers, CAS loops, and epoch-based reclamation. Full design documented at: Building a Lock-Free Concurrent HashMap in Rust
Key properties:
- Lookups are lock-free atomic probes
- Writers serialize only on the matching entry; unrelated keys do not contend
- Lock, occupied state, and seqlock generation share one atomic state word
- Keys up to 15 bytes are stored inline; longer keys use one boxed string
- Shards begin with eight slots and grow lazily to the configured key limit
- Retired entries and tables are freed through EBR after a grace period
Compact Values and Collections
- Strings and JSON payloads use
SmallStr, keeping values up to 23 bytes inline - Hashes and lists use compact vector/deque forms for small collections
- Sets use sorted integers for numeric members, a compact vector for small sets, and promote to a hash set only when needed
- Compact collections promote after 64 elements and can demote again after shrinking
- Sorted sets use one score-ordered vector instead of duplicate ordered and lookup indexes
- Removal paths shrink oversized buffers, while background defragmentation compacts long-lived values
Memory Allocation and Reclamation
FyroDB uses the internal rust-zmalloc layer backed by mimalloc. It tracks live allocated bytes for INFO, exposes RSS and fragmentation metrics, and can force unused pages back to the operating system.
Memory maintenance runs outside command hot paths:
- EBR garbage collection and mimalloc collection every 10 seconds
- Fragmentation checks and bounded value defragmentation every 60 seconds
- Underutilized shard-table compaction every 120 seconds
FLUSHALL/FLUSHDBperform quiescent EBR collection before allocator purge
Pub/Sub
- Arc snapshot — publish reads an Arc-cloned subscriber list (zero locks)
- First-byte pattern index — patterns bucketed by first character, PUBLISH only checks relevant bucket
- Lock-free delivery — SegQueue per subscriber, coalesced wake notifications
Concurrency Model
main thread
├── N worker threads (epoll, one per core)
├── expiry/maintenance thread (incremental TTL scan every second)
├── RDB saver thread (per-slot iteration every 5min)
└── signal thread (SIGTERM → drain → save → exit)
No global lock exists on a command hot path. Workers contend only on the atomic state of the individual entry they update; table growth and compaction are isolated per shard.