Consistent Hashing
Consistent Hashing
Consistent hashing distributes keys across cache nodes while minimizing key redistribution when nodes are added or removed.
Problem with Simple Hashing
Simple Hashing (mod N):
node = hash(key) % num_nodes
Problem: Adding/removing a node rehashes ALL keys!
Before: node = hash(key) % 4 → node 2
After: node = hash(key) % 5 → node 4 (different!)
Result: Massive cache miss spike during scaling
Consistent Hashing Solution
Consistent Hash Ring:
Node A
|
----+----+----
/ | | \
/ | | \
Node D | | Node B
\\ | | /
\\ | | /
----+----+----
|
Node C
Keys are mapped to positions on the ring.
Keys go to the next node clockwise.
Adding Node E between B and C:
- Only keys between B and E move to E
- All other keys stay put!
Implementation
import hashlib
import bisect
class ConsistentHashRing:
def __init__(self, nodes=None, virtual_nodes=150):
self.ring = {}
self.sorted_keys = []
self.virtual_nodes = virtual_nodes
if nodes:
for node in nodes:
self.add_node(node)
def _hash(self, key):
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def add_node(self, node):
"""Add node with virtual nodes"""
for i in range(self.virtual_nodes):
virtual_key = f"{node}:v{i}"
hash_val = self._hash(virtual_key)
self.ring[hash_val] = node
bisect.insort(self.sorted_keys, hash_val)
def remove_node(self, node):
"""Remove node and its virtual nodes"""
for i in range(self.virtual_nodes):
virtual_key = f"{node}:v{i}"
hash_val = self._hash(virtual_key)
del self.ring[hash_val]
self.sorted_keys.remove(hash_val)
def get_node(self, key):
"""Get node for a key"""
if not self.ring:
return None
hash_val = self._hash(key)
idx = bisect.bisect_right(self.sorted_keys, hash_val)
if idx >= len(self.sorted_keys):
idx = 0
return self.ring[self.sorted_keys[idx]]
# Usage
ring = ConsistentHashRing(['cache-1', 'cache-2', 'cache-3'])
node = ring.get_node('user:123') # Returns cache-2
ring.add_node('cache-4') # Only ~25% of keys redistributed
Virtual Nodes
Without Virtual Nodes:
- Uneven distribution possible
- Hot spots on some nodes
With Virtual Nodes (150 per physical node):
- Better distribution
- ~equal load across nodes
- Trade-off: More memory for ring lookup
Cache Replication
Cache Replication
Replication copies cache data across multiple nodes for high availability and read scaling.
Replication Strategies
1. Master-Slave (Redis Default):
[Master] ──replicate──→ [Slave 1]
│ [Slave 2]
↓
Write Operations
(slaves are read-only)
2. Multi-Master:
[Master A] ←──sync──→ [Master B]
↓ ↓
[Slave A1] [Slave B1]
3. Client-Side Replication:
App writes to: Node A, Node B, Node C
App reads from: Any node
Redis Replication
# Slave configuration
# redis.conf
replicaof 192.168.1.100 6379
masterauth your_password
# Check replication status
INFO replication
# Manual failover
CLUSTER FAILOVER
Replication Monitoring
class ReplicationMonitor:
def __init__(self, redis_clients):
self.masters = redis_clients
def check_lag(self):
"""Check replication lag across all masters"""
lags = {}
for master_name, client in self.masters.items():
info = client.info('replication')
for slave in info.get('slaves', []):
lag = slave.get('lag', 0)
if lag > 10: # Alert threshold
alert(f"High replication lag: {master_name} slave lag={lag}s")
lags[f"{master_name}:{slave['ip']}"] = lag
return lags
# Monitor replication health
monitor = ReplicationMonitor(redis_clients)
lags = monitor.check_lag()
Trade-offs
| Strategy | Consistency | Availability | Performance |
|---|---|---|---|
| Async Replication | Eventual | High | Best |
| Sync Replication | Strong | Lower | Slower writes |
| Semi-Sync | bounded delay | High | Good |
Failure Handling
Cache Failure Handling
Failure Scenarios
1. Single Node Failure:
- Cache miss spike
- Need fallback mechanism
2. Network Partition:
- Split-brain risk
- Inconsistent data
3. Cluster Failure:
- Complete cache outage
- DB must handle all traffic
4. Cascading Failure:
- Cache failure → DB overload → System failure
Failure Handling Patterns
class ResilientCache:
def __init__(self, primary_cache, fallback_cache=None):
self.primary = primary_cache
self.fallback = fallback_cache
self.circuit_breaker = CircuitBreaker(
failure_threshold=5,
recovery_timeout=30
)
def get(self, key):
"""Get with fallback"""
try:
if self.circuit_breaker.is_open:
# Circuit open, use fallback
return self._get_fallback(key)
value = self.primary.get(key)
self.circuit_breaker.record_success()
return value
except Exception as e:
self.circuit_breaker.record_failure()
return self._get_fallback(key)
def _get_fallback(self, key):
"""Fallback to secondary cache or DB"""
if self.fallback:
return self.fallback.get(key)
return db.query(key) # Direct DB fallback
def set(self, key, value, ttl=3600):
"""Write to both caches"""
try:
self.primary.set(key, value, ttl)
except:
pass # Log but don't fail
if self.fallback:
try:
self.fallback.set(key, value, ttl)
except:
pass
Circuit Breaker Integration
class CircuitBreaker:
def __init__(self, failure_threshold=5, recovery_timeout=30):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.failure_count = 0
self.last_failure_time = None
self.state = 'closed' # closed, open, half-open
@property
def is_open(self):
if self.state == 'open':
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = 'half-open'
return False
return True
return False
def record_success(self):
self.failure_count = 0
self.state = 'closed'
def record_failure(self):
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = 'open'
Best Practices
- Monitor cache health (hit ratio, latency, errors)
- Implement circuit breakers to prevent cascading failures
- Use fallback caches or direct DB access
- Plan for cache warming after recovery
- Test failure scenarios regularly
Practice Problems
Design a scalable Distributed Cache system. Cover high-level architecture, data model, and API design.
Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliabilityHow would you scale Distributed Cache to handle 10x the current load? Identify bottlenecks and solutions.
Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decompositionAnalyze potential failure modes for Distributed Cache and design mitigation strategies.
Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradationQuiz
1. What problem does consistent hashing solve?
2. What are virtual nodes in consistent hashing?
3. What is the main benefit of cache replication?
4. What does a circuit breaker do when cache fails?
5. What happens during a cache node failure without replication?
Flashcards
Question
What is consistent hashing?
Click to reveal answer
Answer
A technique that distributes keys across nodes using a hash ring, minimizing redistribution when nodes are added/removed
Question
Why use virtual nodes?
Click to reveal answer
Answer
Virtual nodes create multiple positions per physical node on the hash ring, ensuring better load distribution
Question
Cache replication benefits?
Click to reveal answer
Answer
High availability (survive failures) and read scaling (distribute read load across replicas)
Question
What is cache failure cascading?
Click to reveal answer
Answer
Cache failure → DB overload → System failure. Prevented by circuit breakers and fallback mechanisms.
Question
What is circuit breaker in caching?
Click to reveal answer
Answer
Pattern that stops calling failing cache and routes to fallback, preventing cascading failures
Revision Notes
Key Takeaways
- 1.Consistent hashing minimizes key redistribution during scaling
- 2.Virtual nodes ensure even distribution across cache nodes
- 3.Replication provides high availability and read scaling
- 4.Circuit breakers prevent cascading failures
- 5.Always have fallback mechanisms for cache failures
Interview Tips
- •Draw the consistent hash ring and explain virtual nodes
- •Discuss replication lag monitoring and its impact
- •Explain circuit breaker pattern with state transitions
- •Mention cache warming after failure recovery
Cheat Sheet
Cheat Sheet: Distributed Cache
Consistent Hashing
- Hash ring for key distribution
- Only ~1/N keys remap on node change
- Use 150+ virtual nodes per physical node
Replication
- Master-Slave: Async, read scaling
- Multi-Master: Write scaling, complexity
- Semi-Sync: Bounded delay
Failure Handling
- Circuit Breaker: Stop calling failing cache
- Fallback: Secondary cache or direct DB
- Cache Warming: Rebuild after recovery
Monitoring
- Hit ratio, latency, errors
- Replication lag
- Circuit breaker state