Piazzare una bella cache per andare più veloci è un classico, si sa. E all’inizio sembra funzionare: risposte immediate, ridotte chiamate al database, meno traffico di rete... insomma tutti felici e contenti. Poi aumenta il traffico, le risposte iniziano a rallentare, i dati dei prodotti non quadrano e un cliente nervoso segnala la presenza di informazioni 'che non tornano'. Peggio ancora, nel panico più totale, ci ritroviamo costretti a riavviare un'applicazione ormai collassata. E così, quella cache che tanto beneficio prometteva, si trasforma nel motivo principale per cui siamo svegli alle tre di notte a guardare log e a maledire il giorno in cui l'abbiamo integrata.
In questa prima puntata di Sbagliando si impara, raccontiamo cinque errori comuni nella gestione della cache applicativa. Descriveremo la bad practice, i suoi sintomi e la rispettiva soluzione.
1. La cache che non scade mai
Si sa, partiamo sempre con le migliori intenzioni. Il mood in genere è "ma si, va bene così, invaliderò la cache a mano, quando servirà ". Risultato: un utente continua a vedere per settimane un prezzo che non esiste più o un profilo con dati vecchi di mesi. Noi, ormai presi su altri progetti, riceviamo la bella sorpresa solo quando il nostro gentile capo viene a chiederci spiegazioni, sbattendo sul tavolo un’email ricevuta da un cliente furioso.
La soluzione è semplice: per tutti i dati applicativi che possono cambiare e la cui freschezza è fondamentale, è importante impostarne il TTL in cache. La durata, possiamo configurarla in base a quanto, nello specifico contesto di business, è tollerabile avere il dato obsoleto.
2. Rigenerazione cache lasciata al caso
Questa è più subdola perché non è colpa di nessuno in particolare. Una chiave di un prodotto/elemento molto richiesto scade, e in un momento di picco ne arrivano centinaia di richieste concorrenti. Non trovando nulla in cache, tutte le richieste provano a ricalcolare lo stesso valore, magari tramite una query pesante o chiamando un' API che sfortunatamente non è dimensionata per reggere centinaia di richieste in parallelo. Il fenomeno descritto è noto come cache stampede.
Cosa fare per evitarlo? Coordiniamo la rigenerazione della cache! All’interno di un singolo processo possiamo usare il pattern singleflight; tra più istanze possiamo usare un lock distribuito. Concettualmente, una richiesta aggiorna la cache, mentre le altre attendono, entro un tempo massimo, che il valore torni disponibile. A questo affianchiamo un TTL leggermente randomizzato (jitter), per distribuire le scadenze tra chiavi diverse.
Una rigenerazione, più richieste
3. Scegliere la chiave della cache guardando il fantacalcio
Scegliere la chiave cache corretta sembra un dettaglio trascurabile. Poi qualcuno bussa alla porta et voilà: Ciao! mi chiamo.. incoerenza! Una chiave troppo generica e all’improvviso un utente italiano si ritrova l’interfaccia in inglese, oppure, peggio ancora, vede i dati di altri utenti. Una chiave troppo complessa e vedremo la nostra cache crescere all'infinito con cache-hit più unici che rari.
Come risolviamo? Non c’è molto da inventare, solo da essere rigorosi: la chiave deve contenere tutti e soli i dati che rendono univoco l’elemento in cache. Se i dati della chiave non sono sufficienti o, al contrario, sono troppi, è meglio non implementare alcuna cache. Inoltre: centralizziamo la creazione della chiave in un unico punto dell’applicazione, evitando che ogni modulo la costruisca a modo suo.
4. Fidarsi troppo della cache
Ideona! "Aggiorno solo la base dati, tanto la cache si allinea da sola". Eh No.. non funziona così, ma è incredibile quante volte si casca in questo tranello. Nel frattempo, metà degli utenti vede il dato aggiornato e metà quello vecchio, a seconda che la loro richiesta abbia trovato o meno qualcosa in cache. E se parliamo di prezzi o disponibilità di magazzino, apprezzeremo anche, con tanta gioia, le urla di qualcuno.
Ma niente panico, risolviamo. Le strade sono due. La prima è il write-through: si scrive il nuovo valore sia nel database sia nella cache. Seconda strada, più semplice, è invalidare la chiave, dopo il commit sulla base dati: invece di aggiornare la cache, la si cancella, e alla prossima lettura la base dati ci aiuterà a ripopolarla.
Anche in questo caso, facciamo attenzione! Il meccanismo di invalidazione deve essere implementato in un unico posto, non reinventato in dieci file diversi da dieci sviluppatori con dieci convinzioni diverse.
5. TTL eccessivo per il caching di errori
Un servizio esterno risponde con un 500 o va in timeout. Il nostro codice, per “ottimizzare”, memorizza in cache l’errore impostando lo stesso TTL delle risposte corrette, supponiamo 30 minuti. Il servizio esterno torna su, magari dopo trenta secondi — ma l’app continua a restituire l’errore. Un guasto di trenta secondi diventa così un disservizio di 30 minuti per gli utenti finali.
Salvare temporaneamente in cache un errore può essere utile per evitare di bombardare un servizio già in difficoltà: è il negative caching. Ma la bad practice è memorizzare indiscriminatamente gli errori con la stessa politica e lo stesso TTL delle risposte valide.
Come risolviamo? Definiamo una politica dedicata: quali errori memorizzare e per quanto tempo, distinguendo, per esempio, una risorsa assente da un guasto transitorio. Per questi ultimi scegliamo un TTL breve, commisurato al tempo di recupero che possiamo accettare.
Riepilogo veloce
| # | Errore | Sintomo principale | Soluzione |
|---|---|---|---|
| 1 | TTL infinito | Dati stale permanenti | TTL scelto in base alla tolleranza ai dati obsoleti |
| 2 | Rigenerazione lasciata al caso | Picchi di carico sulla fonte dati | Singleflight o lock distribuito; jitter sui TTL |
| 3 | Chiave della cache improvvisata | Dati mescolati tra utenti, prodotti, altro | Includere tutte e sole le variabili rilevanti; generazione centralizzata |
| 4 | Invalidazione non gestita | Incoerenza dopo una scrittura | Politica dopo il commit, con gestione degli errori e delle race condition |
| 5 | Stesso TTL per dati ed errori | Errori restituiti anche dopo il ripristino | Negative caching selettivo, con TTL dedicato |
Per chiudere
Scadenze, rigenerazione, chiavi, invalidazione ed elementi in errore nella cache: cinque aspetti che vale la pena progettare bene prima che siano i nostri simpatici capi a ricordarcelo.
Nella Parte 2 affronteremo altre cinque bad practice nella gestione della cache applicativa.
Alla prossima puntata di Sbagliando si impara.
