fbpx

Ci capita sempre più spesso di dover utilizzare ElasticSearch con degli applicativi in cloud multi-tenant. La prima domanda posta da 2 dei nostri clienti è stata la seguente: come poter strutturare e ottimizzare gli indici ed i nodi per mantenere prestazioni ottimali, con il crescere di dati e utilizzatori?

Utilizzare un indice per ciascun Tenant

La soluzione più semplice ed ovvia potrebbe essere quella di applicare la separazione degli indici per ciascun TENANT (intesa come singola istanza di un software, eseguita da server, utilizzata da più inquilini o tenant) utilizzando un prefisso univoco. Ciascun indice potrebbe poi essere assegnato ad uno o più nodi attraverso le shard allocation settings di ELASTIC SEARCH in base anche a regole di allocazione personalizzate.

Questa soluzione permetterebbe di raggiungere il maggior livello di velocità nell’esecuzione delle query, poiché ciascun indice risulterebbe già filtrato per tenant; questo si tradurrebbe nella possibilità di semplificare i filtri. Inoltre, in questa particolare configurazione si avrebbero dei nodi più performanti con i dati già raggruppati per TENANT.

Bisogna però tener conto del costo in termini di risorse necessarie per poter implementare questa soluzione e della necessità di un’infrastruttura facilmente scalabile.

In una configurazione standard su un singolo SERVER, si arriverebbe ben presto a riscontrare errori di memoria dovuti all’allocazione di molteplici indici.

Dopo aver discusso della possibilità di creare un indice per ciascun tenant, si può affermare con certezza che non è la soluzione definitiva per applicativi che prevedono molti utenti: il beneficio che si avrebbe nella fase iniziale del progetto andrebbe a scemare col l’aumentare degli utenti e dei dati.

Utilizzare un indice condiviso per più Tenant

L’utilizzo di un solo indice e di un FIELD per filtrare il TENANT sembrerebbe la soluzione più appropriata sia nel breve che nel lungo termine, poiché non richiederebbe interventi di tipo infrastrutturale e/o complesse migrazioni di dati. Si potrebbe agire semplicemente sulle query e sul “mapping” dell’indice per ottimizzare l’uso delle risorse.

Con il crescere del numero di utenti e di dati, si potrebbero collegare nuovi nodi e scalare molto velocemente, mantenendo lo stesso livello di prestazioni. Ottenendo, oltretutto, una distribuzione più omogenea dei dati rispetto alla soluzione precedente.

Utilizzo di un routing personalizzato

Anche se la soluzione di mantenere un indice multi-tenant è l’unica strada che ci sembra percorribile è chiaro che le prestazioni saranno sempre legate alla quantità di dati e al numero di SHARDS che ciascun indice conterrà. Per ottimizzare le query di ricerca ed ottenere delle prestazioni indipendenti dal numero di shards presenti sull’indice, ci viene incontro la funzionalità di custom routing offerta da Elasticsearch: è possibile infatti per ciascun documento ed in generale per ciascuna richiesta, specificare un attributo routing che servirà a raggruppare i dati nello stesso shard ed ottimizzare tutte le operazioni sull’indice. Nel nostro caso andremo a specificare per ciascuna query l’ID univoco del TENANT come chiave di routing. Un approccio che ci permetterà di avere una distribuzione ottimale dei dati sia all’interno di ciascun shard che a livello globale, sui singoli nodi.

Inserire schema come questo:

Introduzione del concetto di Caching

Un ulteriore incremento delle prestazioni potrebbe essere ottenuto attraverso l’utilizzo della cache disponibile nativamente in Elastic.

Bisogna però considerare i vari livelli di CACHE utilizzabili e la possibilità di riutilizzare i risultati tra i vari client.

Node Query Cache

Questo tipo di cache viene condivisa con tutti gli shards presenti nel singolo nodo. Si applica solo al contesto filter della QUERY e per questo motivo non ha impatti sullo score del risultato.

Volendo semplificare molto il funzionamento di questa cache, possiamo affermare che si limita a memorizzare solo i documenti inclusi (ed esclusi) nel risultato della query.

Shard Request Cache

Questo livello di cache memorizza i risultati in modo indipendente per ciascun shard.

Solo le request di tipo size, aggregations, counts e suggestions vengono memorizzate.

Alcune clausole, come quelle di tipo DateTime contenente la variabile “now”, vengono escluse automaticamente dalla cache. Quando uno SHARD viene aggiornato la cache viene invalidata in modo automatico. Per questo motivo bisogna fare attenzione ad utilizzarla su indici sottoposti a frequenti aggiornamenti, per evitare un peggioramento generale delle performance.

Field Data Cache

Questo livello di cache viene utilizzato durante le aggregazioni e il sorting dei risultati. I dati dei fields coinvolti vengono caricati nella memoria per permettere ad Elastic Search di accedervi velocemente. Per questo motivo è importante disporre di una buona quantità di memoria da allocare per il field data cache.

Conclusione

Per applicare i concetti spiegati in questo articolo è necessario prestare molta attenzione nella costruzione delle query, applicando delle logiche di separazione per sfruttare a pieno le potenzialità della cache.

Inserire uno schema come questo (triangoli,cerchi e quadrati sono documenti).

Come prima cosa bisognerebbe estrapolare e separare le aggregazioni dalle query :

questo approccio potrebbe sembrare un contro-senso. Perché calcolare due volte qualcosa o effettuare due query, se si può ottenere lo stesso risultato in un solo blocco di condizioni? In pratica, applicando la separazione delle logiche, le aggregazioni saranno più performanti perché memorizzate nella cache. Inoltre, questo eviterà di ricalcolare le aggregazioni per ciascun utente.

Distinguere i filtri dalle clausole match. I filtri comuni sono altamente memorizzabili nella cache e veloci da calcolare; i filtri che incidono sullo score invece sono difficilmente gestibili nella cache.

Anche in questo caso la soluzione potrebbe apparire ridondante, ma la separazione dei filtri generici rispetto alle clausole match che agiscono sullo score può tornare molto utile.

Usare filtri riutilizzabili per restringere il set di risultati prima di applicare lo score. Allo stesso modo, usare i campi script per ottenere lo score ma non per filtrare.

I filtri vengono eseguiti più o meno in ordine. ElasticSearch esegue qualche riscrittura delle query, ma in generale prima posiziona i filtri meno costosi e poi quelli più costosi.

Se è necessario filtrare per data / ora, utilizzare una granularità grossolana in modo che il valore della query cambi di rado e si possa lavorare con i filtri, sfruttando così al massimo la cache.

Vuoi parlarne di persona?
Non esitare, contattaci ora!