Späť na blog

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:

  1. Fragmentácia ratio ukazuje skutočný memory overhead
  2. Variabilné kľúče s TTL spôsobujú najhoršiu fragmentáciu
  3. Aktívna defragmentácia kompaktuje pamäť automaticky
  4. 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

Súvisiace články

Citujte tento článok

Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.

Michal Drozd. "Redis Memory Fragmentácia: Keď maxmemory Nestačí". https://www.michal-drozd.com/sk/blog/redis-memory-fragmentacia/ (Publikované 22. mája 2025).