Scale · 06 · Memoria e tuning
Redis è un database in memoria: la memoria non è una risorsa da monitorare, è la risorsa. Questa unità è quella con il miglior rapporto tra sforzo e risparmio: le soglie di encoding da sole valgono più di qualunque altro tuning.
Prerequisito: istanza di lab su 6379.
Esercizio 6.1 — Dove va la memoria
Sezione intitolata “Esercizio 6.1 — Dove va la memoria”redis-cli INFO memory | grep -E 'used_memory_human|used_memory_rss_human|used_memory_peak_human|used_memory_lua|used_memory_dataset|maxmemory_human|maxmemory_policy|mem_fragmentation_ratio|mem_allocator'redis-cli MEMORY STATS | head -30Le voci che contano:
| Voce | Cosa misura |
|---|---|
used_memory | quanto ha allocato Redis (visione dell’allocatore) |
used_memory_rss | quanto vede il sistema operativo |
used_memory_dataset | i soli dati, al netto dell’overhead |
used_memory_peak | il picco storico — è questo che dimensiona il nodo |
mem_fragmentation_ratio | rss / used_memory |
Verifica: dimensiona sempre sul picco, non sul valore corrente. Il picco
include l’overhead del fork durante il BGSAVE, ed è il numero che determina se
il nodo va in OOM alle 3 di notte.
Esercizio 6.2 — Encoding: il tuning che rende di più
Sezione intitolata “Esercizio 6.2 — Encoding: il tuning che rende di più”Redis usa rappresentazioni compatte (listpack, intset) finché la collezione
resta sotto una soglia, poi passa a strutture con puntatori (hashtable,
skiplist) molto più costose in memoria.
redis-cli CONFIG GET hash-max-listpack-entries hash-max-listpack-value \ zset-max-listpack-entries set-max-intset-entries list-max-listpack-sizeDefault misurati (Redis 7.0):
hash-max-listpack-entries 512hash-max-listpack-value 64zset-max-listpack-entries 128set-max-intset-entries 512list-max-listpack-size -2 (negativo = limite per dimensione, non per numero)Osserva la conversione:
{ for i in $(seq 1 600); do echo "HSET h3 f$i v$i"; done; } | redis-cli --piperedis-cli OBJECT ENCODING h3redis-cli MEMORY USAGE h3
redis-cli CONFIG SET hash-max-listpack-entries 1000{ for i in $(seq 1 600); do echo "HSET h4 f$i v$i"; done; } | redis-cli --piperedis-cli OBJECT ENCODING h4redis-cli MEMORY USAGE h4Output misurato:
h3 600 campi, soglia 512 -> hashtable 32296 byteh4 600 campi, soglia 1000 -> listpack 7216 byte4,5× di memoria in meno con lo stesso identico dato, cambiando un parametro.
Encoding degli insiemi:
redis-cli DEL s; redis-cli SADD s 1 2 3; redis-cli OBJECT ENCODING s # intsetredis-cli SADD s ciao; redis-cli OBJECT ENCODING s # hashtableVerifica: un solo elemento non numerico fa perdere l’intset all’intero
set. Se un’applicazione mescola id numerici e stringhe nello stesso set, il
costo in memoria esplode: è un finding da girare agli AM con il numero in mano
(MEMORY USAGE prima e dopo).
Esercizio 6.3 — maxmemory e le policy di eviction
Sezione intitolata “Esercizio 6.3 — maxmemory e le policy di eviction”redis-cli FLUSHALLredis-cli CONFIG SET maxmemory 3mbredis-cli CONFIG SET maxmemory-policy allkeys-lru{ for i in $(seq 1 40000); do echo "SET big:$i payloadpayloadpayloadpayload$i"; done; } | redis-cli --piperedis-cli INFO stats | grep evicted_keysredis-cli DBSIZEredis-cli INFO memory | grep -E 'used_memory_human|maxmemory_human'Output misurato:
evicted_keys:23231DBSIZE:16549used_memory_human:3.00Mmaxmemory_human:3.00MRedis ha scritto tutte le 40000 chiavi, ne ha sfrattate 23231 e si è fermato esattamente sul limite. Nessun errore lato client: il comportamento corretto per una cache.
Ora la stessa prova con la policy di default:
redis-cli CONFIG SET maxmemory-policy noeviction{ for i in $(seq 1 20000); do echo "SET oom:$i payloadpayloadpayload$i"; done; } | redis-cli --pipeOutput misurato:
errors: 19999, replies: 20000OOM command not allowed when used memory > 'maxmemory'.Verifica: questa è la differenza tra una cache e un data store, e si decide con un solo parametro.
| Uso | maxmemory-policy | Effetto al limite |
|---|---|---|
| Cache | allkeys-lru / allkeys-lfu | sfratta le chiavi meno usate, il servizio continua |
| Data store | noeviction | rifiuta le scritture con OOM, nessun dato perso |
| Sessioni con TTL | volatile-lru / volatile-ttl | sfratta solo le chiavi con TTL |
maxmemory 0 è il default: nessun limite, crescita fino all’OOM killer del
kernel. Su ogni istanza cache va impostato esplicitamente.
Esercizio 6.4 — Frammentazione e defrag attivo
Sezione intitolata “Esercizio 6.4 — Frammentazione e defrag attivo”redis-cli INFO memory | grep -E 'mem_fragmentation_ratio|mem_allocator'redis-cli MEMORY DOCTORredis-cli CONFIG GET activedefrag active-defrag-threshold-lower active-defrag-ignore-bytes active-defrag-cycle-minInterpretazione — l’unica tabella da ricordare:
mem_fragmentation_ratio | Significato | Azione |
|---|---|---|
| ~1.0–1.4 | normale | nessuna |
| > 1.5 | frammentazione reale | valuta activedefrag yes |
| < 1.0 | il processo sta swappando | emergenza: RAM insufficiente |
| molto alto su istanza quasi vuota | falso positivo | l’overhead fisso domina |
redis-cli CONFIG SET activedefrag yes # richiede allocatore jemallocredis-cli INFO memory | grep active_defragVerifica: activedefrag funziona solo con jemalloc (mem_allocator:jemalloc,
il default nei build ufficiali). Costa CPU: il defrag gira sul thread
principale, quindi va acceso con active-defrag-cycle-min basso e verificato
sullo slowlog.
Esercizio 6.5 — Il fork, la RAM e il kernel
Sezione intitolata “Esercizio 6.5 — Il fork, la RAM e il kernel”Il punto che manda in OOM i nodi: BGSAVE e BGREWRITEAOF fanno fork(), e le
pagine modificate durante lo snapshot vengono copiate.
redis-cli INFO stats | grep latest_fork_useccat /proc/meminfo | grep -E 'MemTotal|MemAvailable|Committed_AS'sysctl vm.overcommit_memory vm.swappinesscat /sys/kernel/mm/transparent_hugepage/enabledOutput misurato su un host non preparato:
vm.overcommit_memory = 0vm.swappiness = 60always [madvise] never <- THP attiveTutte e tre le impostazioni sono sbagliate per Redis. Correzione persistente:
cat > /etc/sysctl.d/99-redis.conf <<'EOF'vm.overcommit_memory = 1vm.swappiness = 1net.core.somaxconn = 1024EOFsysctl --system# THP: latenza del fork fino a 10x. Disattivare, in modo persistente.echo never > /sys/kernel/mm/transparent_hugepage/enabledSu RHEL, in modo che sopravviva al reboot:
grubby --update-kernel=ALL --args="transparent_hugepage=never"# oppure una unit systemd che scrive il valore prima di redis.serviceVerifica: con vm.overcommit_memory = 0 il fork() può fallire quando
used_memory supera metà della RAM libera, e il BGSAVE non parte:
rdb_last_bgsave_status:err senza altri sintomi. È la causa più comune di
backup silenziosamente assenti.
Regola di sizing: maxmemory ≈ 60–70% della RAM del nodo. Il resto serve al
fork, al buffer di replica e all’output buffer dei client.
Esercizio 6.6 — Lazy free
Sezione intitolata “Esercizio 6.6 — Lazy free”La cancellazione di una chiave enorme è sincrona per default: blocca l’istanza
per tutto il tempo della free().
redis-cli CONFIG GET lazyfree-lazy-eviction lazyfree-lazy-expire \ lazyfree-lazy-server-del lazyfree-lazy-user-del replica-lazy-flushDefault misurati: tutti no. Attivali su istanze con collezioni grandi:
for p in lazyfree-lazy-eviction lazyfree-lazy-expire lazyfree-lazy-server-del lazyfree-lazy-user-del replica-lazy-flush; do redis-cli CONFIG SET $p yesdoneEquivalente puntuale, senza cambiare la configurazione:
redis-cli UNLINK chiave-enorme # asincrono, invece di DELredis-cli FLUSHALL ASYNCVerifica: con lazyfree-lazy-expire no, la scadenza simultanea di molte
chiavi grandi produce spike visibili in LATENCY HISTORY expire-cycle. È il
sospettato numero uno quando la latenza ha picchi periodici senza carico
corrispondente.
Esercizio 6.7 — Memoria dei client (Redis 7+)
Sezione intitolata “Esercizio 6.7 — Memoria dei client (Redis 7+)”redis-cli CONFIG GET maxmemory-clientsredis-cli CLIENT NO-EVICT on # esenta il tuo client di ops dall'evictionredis-cli INFO clients | grep -E 'client_recent_max_output_buffer|client_recent_max_input_buffer'maxmemory-clients (default 0 = disattivato) limita la memoria complessiva
dei buffer client e sfratta le connessioni più esose invece di far crescere
used_memory fino all’OOM. Valore ragionevole: 5% o un valore assoluto.
Ricordati di esentare le connessioni di monitoraggio e le replica.
Esercizio 6.8 — Trovare cosa occupa la memoria
Sezione intitolata “Esercizio 6.8 — Trovare cosa occupa la memoria”redis-cli --bigkeys # la chiave più grande per tipo, via SCANredis-cli --memkeys # ordinamento per memoria occupataredis-cli --memkeys-samples 0 # campionamento esatto (più lento)redis-cli MEMORY USAGE <chiave> SAMPLES 0Distribuzione per prefisso — il comando che serve davvero per parlare con gli AM:
redis-cli --scan --count 1000 | awk -F: '{print $1}' | sort | uniq -c | sort -rn | head -20Verifica: tutti usano SCAN sotto il cofano, quindi sono sicuri in
produzione. KEYS * no: è O(n) sul thread principale e su milioni di chiavi
congela l’istanza per secondi. Se lo trovi in uno slowlog, è un finding da
change immediato.
Checklist del modulo
Sezione intitolata “Checklist del modulo”-
maxmemoryimpostato (mai 0 su una cache), a ~60–70% della RAM del nodo -
maxmemory-policycoerente con l’uso (cache vs data store) - con policy
volatile-*, verificato che le chiavi abbiano davvero un TTL - soglie di encoding tarate sul 95° percentile delle collezioni reali
-
mem_fragmentation_ratiomonitorato, con allerta separata per< 1.0 - THP disattivate in modo persistente
-
vm.overcommit_memory = 1,vm.swappiness = 1 -
lazyfree-*attivi se esistono collezioni grandi -
used_memory_peakusato per il sizing, nonused_memory
Domande di verifica
Sezione intitolata “Domande di verifica”- Stesso hash da 600 campi, 32 KB in un caso e 7 KB nell’altro. Cosa è cambiato e cosa hai pagato in cambio?
- Policy
volatile-lrue istanza che rispondeOOM. Qual è la causa? mem_fragmentation_ratioa 0.7:activedefragrisolve?rdb_last_bgsave_status:errcon disco libero e permessi corretti. Quale parametro del kernel guardi?
Prossimo passo: 07 · Workload applicativi reali.