Redis Memory Fragmentácia: Keď maxmemory Nestačí
Redis hovoril 4GB, kernel 6GB. Kernel vyhral. maxmemory je limit na data, nie na realny RSS.
Ta medzera je fragmentacia a v praxi je to jedna z najviac nepochopenych veci v Redise. maxmemory kontroluje, kolko dat Redis drzi. RSS je realna pamat procesu. Pri nerovnomernych alokaciach ostanu v jemalloc diery, ktore RSS pocita, aj ked Redis tvrdi, ze je pod limitom.
Testované na: Redis 7.2, jemalloc 5.3, Kubernetes s 8GB memory limitom
Skrytá Pamäť
Čo Redis Hlási vs Realita
# Redis INFO memory výstup
redis-cli INFO memory
used_memory:4294967296 # 4GB - čo Redis sleduje
used_memory_rss:6442450944 # 6GB - čo vidí OS!
mem_fragmentation_ratio:1.50 # 50% overhead
# Táto medzera = fragmentácia
# OOM killer vidí RSS, nie used_memory
Prečo Fragmentácia Vzniká
Vzor alokácie pamäte záleží:
Scenár: Kľúče s variabilnou veľkosťou a TTL
1. Alokuj 1KB kľúč (jemalloc vyberie 1KB slab)
2. Alokuj 500B kľúč (jemalloc vyberie 512B slab)
3. Kľúč 1 expiruje, 1KB slab má teraz dieru
4. Nový 600B kľúč sa nezmestí do 512B slab, potrebuje novú alokáciu
5. 1KB slab stále rezervovaný ale čiastočne prázdny
Časom:
┌─────────────────────────────────────────┐
│ [used][ diera ][used][ diera ] │ ← jemalloc aréna
│ [used][used][ diera ] │
│ [ diera ][used][used][ diera ] │
└─────────────────────────────────────────┘
RSS = všetky alokované slaby
used_memory = skutočné dáta
Fragmentácia = RSS / used_memory
Reprodukcia Problému
Test Skript
# fragment_redis.py
import redis
import random
import string
import time
r = redis.Redis(host='localhost', port=6379)
def random_value(min_size, max_size):
size = random.randint(min_size, max_size)
return ''.join(random.choices(string.ascii_letters, k=size))
# Fáza 1: Vytvor kľúče s variabilnou veľkosťou a TTL
print("Fáza 1: Vytváram 1M kľúčov s variabilnou veľkosťou...")
for i in range(1_000_000):
key = f"key:{i}"
value = random_value(100, 10000) # 100B až 10KB
ttl = random.randint(60, 300) # 1-5 min TTL
r.setex(key, ttl, value)
if i % 100000 == 0:
info = r.info('memory')
ratio = info['mem_fragmentation_ratio']
print(f"Kľúče: {i}, Fragmentácia: {ratio:.2f}")
# Fáza 2: Počkaj na TTL a sleduj fragmentáciu
print("\nFáza 2: Čakám na expiráciu...")
for _ in range(10):
time.sleep(60)
info = r.info('memory')
print(f"used_memory: {info['used_memory_human']}, "
f"RSS: {info['used_memory_rss_human']}, "
f"fragmentácia: {info['mem_fragmentation_ratio']:.2f}")
Výsledky
Fáza 1: Vytváram 1M kľúčov s variabilnou veľkosťou...
Kľúče: 100000, Fragmentácia: 1.05
Kľúče: 500000, Fragmentácia: 1.12
Kľúče: 1000000, Fragmentácia: 1.18
Fáza 2: Čakám na expiráciu...
Minúta 1: used_memory: 850MB, RSS: 1.2GB, fragmentácia: 1.41
Minúta 3: used_memory: 620MB, RSS: 1.1GB, fragmentácia: 1.77
Minúta 5: used_memory: 380MB, RSS: 980MB, fragmentácia: 2.58 # Kritické!
# Dáta sa zmenšili ale RSS sa takmer nepohlo
# jemalloc drží fragmentovanú pamäť
Riešenia
1. Aktívna Defragmentácia (Redis 4.0+)
# redis.conf
activedefrag yes
# Spusti defrag keď fragmentácia > 10%
active-defrag-ignore-bytes 100mb
active-defrag-threshold-lower 10
# Zastav defrag keď fragmentácia < 5%
active-defrag-threshold-upper 100
# CPU úsilie (1-100)
active-defrag-cycle-min 1 # Min CPU% pri defragmentácii
active-defrag-cycle-max 25 # Max CPU% pri defragmentácii
# Scan limity per cyklus
active-defrag-max-scan-fields 1000
Ako Aktívny Defrag Funguje
Pred defragom:
┌─────────────────────────────────────────┐
│ [A][ diera ][B][diera][C][ diera ] │ Aréna 1
│ [D][diera][E][ diera ][F] │ Aréna 2
└─────────────────────────────────────────┘
Defrag proces:
1. Skenuj fragmentované hodnoty
2. Alokuj novú pamäť pre hodnotu
3. Kopíruj dáta na nové miesto
4. Aktualizuj pointer atomicky
5. Uvoľni starú pamäť
Po defrage:
┌─────────────────────────────────────────┐
│ [A][B][C][D][E][F] │ Aréna 1 (kompaktná)
│ (vrátená OS) │ Aréna 2 (uvoľnená)
└─────────────────────────────────────────┘
2. Tuning Memory Allocatora
# jemalloc background thread pre vrátenie pamäte
# Nastav v redis.conf alebo environment
# Možnosť 1: Povoľ jemalloc background thready
redis-server --jemalloc-bg-thread yes
# Možnosť 2: Vynúť vrátenie pamäte OS
# MEMORY PURGE príkaz (Redis 4.0+)
redis-cli MEMORY PURGE
# Možnosť 3: Tuning jemalloc decay času
# Nižší = rýchlejšie vrátenie pamäte, vyššie CPU
export MALLOC_CONF="background_thread:true,dirty_decay_ms:1000,muzzy_decay_ms:1000"
3. Uniformné Veľkosti Hodnôt
# Zlé: Variabilné veľkosti spôsobujú fragmentáciu
r.set("user:1", json.dumps(small_user)) # 200B
r.set("user:2", json.dumps(large_user)) # 50KB
# Lepšie: Zaokrúhli na mocniny 2
def pad_value(value, target_size=None):
data = json.dumps(value)
if target_size is None:
# Zaokrúhli nahor na najbližšiu mocninu 2
size = len(data)
target_size = 1 << (size - 1).bit_length()
return data.ljust(target_size, '\0')
# Alebo použi separátne Redis inštancie pre rôzne triedy veľkostí
# small_redis: hodnoty < 1KB
# large_redis: hodnoty > 1KB
4. Kubernetes Konfigurácia Pamäte
# Nenastavuj memory limit = maxmemory!
apiVersion: v1
kind: Pod
spec:
containers:
- name: redis
resources:
requests:
memory: "4Gi"
limits:
memory: "6Gi" # 50% rezerva pre fragmentáciu!
env:
- name: REDIS_MAXMEMORY
value: "4gb"
---
# Redis config
apiVersion: v1
kind: ConfigMap
data:
redis.conf: |
maxmemory 4gb
maxmemory-policy allkeys-lru
activedefrag yes
active-defrag-threshold-lower 10
active-defrag-cycle-max 25
Monitoring
Prometheus Metriky
# Redis exporter metriky
- alert: RedisVysokaFragmentacia
expr: |
redis_memory_fragmentation_ratio > 1.5
for: 30m
labels:
severity: warning
annotations:
summary: "Redis fragmentácia ratio {{ $value }}"
description: "Zváž povolenie activedefrag"
- alert: RedisFragmentaciaKriticka
expr: |
redis_memory_fragmentation_ratio > 2.0
for: 10m
labels:
severity: critical
annotations:
summary: "Redis fragmentácia kritická: {{ $value }}"
description: "OOM riziko - RSS oveľa vyššie ako used_memory"
- alert: RedisRSSBlizkoPriLimite
expr: |
redis_memory_used_rss_bytes / on(instance)
(container_spec_memory_limit_bytes) > 0.85
for: 5m
labels:
severity: critical
annotations:
summary: "Redis RSS na {{ $value | humanizePercentage }} limitu"
Grafana Dashboard Queries
# Fragmentácia ratio v čase
redis_memory_fragmentation_ratio
# Memory breakdown
redis_memory_used_bytes
redis_memory_used_rss_bytes
redis_memory_used_peak_bytes
# Active defrag štatistiky
redis_active_defrag_running
redis_active_defrag_hits
redis_active_defrag_misses
redis_active_defrag_key_hits
Debugging Príkazy
# Skontroluj aktuálnu fragmentáciu
redis-cli INFO memory | grep -E "(used_memory|fragmentation)"
# Memory doctor (Redis 4.0+)
redis-cli MEMORY DOCTOR
# Príklad výstupu:
# "Sam, I have a few reports for you:
# * Peak memory: 6.2GB, RSS: 8.1GB, Fragmentation: 1.31
# * High fragmentation: Consider enabling activedefrag"
# Skontroluj allocator štatistiky
redis-cli MEMORY MALLOC-SIZE 1024
redis-cli MEMORY STATS
# Vynúť defrag check
redis-cli DEBUG QUICKLIST-PACKED-THRESHOLD 0
# Vynúť vrátenie pamäte (opatrne v produkcii)
redis-cli MEMORY PURGE
Stratégie Prevencie
Dizajn Kľúčov
# Vyhni sa miešaniu malých a obrovských hodnôt
# Zlé
r.set("config", "true") # 4 bajty
r.set("user:session:123", huge_json) # 100KB
# Lepšie: Použi vhodné dátové štruktúry
r.hset("config", "feature_x", "true") # Hash pre malé hodnoty
r.set("session:123", compressed_data) # Komprimuj veľké hodnoty
# Použi konzistentné TTL per typ kľúča
SESSION_TTL = 3600 # 1 hodina pre všetky sessions
CACHE_TTL = 300 # 5 min pre všetky cache záznamy
Architektonické Vzory
Vzor: Sharding podľa veľkosti
┌─────────────────────────────────────────┐
│ Aplikácia │
└───────────────┬─────────────────────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐
│ Small │ │ Medium│ │ Large │
│ <1KB │ │ 1-10KB│ │ >10KB │
│ Redis │ │ Redis │ │ Redis │
└───────┘ └───────┘ └───────┘
Každá inštancia má uniformné veľkosti hodnôt = minimálna fragmentácia
Checklist
## Redis Memory Fragmentácia Prevencia
### Konfigurácia
- [ ] Povoľ activedefrag v redis.conf
- [ ] Nastav threshold-lower na 10%
- [ ] Nastav cycle-max na 25% (uprav podľa CPU budgetu)
- [ ] Konfiguruj maxmemory s 30-50% rezervou k container limitu
### Monitoring
- [ ] Alert na fragmentation_ratio > 1.5
- [ ] Alert na RSS blížiaci sa k container limitu
- [ ] Dashboard ukazujúci used_memory vs RSS
### Dizajn Kľúčov
- [ ] Použi konzistentné veľkosti hodnôt kde možné
- [ ] Komprimuj veľké hodnoty pred uložením
- [ ] Použi vhodné dátové štruktúry (hashe pre malé hodnoty)
### Operácie
- [ ] Naplánuj MEMORY PURGE počas nízkej záťaže (ak nepoužívaš activedefrag)
- [ ] Monitoruj activedefrag hits/misses
- [ ] Zváž sharding podľa veľkosti pre extrémne prípady
Záver
Redis maxmemory nie je tvoj memory limit:
- Fragmentácia ratio ukazuje skutočný memory overhead
- Variabilné kľúče s TTL spôsobujú najhoršiu fragmentáciu
- Aktívna defragmentácia kompaktuje pamäť automaticky
- Nastav container limity 30-50% nad maxmemory pre bezpečnosť
Monitoruj mem_fragmentation_ratio - tvoj ďalší OOM sa tam možno skrýva.
Súvisiace články
- Connection Pool Sizing s Little’s Law - Resource sizing
- Redlock vs Postgres Advisory Locks - Redis vzory
Súvisiace články
PostgreSQL OOM by Design: work_mem × Parallel Workers × Plan Nodes
work_mem vyzerá malé na 256MB, ale parallel hash join so 4 workers naprieč 3 plan nodes používa 3GB. Tu je ako zabrániť PostgreSQL legitímne OOMnúť váš kontajner.
Redis Cluster Migrácia Slotov: Dočasná Explózia Pamäte
Redis nody OOMKilled počas rebalancingu clustra. Príčina: migrácia slotov kopíruje kľúče do cieľa pred zmazaním zo zdroja, dočasne zdvojnásobuje využitie pamäte.
Java OOMKilled So Stabilným Heapom: Native Memory, Direct Buffers a glibc Arenas
Heap metriky vyzerajú dobre, GC je spokojný, ale kontajner stále umiera. Vinník: native memory z direct buffers, JNI a glibc memory allocator fragmentácia.
Kubernetes OOM Killer: Prečo Kontajner Zomiera pri 50% Pamäte
Kontajner má 4GB memory limit ale OOM kill pri 2GB used. Kernel buffers, page cache a cgroup accounting triky spôsobujú skoré OOMKills. Tu je celý obraz.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.