Redis Cluster on Kubernetes
Redis Modes
graph TD
STANDALONE["Standalone<br/>Single instance<br/>No HA<br/>Use: dev, small cache"] --> SENTINEL
SENTINEL["Sentinel<br/>1 master + N replicas<br/>Sentinel monitors + promotes<br/>Use: HA without sharding"] --> CLUSTER
CLUSTER["Redis Cluster<br/>N masters, each with replicas<br/>Data sharded across masters<br/>Use: horizontal scale + HA"]
Redis Cluster Architecture (6-node minimum)
graph TD
subgraph "Redis Cluster on K8s"
M0["redis-0 (master)<br/>slots: 0-5460"] --> R0["redis-3 (replica of redis-0)"]
M1["redis-1 (master)<br/>slots: 5461-10922"] --> R1["redis-4 (replica of redis-1)"]
M2["redis-2 (master)<br/>slots: 10923-16383"] --> R2["redis-5 (replica of redis-2)"]
end
CLIENT["Client<br/>(redis-py cluster / jedis cluster)"] --> M0 & M1 & M2
16384 hash slots are distributed across masters. Every key is hashed (CRC16(key) % 16384) to find which slot (and thus which master) owns it.
Key Distribution (Hash Slots)
graph LR
KEY["SET user:123 alice"] --> HASH["CRC16('user:123') % 16384 = 8547"]
HASH --> SLOT["Slot 8547 is on redis-1<br/>(slots 5461-10922)"]
SLOT --> M1_2["Request routed to redis-1"]
Hash tags {tag}key force keys to the same slot (for multi-key operations):
SET {user:123}.name alice -- slot = CRC16('user:123') % 16384
SET {user:123}.email a@b.c -- same slot
MGET {user:123}.name {user:123}.email -- works (same slot)
Failover
sequenceDiagram
participant M1_3 as redis-1 (master)
participant R1_3 as redis-4 (replica)
participant M0_2 as redis-0 (other master)
participant M2_2 as redis-2 (other master)
participant CLIENT2 as Client
Note over M1_3: redis-1 crashes
M0_2->>M1_3: PING (cluster gossip)
M2_2->>M1_3: PING (cluster gossip)
M0_2--xM1_3: No response (PFAIL after cluster-node-timeout=15s)
M2_2--xM1_3: No response
Note over M0_2,M2_2: Cluster marks redis-1 as FAIL (majority agree)
R1_3->>M0_2: FAILOVER request
M0_2->>R1_3: ACK — vote for redis-4 as new master
M2_2->>R1_3: ACK — vote for redis-4 as new master
R1_3->>R1_3: Promoted to master for slots 5461-10922
Note over CLIENT2: Client gets MOVED/ASK redirect to redis-4
CLIENT2->>R1_3: Requests for slots 5461-10922 now go here
Failover time: cluster-node-timeout (default 15s) + election + promotion ≈ 15-30s.
Persistence: RDB vs AOF
graph LR
subgraph RDB["RDB (Redis Database Snapshot)"]
R1_P["Full snapshot every N seconds<br/>save 900 1<br/>save 300 10<br/>save 60 10000<br/>Fast restore, possible data loss<br/>up to last snapshot"]
end
subgraph AOF["AOF (Append Only File)"]
A1_P["Every write operation logged<br/>appendfsync always: flush per write<br/>appendfsync everysec: flush per second<br/>Slower, near-zero data loss"]
end
subgraph BOTH["RDB + AOF (recommended for production)"]
B1_P["AOF for durability<br/>RDB for fast restart<br/>Redis loads RDB first (faster),<br/>then replays AOF for recent data"]
end
# redis.conf
save 3600 1 # snapshot if 1 key changed in 1h
save 300 100 # snapshot if 100 keys changed in 5m
save 60 10000 # snapshot if 10000 keys changed in 1m
appendonly yes
appendfsync everysec # fsync every second (good balance)
no-appendfsync-on-rewrite yes # don't fsync during compaction
Redis Cluster on K8s (StatefulSet)
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
replicas: 6 # 3 masters + 3 replicas
serviceName: redis-headless
template:
spec:
containers:
- name: redis
image: redis:7.2-alpine
command:
- redis-server
- /etc/redis/redis.conf
- --cluster-enabled yes
- --cluster-config-file /data/nodes.conf
- --cluster-node-timeout 15000
- --appendonly yes
- --save 3600 1
resources:
limits:
memory: 8Gi
requests:
memory: 4Gi
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: premium-rwo
resources:
requests:
storage: 50Gi
# Initialize the cluster (run once after pods start)
redis-cli --cluster create \
redis-0.redis-headless:6379 \
redis-1.redis-headless:6379 \
redis-2.redis-headless:6379 \
redis-3.redis-headless:6379 \
redis-4.redis-headless:6379 \
redis-5.redis-headless:6379 \
--cluster-replicas 1 # 1 replica per master
# Check cluster state
redis-cli -c cluster info
redis-cli -c cluster nodes
Backups
# Trigger RDB snapshot on primary
redis-cli BGSAVE
# Wait for completion
redis-cli LASTSAVE # returns UNIX timestamp of last successful save
# Copy RDB to GCS
gsutil cp /data/dump.rdb gs://my-redis-backups/$(date +%Y%m%d)/dump.rdb
# Volume snapshot (quicker, consistent)
kubectl apply -f - << 'EOF'
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: redis-snapshot-$(date +%Y%m%d)
spec:
source:
persistentVolumeClaimName: data-redis-0
EOF
Monitoring
# Memory usage (alert > 80% of maxmemory)
redis_memory_used_bytes / redis_memory_max_bytes > 0.8
# Cache hit rate (alert < 90%)
rate(redis_keyspace_hits_total[5m]) /
(rate(redis_keyspace_hits_total[5m]) + rate(redis_keyspace_misses_total[5m])) < 0.9
# Cluster state (1 = ok)
redis_cluster_state != 1
# Replica count (this counts replicas, NOT lag)
redis_connected_slaves < 1 # alert if no replicas
# For true lag use: master_repl_offset − slave's slave_repl_offset (bytes behind)