Elasticsearch è un motore di ricerca e analisi distribuito, costruito sopra Apache Lucene, progettato per offrire indicizzazione near real-time, interrogazioni complesse e aggregazioni su grandi volumi di dati. Il suo design implementativo combina una base “per nodo” (processo JVM che gestisce risorse locali) con un piano “per cluster” (coordinamento e consenso sulle decisioni globali), mantenendo come unità fondamentali di scalabilità e disponibilità gli shard e le repliche.
Obiettivi architetturali
- Distribuzione trasparente: l’utente interagisce con un endpoint logico (cluster), mentre i dati e le query sono partizionati e replicati.
- Bilanciamento tra consistenza e disponibilità: scritture con primari e repliche, letture da copie multiple, tolleranza ai guasti e recupero automatico.
- Prestazioni prevedibili: separazione dei percorsi critici (scrittura, refresh, merge, ricerca), cache e controlli di pressione sulle risorse.
- Evolvibilità: estensioni tramite plugin, ingest, scripting controllato e APIs coerenti.
Concetti di base: indici, shard, repliche
Un indice è un namespace logico che raggruppa documenti con mappature compatibili. Fisicamente, l’indice è suddiviso in shard primari, ciascuno implementato come un indice Lucene indipendente, e può avere shard replica per disponibilità e parallelismo in lettura.
- Partizionamento: ogni documento viene assegnato a uno shard tramite una funzione di routing (tipicamente hashing di un valore di routing).
- Replica: ogni shard primario mantiene una o più repliche, aggiornate per ridurre il rischio di perdita dati e incrementare la capacità di ricerca.
- Ribilanciamento: il cluster può spostare shard tra nodi per rispettare vincoli di allocazione e per uniformare l’utilizzo di disco, CPU e heap.
Nodi e ruoli
Un cluster è un insieme di nodi. Il design prevede ruoli separabili per isolare carichi e aumentare la stabilità:
- Master-eligible: partecipano all’elezione del master e gestiscono decisioni di cluster (stato, allocazione shard, metadata).
- Data: ospitano shard e gestiscono indicizzazione e query per i dati locali.
- Ingest: applicano pipeline di trasformazione e arricchimento prima dell’indicizzazione.
- Coordinating: ricevono richieste client e orchestrano le fasi distribuite di ricerca e aggregazione (tutti i nodi possono coordinare, ma in produzione spesso si dedicano nodi a questo ruolo).
Il cluster state: metadati e coerenza operativa
Il cluster state rappresenta l’insieme dei metadati globali: indici, mapping, impostazioni, routing table (dove vivono gli shard), e informazioni sui nodi. È centralizzato nella gestione (dal master), ma distribuito nella lettura: ogni nodo mantiene una copia locale per instradare correttamente le operazioni.
Dal punto di vista implementativo, il cluster state deve rimanere relativamente compatto perché viene aggiornato e propagato frequentemente. Per questo motivo, Elasticsearch incentiva mappature e template gestiti con attenzione e un numero di indici/shard commisurato alle risorse. La propagazione degli aggiornamenti è asincrona ma ordinata: i nodi applicano le versioni in sequenza per preservare la convergenza dello stato.
Perché Lucene: segmenti, immutabilità e costo delle modifiche
Lucene memorizza i dati in segmenti immutabili. L’immutabilità permette letture concorrenti efficienti: una query può attraversare i segmenti senza bloccare l’indicizzazione. Il prezzo è che le modifiche non riscrivono in-place: gli aggiornamenti e le cancellazioni sono gestiti tramite marcature e nuove versioni, e l’effettiva compattazione avviene tramite merge.
- Inverted index: struttura primaria per la ricerca testuale; token e postings list consentono matching rapido.
- Stored fields: usati per recuperare campi in fase di fetch (ad esempio per ricostruire la risposta).
- Doc values: colonne ottimizzate per sorting, aggregazioni e script; riducono la necessità di strutture in heap.
- Strutture spaziali e numeriche: per range query e geo, Lucene usa strutture specializzate (ad esempio alberi per valori numerici e punti).
Il percorso di scrittura: dall’API al segmento
Una scrittura (index/update/delete) attraversa più livelli, con l’obiettivo di coniugare durabilità, throughput e visibilità near real-time.
1) Coordinamento e routing
Il nodo che riceve la richiesta calcola lo shard target in base al routing e inoltra l’operazione al nodo che detiene lo shard primario. Questo passaggio evita doppie decisioni e centralizza l’ordine delle operazioni sul primario.
2) Applicazione sul primario e replica
Il primario valida l’operazione (controlli di versione e concorrenza, se presenti) e la applica localmente. In seguito propaga la stessa operazione alle repliche. Dal punto di vista implementativo, il primario funge da “sequencer” per lo shard: mantiene un ordine coerente che consente alle repliche di convergere. In caso di replica in ritardo o guasta, il sistema gestisce retry, riallineamento e, se necessario, riassegnazioni.
3) Translog e durabilità
Per evitare che un crash provochi perdita delle ultime operazioni non ancora consolidate su disco nei segmenti, Elasticsearch usa un transaction log (translog) per shard. Ogni operazione viene registrata nel translog e poi applicata alle strutture in-memory di Lucene (buffer di indicizzazione). La durabilità è configurabile: si può forzare la sincronizzazione del translog su disco con granularità adeguata al profilo di rischio e performance.
4) Refresh: visibilità near real-time
La ricerca diventa “quasi” real-time grazie al refresh, che rende visibili i documenti indicizzati aprendo un nuovo searcher su segmenti aggiornati. Il refresh non equivale a un commit completo su disco; è un’operazione pensata per bilanciare latenza di visibilità e overhead. Un refresh troppo frequente aumenta il numero di segmenti piccoli, con impatto negativo sulle query e sui merge.
5) Flush e commit
Periodicamente, o in risposta a condizioni specifiche, lo shard esegue un flush, che stabilizza lo stato su disco e consente di troncare il translog. Il flush riduce il costo del recovery dopo un riavvio, perché diminuisce la porzione di translog da riprodurre.
6) Merge: ottimizzazione progressiva
Il merge combina segmenti più piccoli in segmenti più grandi. Implementativamente è un processo intensivo in I/O e CPU, ma essenziale per mantenere buone prestazioni: riduce il numero di segmenti da attraversare in query, libera spazio eliminando cancellazioni e migliora l’efficienza delle strutture on-disk. Le politiche di merge devono essere bilanciate con la capacità del disco e con il profilo del carico.
Gestione degli aggiornamenti: versioning e concorrenza
Un “update” in Elasticsearch è concettualmente una riscrittura: legge il documento corrente (o applica una patch), poi indicizza una nuova versione e marca la precedente come eliminata. Per evitare conflitti e scritture fuori ordine, il sistema supporta strategie di controllo concorrenza (ad esempio basate su versioni o su metadati di sequenza interni) che permettono al client di garantire che un’operazione si applichi solo se lo stato di partenza corrisponde alle aspettative.
Ricerca distribuita: fasi query e fetch
Il percorso di lettura è dominato dalla necessità di combinare risultati provenienti da più shard, mantenendo latenza e consumo memoria sotto controllo.
1) Fan-out
Il nodo coordinatore determina gli shard coinvolti (per indice, alias, routing e filtri) e invia la query in parallelo ai nodi che ospitano le copie degli shard. A seconda della strategia e dello stato del cluster, il coordinatore può scegliere tra primari e repliche per bilanciare il carico.
2) Fase di query
Ogni shard esegue la query localmente su Lucene e restituisce al coordinatore un insieme compatto di informazioni: tipicamente gli identificativi dei documenti più rilevanti (top-N) con punteggio e metadati necessari per la fusione globale. Questo minimizza il traffico di rete rispetto all’invio immediato dei documenti completi.
3) Fusione e ordinamento globale
Il coordinatore combina i risultati parziali applicando sorting e criteri di ranking. L’implementazione deve gestire con attenzione il costo di mantenere heap e strutture temporanee: l’uso di top-N per shard limita la crescita combinatoria.
4) Fase di fetch
Una volta determinati i documenti finali, il coordinatore richiede i contenuti completi agli shard che li possiedono. Il fetch recupera stored fields, sorgente e campi richiesti, applica eventuali evidenziazioni, script o trasformazioni, e costruisce la risposta.
Aggregazioni: design per analisi ad alte prestazioni
Le aggregazioni sono un punto distintivo: permettono analisi in tempo reale su grandi dataset. Il loro design implementativo si appoggia a doc values e a strutture specializzate per ridurre l’uso di heap e massimizzare l’accesso sequenziale su disco.
- Pipeline per shard: ogni shard calcola bucket e metriche locali; il coordinatore effettua la riduzione (reduce) globale.
- Riduzione incrementale: dove possibile, l’aggregazione riduce parzialmente i risultati per limitare memoria e traffico.
- Cardinalità e approssimazioni: alcune metriche possono usare strutture probabilistiche per offrire compromessi tra accuratezza e performance.
- Sorting e paginazione: bucket ordinati e paginati richiedono strutture aggiuntive; per grandi dimensioni occorrono strategie che evitino la materializzazione completa in memoria.
Cache e prestazioni: cosa si memorizza e perché
Elasticsearch usa più livelli di caching, ciascuno con obiettivi differenti:
- Cache a livello di file system: la più importante; Lucene lavora bene se il sistema operativo può mantenere in cache i segmenti letti frequentemente.
- Query cache: utile per filtri ripetuti su segmenti immutabili; invalidazione legata a refresh e nuovi segmenti.
- Request cache: memorizza risposte di query “cacheabili”, spesso per dashboard e aggregazioni su dati che cambiano lentamente.
- Fielddata: per alcuni casi (storicamente su campi text) può caricare strutture in heap; è potente ma rischiosa e va gestita con attenzione.
Un aspetto implementativo centrale è l’invalidazione: l’immutabilità dei segmenti permette di rendere la cache sicura e semplice, ma i refresh frequenti aumentano il churn e riducono l’efficacia. La configurazione ottimale dipende dal rapporto tra scritture e letture e dal tipo di query.
Gestione della memoria e protezioni
Elasticsearch opera su JVM e deve prevenire situazioni di memoria incontrollata. Oltre alla distinzione tra heap JVM e page cache del sistema operativo, adotta meccanismi di protezione:
- Circuit breakers: stime preventive dell’uso memoria per evitare OutOfMemoryError durante query e aggregazioni.
- Strutture off-heap implicite: doc values e segmenti sono su disco e sfruttano la cache del kernel, riducendo pressione sull’heap.
- Politiche di dimensionamento: la scelta della dimensione heap influenza garbage collection e latenza; un heap eccessivo può peggiorare le pause, uno troppo piccolo limita buffer e cache in memoria JVM.
Concorrenza e thread pool
Per isolare carichi e prevenire starvation, il design prevede thread pool dedicati a categorie di operazioni: ricerca, scrittura, gestione, snapshot, fetch, e così via. Ogni pool ha code e limiti che fungono da backpressure. Se il sistema è saturo, le code crescono fino a soglie oltre le quali le richieste vengono rifiutate per proteggere il cluster da un collasso a cascata.
Replica, recovery e resilienza ai guasti
La resilienza dipende dall’abilità di ricostruire rapidamente shard e repliche dopo guasti o riallocazioni:
- Peer recovery: una replica si riallinea copiando segmenti dal primario e applicando operazioni recenti non presenti localmente.
- Riapertura dopo crash: lo shard riparte leggendo i segmenti consistenti e riproducendo il translog residuo.
- Gestione delle elezioni: in caso di perdita del master, i nodi master-eligible eleggono un nuovo master, che ripubblica un cluster state coerente.
- Promozione di replica a primario: se il primario si perde, una replica può diventare nuovo primario, preservando la disponibilità dell’indice.
Snapshot e ripristino: backup incrementali
Per il backup, Elasticsearch offre snapshot che copiano segmenti immutabili verso uno storage remoto. L’immutabilità consente approcci incrementali: segmenti già presenti in uno snapshot precedente non vengono ricopiati. Il ripristino ricostruisce gli shard scaricando i segmenti necessari e riallineando i metadati di indice e cluster, nei limiti delle configurazioni e delle compatibilità di versione.
Mapping e analisi del testo
Il mapping definisce tipi di campo, analizzatori e opzioni di indicizzazione. Implementativamente, il mapping governa come un campo viene trasformato in token (analisi) e quali strutture Lucene vengono popolati (inverted index, doc values, stored fields).
- Analizzatori: pipeline di tokenizzazione e normalizzazione che determina il comportamento di match.
- Campi multi-field: lo stesso valore può essere indicizzato in forme diverse (ad esempio una variante per full-text e una per sorting/aggregazioni).
- Dynamic mapping: comodo per onboarding rapido, ma rischioso per crescita del cluster state e incoerenze; in sistemi critici è preferibile un controllo più rigoroso.
Ingest: trasformazioni prima dell’indicizzazione
Le pipeline di ingest permettono di trasformare documenti in ingresso: parsing, arricchimento, normalizzazione, estrazione di campi. Dal punto di vista implementativo, spostano parte del lavoro applicativo dentro il cluster, con vantaggi di standardizzazione ma anche con costi di CPU e latenza. Un design robusto separa nodi ingest dai nodi data in modo da preservare la stabilità del percorso di ricerca.
Scalabilità operativa: scelte progettuali e compromessi
Molte decisioni implementative ruotano intorno a tre leve: numero di shard, dimensione dei segmenti e profilo di refresh/merge. Un numero eccessivo di shard moltiplica overhead (file aperti, metadati, recovery), mentre shard troppo grandi aumentano i tempi di recovery e possono creare hotspot. La progettazione di un cluster efficace consiste nell’allineare questi parametri al carico reale di scrittura, lettura e analisi.
Osservabilità e diagnosi
Per mantenere il sistema in salute, il design include metriche e endpoint di introspezione per thread pool, tempi di garbage collection, pressione heap, utilizzo disco, stato shard e latenza di query. A livello implementativo, la diagnostica è essenziale perché molti problemi emergono come effetti secondari: merge in competizione con query, refresh troppo frequenti, aggregazioni con cardinalità elevata, o saturazione delle code.
Conclusione
Il design implementativo di Elasticsearch è un equilibrio tra il modello immutabile e segmentato di Lucene e la necessità di offrire un servizio distribuito, elastico e resiliente. Shard e repliche sono la base della scalabilità; cluster state e ruoli dei nodi governano il coordinamento; translog, refresh, flush e merge definiscono il ciclo di vita dei dati; query/fetch e riduzioni delle aggregazioni determinano la latenza in lettura. Comprendere questi meccanismi, e i loro compromessi, è il passo decisivo per progettare indici, configurare cluster e ottenere prestazioni stabili nel tempo.