Project 2: Log-Structured Key-Value Store (Mini LSM-Tree)
Build a persistent key-value store using append-only log segments and background compaction.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Expert |
| Time Estimate | 2-3 weeks |
| Language | C |
| Prerequisites | File I/O, hash tables, C memory management |
| Key Topics | append-only logs, SSTables, compaction |
1. Learning Objectives
By completing this project, you will:
- Implement append-only log segments with durable writes.
- Build an in-memory index mapping keys to file offsets.
- Generate sorted segment files (SSTable-style).
- Compact multiple segments into a single sorted run.
2. Theoretical Foundation
2.1 Core Concepts
- Log-structured storage: Sequential writes outperform random updates on disk.
- Indexing: A hash map in memory provides fast lookup to log offsets.
- Compaction: Merges multiple segments into a single sorted structure.
2.2 Why This Matters
This is how real storage engines like LevelDB and RocksDB achieve fast writes and acceptable reads.
2.3 Historical Context / Background
LSM-trees were designed to leverage sequential I/O and minimize random disk access.
2.4 Common Misconceptions
- “Append-only is inefficient”: It avoids random writes and improves throughput.
- “Compaction is optional”: Without it, reads degrade as segments pile up.
3. Project Specification
3.1 What You Will Build
A persistent key-value store that writes to log segments, keeps a memory index, and compacts segments to maintain read performance.
3.2 Functional Requirements
- Support
PUT key valueandGET key. - Append records to a log file with length-prefix or checksum.
- Build an in-memory index on startup by scanning the log.
- Perform compaction to produce a sorted segment.
3.3 Non-Functional Requirements
- Durability: Use fsync or explicit flush on write.
- Performance: Keep writes sequential.
- Reliability: Recover on restart by log replay.
3.4 Example Usage / Output
$ ./mini_lsm put user:1 Alice
OK
$ ./mini_lsm get user:1
Alice
3.5 Real World Outcome
You will insert records, restart the process, and still retrieve them from disk:
$ ./mini_lsm put user:1 Alice
OK
$ ./mini_lsm get user:1
Alice
4. Solution Architecture
4.1 High-Level Design
PUT -> append log -> update mem index
GET -> index lookup -> read offset
Compaction -> merge segments -> replace with SSTable
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| Log writer | Append records | Use length + checksum |
| Index | Map key -> (segment, offset) | Hash table |
| Segment manager | Rotate logs | Size-based rotation |
| Compactor | Merge sorted runs | Background or manual |
4.3 Data Structures
typedef struct {
uint32_t key_len;
uint32_t value_len;
// key bytes, value bytes follow
} record_hdr_t;
4.4 Algorithm Overview
Key Algorithm: Compaction Merge
- Read sorted entries from segments.
- Merge by key, keeping newest value.
- Write a new compacted segment.
Complexity Analysis:
- Time: O(n) for merge
- Space: O(1) streaming merge
5. Implementation Guide
5.1 Development Environment Setup
gcc --version
5.2 Project Structure
project-root/
├── src/
│ ├── log.c
│ ├── index.c
│ ├── compact.c
│ └── main.c
└── README.md
5.3 The Core Question You’re Answering
“Why do storage engines favor append-only logs with compaction instead of in-place updates?”
5.4 Concepts You Must Understand First
Stop and research these before coding:
- Sequential vs random I/O
- Write-ahead logging concepts
- Merge algorithms for sorted runs
5.5 Questions to Guide Your Design
Before implementing, think through these:
- How will you detect partial writes on crash?
- How do you resolve duplicate keys across segments?
- When do you trigger compaction?
5.6 Thinking Exercise
Simulate log replay
Write a tiny log file and re-scan it to rebuild the index. Verify the newest value wins.
5.7 The Interview Questions They’ll Ask
Prepare to answer these:
- “Why do LSM trees prefer sequential writes?”
- “How does compaction work?”
- “What are the read tradeoffs of LSM vs B-tree?”
5.8 Hints in Layers
Hint 1: Use fixed headers Store lengths so you can skip records quickly.
Hint 2: Store offsets Index value should include segment id and offset.
Hint 3: Start with manual compaction Provide a command to run it before automating.
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| LSM design | “Designing Data-Intensive Applications” | Ch. 3 |
| Merge algorithms | “Algorithms” | Ch. 2.2 |
| Sequential I/O | “CS:APP” | Ch. 6 |
5.10 Implementation Phases
Phase 1: Foundation (4-5 days)
Goals:
- Append-only log with replay.
Tasks:
- Write and read records.
- Rebuild index on startup.
Checkpoint: Data survives restart.
Phase 2: Core Functionality (1 week)
Goals:
- Multiple segments and rotation.
Tasks:
- Rotate log after size threshold.
- Track segment ids.
Checkpoint: GET finds values across segments.
Phase 3: Polish & Edge Cases (4-5 days)
Goals:
- Compaction and cleanup.
Tasks:
- Merge segments into SSTable.
- Delete old segments after success.
Checkpoint: Reads still work after compaction.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| Index type | hash vs tree | hash | Fast lookup |
| Compaction | manual vs background | manual first | Easier to verify |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Persistence | Restart test | PUT/GET after restart |
| Compaction | Merge correctness | Duplicate keys |
| Recovery | Partial write | Truncated log |
6.2 Critical Test Cases
- Latest value wins after multiple PUTs.
- Compaction removes stale entries.
- Crash during write does not corrupt earlier data.
6.3 Test Data
Keys: a, b, c with updates
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|---|---|---|
| No length prefix | Corrupt parsing | Store sizes in header |
| No fsync | Data loss | Flush on commit |
| Compaction bug | Missing keys | Keep newest by segment id |
7.2 Debugging Strategies
- Add a log dump tool to print records.
- Validate index entries by reading offsets.
7.3 Performance Traps
Replaying the full log on every start can be slow; add checkpoints later.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add DELETE tombstones.
- Add a stats command.
8.2 Intermediate Extensions
- Implement a simple bloom filter.
- Background compaction thread.
8.3 Advanced Extensions
- Add WAL checksum validation.
- Build a multi-level LSM tree.
9. Real-World Connections
9.1 Industry Applications
- Storage engines in databases and log platforms.
9.2 Related Open Source Projects
- LevelDB: https://github.com/google/leveldb
- RocksDB: https://rocksdb.org
9.3 Interview Relevance
- LSM tree and compaction knowledge is highly valued.
10. Resources
10.1 Essential Reading
- Designing Data-Intensive Applications - storage engines chapter
- LSM-tree papers (search “LSM tree”)
10.2 Video Resources
- LSM tree talks (search “LSM tree compaction”)
10.3 Tools & Documentation
- fsync(2) -
man 2 fsync
10.4 Related Projects in This Series
- B-Tree File Index: compare LSM vs B-tree.
11. Self-Assessment Checklist
11.1 Understanding
- I can explain log-structured storage.
- I can describe compaction.
- I can explain LSM read/write tradeoffs.
11.2 Implementation
- Data persists across restarts.
- Compaction works correctly.
- Index lookup is correct.
11.3 Growth
- I can explain how to improve read performance.
- I can extend with bloom filters.
12. Submission / Completion Criteria
Minimum Viable Completion:
- Append-only log with replayable index.
Full Completion:
- Multi-segment support and compaction.
Excellence (Going Above & Beyond):
- Multi-level compaction and bloom filters.
This guide was generated from SPRINT_4_5_REAL_WORLD_PROJECTS.md. For the complete learning path, see the parent directory.