Quando si lavora con grandi volumi di dati, può sorgere il problema della mancanza di spazio su disco. Uno dei modi per affrontare questo problema è la compressione, che consente di aumentare le capacità di archiviazione utilizzando la stessa infrastruttura. In questo articolo, esamineremo come funziona la compressione dei dati in Apache Ignite. Verranno descritti solo i metodi di compressione implementati all'interno del prodotto. Altri metodi di compressione dei dati (in rete, in memoria), sia implementati che non, rimarranno al di fuori dell'ambito di questo articolo.
Pertanto, quando la modalità persistence è attivata, a seguito di modifiche ai dati nelle cache, Ignite inizia a scrivere su disco:
- Il contenuto delle cache
- Il diario delle scritture anticipate (Write Ahead Log, di seguito semplicemente WAL)
Per la compressione del WAL esiste da tempo un meccanismo chiamato WAL compaction. Nella recente versione di Apache Ignite 2.8 sono stati introdotti altri due meccanismi che consentono di comprimere i dati su disco, il disk page compression per la compressione del contenuto delle cache e il WAL page snapshot compression per la compressione di alcune voci del WAL. Maggiori dettagli su questi tre meccanismi di seguito.
Compressore di pagine su disco
Come funziona
Per iniziare, diamo un'overview molto breve su come Ignite memorizza i dati. Viene utilizzata la memoria page-oriented. La dimensione della pagina è impostata all'avvio del nodo e non può essere modificata in seguito; inoltre, deve essere una potenza di due e multiplo della dimensione del blocco del file system. Le pagine vengono caricate nella RAM dal disco all'occorrenza, mentre la dimensione dei dati su disco può superare la quantità di RAM allocata. In caso di insufficienza di spazio nella RAM per caricare una pagina dal disco, le vecchie pagine non più utilizzate verranno espulse dalla RAM.
I dati sul disco sono memorizzati come segue: per ogni partizione di ogni gruppo di cache viene creato un file separato; in questo file, in ordine crescente di indice, si trovano le pagine. L'identificatore completo della pagina contiene l'identificatore del gruppo di cache, il numero della partizione e l'indice della pagina nel file. In questo modo, tramite l'identificatore completo della pagina, possiamo determinare in modo univoco il file e l'offset nel file per ogni pagina. Maggiori dettagli sull'architettura della memoria page-oriented possono essere letti nell'articolo su Apache Ignite Wiki: .
Il meccanismo di compressione delle pagine su disco, come suggerisce il nome, opera a livello di pagina. Quando questo meccanismo è attivo, la gestione dei dati nella RAM avviene senza alcuna compressione, ma al momento del salvataggio delle pagine dalla RAM al disco, viene eseguita la compressione.
Tuttavia, comprimere ogni pagina singolarmente non è ancora una soluzione al problema; è necessario ridurre la dimensione dei file finali contenenti i dati. Se la dimensione della pagina smette di essere fissa, non possiamo più scrivere le pagine nel file una dopo l'altra, poiché questo può generare una serie di problemi:
- Non saremo in grado di calcolare l'offset della pagina utilizzando l'indice della pagina stessa per determinare la sua posizione nel file.
- Non è chiaro come gestire le pagine che non si trovano alla fine del file e cambiano dimensione. Se la dimensione di una pagina diminuisce, lo spazio che ha liberato viene perso. Se la dimensione di una pagina aumenta, è necessario cercare un nuovo spazio nel file per essa.
- Se la pagina si sposta di un numero di byte non multiplo della dimensione del blocco del file system, sarà necessario coinvolgere un blocco del file system in più per la lettura o la scrittura, il che può portare a una degradazione delle prestazioni.
Per non dover affrontare questi problemi a livello personale, la compressione delle pagine disco in Apache Ignite utilizza un meccanismo del file system chiamato file sparse. Un file sparse è un file in cui alcune aree, riempite di zeri, possono essere contrassegnate come «buchi». In questo modo, non verranno allocati blocchi del file system per memorizzare questi buchi, portando a un risparmio di spazio su disco.
È logico che, per liberare un blocco del filesystem, la dimensione del buco debba essere maggiore o uguale a quella del blocco del filesystem, il che impone un'ulteriore limitazione sulla dimensione della pagina in Apache Ignite: affinché la compressione produca qualche effetto, la dimensione della pagina deve essere rigorosamente superiore alla dimensione del blocco del filesystem. Se la dimensione della pagina è uguale a quella del blocco, non saremo mai in grado di liberare alcun blocco, in quanto per poter liberare un singolo blocco la pagina compressa deve occupare 0 byte. Se invece la dimensione della pagina è pari a quella di 2 o 4 blocchi, saremo in grado di liberare almeno un blocco se la nostra pagina si comprime almeno al 50% o al 75% rispettivamente.
Pertanto, la descrizione finale del funzionamento del meccanismo è: quando una pagina viene scritta su disco, si prova a comprimere la pagina. Se la dimensione della pagina compressa consente di liberare uno o più blocchi del filesystem, la pagina viene scritta in forma compressa, e nei luoghi dei blocchi liberati viene creato un "buco" (viene effettuata una chiamata di sistema) fallocate() con il flag «punch hole»). Se la dimensione della pagina compressa non consente di liberare i blocchi, la pagina viene mantenuta così com'è, in formato non compresso. Tutti gli offset delle pagine vengono calcolati come senza compressione, moltiplicando l'indice della pagina per la dimensione della pagina. Non è necessaria alcuna rilocazione delle pagine manualmente. Gli offset delle pagine, così come senza compressione, si trovano ai confini dei blocchi del file system.

Nell'attuale implementazione, Ignite può gestire file sparse solo su sistema operativo Linux, pertanto la compressione delle pagine disco può essere attivata solo quando si utilizza Ignite su questo sistema operativo.
Gli algoritmi di compressione che possono essere utilizzati per la compressione delle pagine disco: ZSTD, LZ4, Snappy. Inoltre, esiste una modalità operativa (SKIP_GARBAGE) in cui vengono eliminati solo gli spazi non utilizzati nella pagina senza applicare compressione sui dati rimanenti, riducendo così il carico sulla CPU rispetto agli algoritmi menzionati in precedenza.
Impatto sulle prestazioni
Sfortunatamente, non ho effettuato misurazioni delle prestazioni in ambienti reali, poiché non abbiamo intenzione di utilizzare questo meccanismo in produzione, ma possiamo riflettere teoricamente su dove potremmo perdere o guadagnare.
Per questo, dobbiamo ricordare come avviene la lettura e la scrittura delle pagine quando vengono chiamate:
- Durante l'operazione di lettura, innanzitutto viene effettuata una ricerca nella RAM; se la ricerca non ha successo, la pagina viene caricata in RAM dal disco dallo stesso thread che esegue la lettura.
- Durante l'operazione di scrittura, la pagina in RAM viene contrassegnata come sporca, mentre il salvataggio fisico della pagina sul disco non avviene immediatamente nel thread che esegue la scrittura. Tutte le pagine sporche vengono salvate sul disco successivamente durante il processo di checkpoint da thread separati.
In questo modo, l'impatto sulle operazioni di lettura è:
- Positivo (IO del disco), grazie alla riduzione del numero di blocchi letti dal file system.
- Negativo (CPU), a causa del carico aggiuntivo necessario per il funzionamento del sistema operativo con file sparse. È anche possibile che qui emerga implicitamente un'ulteriore operazione di I/O per il salvataggio di una struttura più complessa del file sparse (purtroppo non sono esperto in tutti i dettagli del funzionamento dei file sparse).
- Negativo (CPU), a causa della necessità di decomprimere le pagine.
- Nessuna influenza sulle operazioni di scrittura.
- Influenza sul processo di checkpoint (qui è tutto simile alle operazioni di lettura):
- Positivo (disk IO), grazie alla riduzione del numero di blocchi scritti nel filesystem.
- Negativo (CPU, possibile disk IO), a causa del lavoro con i file sparse.
- Negativo (CPU), a causa della necessità di comprimere le pagine.
Quale piatto della bilancia prevarrà? Questo dipende molto dall'ambiente, ma sono propenso a ritenere che la compressione delle pagine di disco porterà più probabilmente a una degradazione delle prestazioni nella maggior parte dei sistemi. Inoltre, i test su altri DBMS che utilizzano un approccio simile con file sparse mostrano un calo delle prestazioni con la compressione attivata.
Come attivare e configurare
Come già accennato in precedenza, la versione minima di Apache Ignite che supporta la compressione delle pagine su disco è la 2.8, ed è supportata solo dal sistema operativo Linux. L'attivazione e la configurazione avvengono nel seguente modo:
- Nel class-path deve essere presente il modulo ignite-compression. Di default, si trova nella distribuzione di Apache Ignite nella directory libs/optional e non è incluso nel class-path. È possibile semplicemente spostare la directory di un livello verso l'alto nella cartella libs, e quindi, avviando tramite ignite.sh, sarà automaticamente incluso.
- La persistenza deve essere attivata (si attiva tramite
DataRegionConfiguration.setPersistenceEnabled(true)). - La dimensione della pagina deve essere maggiore della dimensione del blocco del file system (da configurare con
DataStorageConfiguration.setPageSize()). - Per ogni cache di cui si desidera comprimere i dati, è necessario configurare nella configurazione il metodo di compressione e (opzionalmente) il livello di compressione (metodi
CacheConfiguration.setDiskPageCompression(), CacheConfiguration.setDiskPageCompressionLevel()).
Compattazione WAL
Come funziona
Che cos'è il WAL e a cosa serve? In breve: è un registro che raccoglie tutti gli eventi che alla fine modificano lo spazio di memorizzazione delle pagine. Serve principalmente per consentire il ripristino in caso di guasto. Qualsiasi operazione, prima di restituire il controllo all'utente, deve prima registrare un evento nel WAL, in modo da avere la possibilità di riprodurre il registro e ripristinare tutte le operazioni per le quali l'utente ha ricevuto risposta positiva, anche se tali operazioni non sono ancora state riflesse nello spazio di memorizzazione delle pagine su disco (è già stato descritto che la registrazione effettiva nello spazio di memorizzazione delle pagine avviene tramite un processo chiamato "checkpoint" con un certo ritardo da parte di thread separati).
Le registrazioni in WAL si suddividono in logiche e fisiche. Le logiche sono le chiavi e i valori stessi. Le fisiche riflettono le modifiche alle pagine nella memoria delle pagine. Se le registrazioni logiche possono essere utili anche per altri casi, quelle fisiche sono necessarie solo per il recupero in caso di crash e richiedono registrazioni solo dall'ultimo checkpoint riuscito. Qui non entreremo nei dettagli e non spiegheremo perché funziona in questo modo, ma chi è interessato può fare riferimento all'articolo già menzionato sulla Wiki di Apache Ignite: .
Per ogni registrazione logica spesso ci sono più registrazioni fisiche. Ad esempio, un'operazione di put nella cache coinvolge più pagine nella memoria pagina (la pagina con i dati stessi, le pagine con gli indici, le pagine con le free-list). In alcuni test sintetici, ho riscontrato che le registrazioni fisiche occupavano fino al 90% dello spazio del file WAL. Queste registrazioni sono necessarie solo per un breve periodo (di default, l'intervallo tra i checkpoint è di 3 minuti). Sarebbe logico eliminare questi dati dopo la perdita della loro rilevanza. Questo è esattamente ciò che fa il meccanismo di WAL compaction: rimuove le registrazioni fisiche e comprime le restanti registrazioni logiche usando zip, riducendo in modo significativo la dimensione del file (a volte anche di decine di volte).
Il WAL fisico è costituito da più segmenti (di default 10) di dimensione fissa (di default 64 MB), che vengono sovrascritti in modo circolare. Una volta che il segmento attuale è pieno, viene assegnato il segmento successivo e il segmento pieno viene copiato in archivio da un flusso separato. La compattazione del WAL lavora già con i segmenti archiviati. Inoltre, da un flusso separato, monitora l'esecuzione del checkpoint e inizia la compressione dei segmenti archiviati, per i quali le registrazioni fisiche non sono più necessarie.

Impatto sulle prestazioni
Poiché la compattazione del WAL funziona in un flusso separato, non ci dovrebbe essere un impatto diretto sulle operazioni in corso. Tuttavia, fornisce comunque un carico di lavoro aggiuntivo sulla CPU (compressione) e sul disco (lettura di ogni segmento WAL dall'archivio e scrittura dei segmenti compressi), quindi se il sistema opera al limite delle capacità, ciò porterà anche a una degradazione delle prestazioni.
Come attivare e configurare
È possibile attivare la compattazione del WAL utilizzando la proprietà WalCompactionEnabled in DataStorageConfiguration (DataStorageConfiguration.setWalCompactionEnabled(true)). Inoltre, con il metodo DataStorageConfiguration.setWalCompactionLevel() è possibile impostare il livello di compressione, se il valore predefinito (BEST_SPEED) non è soddisfacente.
Compressione dello snapshot della pagina WAL
Come funziona
In precedenza abbiamo già chiarito che le registrazioni WAL si dividono in logiche e fisiche. Per ogni modifica a ciascuna pagina nella memoria di pagina viene creata una registrazione fisica WAL. Le registrazioni fisiche, a loro volta, si suddividono in due sottotipi: la registrazione dello snapshot della pagina e la registrazione delta. Ogni volta che modifichiamo qualcosa su una pagina e la portiamo da uno stato pulito a uno sporco, viene salvata una copia completa di questa pagina nel WAL (registrazione dello snapshot della pagina). Anche se abbiamo modificato solo un byte, nel WAL verrà salvata una registrazione di dimensioni leggermente superiori a quelle della pagina. Se modifichiamo qualcosa su una pagina già sporca, nel WAL si genera una registrazione delta, che riflette solo le modifiche rispetto allo stato precedente della pagina, ma non l'intera pagina. Poiché il reset dello stato delle pagine da sporco a pulito avviene durante il checkpoint, subito dopo l'inizio del checkpoint quasi tutte le registrazioni fisiche consisteranno solo in snapshot delle pagine (dato che tutte le pagine sono pulite subito dopo l'inizio del checkpoint). Poi, man mano che ci si avvicina al checkpoint successivo, la parte di registrazioni delta inizia a crescere e poi si azzera all'inizio del checkpoint successivo. Le misurazioni in alcuni test sintetici hanno mostrato che la parte degli snapshot delle pagine nel volume totale delle registrazioni fisiche raggiunge il 90%.
L'idea della compressione delle istantanee delle pagine WAL consiste nel comprimere le istantanee delle pagine utilizzando uno strumento di compressione delle pagine già esistente (vedi compressione delle pagine su disco). In questo caso, le registrazioni WAL vengono salvate in modo sequenziale in modalità append-only e non è necessaria l'associazione delle registrazioni ai confini dei blocchi del file system. Pertanto, a differenza del meccanismo di compressione delle pagine su disco, qui non abbiamo affatto bisogno di file sparsi; di conseguenza, questo meccanismo funzionerà non solo su sistemi operativi Linux. Inoltre, non è più importante quanto siamo riusciti a comprimere una pagina. Anche se liberiamo 1 byte, è già un risultato positivo e possiamo salvare i dati compressi nel WAL, a differenza della compressione delle pagine su disco, dove salviamo la pagina compressa solo se liberiamo più di 1 blocco del file system.
Le pagine sono dati facilmente comprimibili, la loro percentuale nel volume totale del WAL è molto alta; pertanto, mantenendo il formato del file WAL, possiamo ottenere una significativa riduzione della sua dimensione. La compressione delle registrazioni logiche richiederebbe invece una modifica del formato e la perdita della compatibilità, ad esempio per i consumatori esterni a cui potrebbero interessare le registrazioni logiche, senza portare a una riduzione significativa del volume del file.
Come per la compressione delle pagine del disco, possono essere utilizzati algoritmi di compressione ZSTD, LZ4, Snappy anche per la compressione degli snapshot delle pagine WAL, così come la modalità SKIP_GARBAGE.
Impatto sulle prestazioni
Come è facile notare, l'abilitazione diretta della compressione degli snapshot delle pagine WAL influisce solo sui flussi che scrivono dati nella memoria a pagina, cioè su quelli flussi che modificano i dati nei cache. La lettura delle registrazioni fisiche WAL avviene solo una volta, al momento dell'avvio del nodo dopo un crash (e solo nel caso di un crash durante il checkpoint).
I flussi che modificano i dati sono influenzati nel seguente modo: otteniamo un effetto negativo (CPU) a causa della necessità di comprimere la pagina ogni volta prima di scriverla su disco e un effetto positivo (disk IO) grazie alla riduzione della quantità di dati scritti. Di conseguenza, qui tutto è semplice: se la performance del sistema è limitata dalla CPU, si ottiene una leggera degradazione, se dal disco — un miglioramento.
Indirectly, la riduzione della dimensione del WAL influisce anche (positivamente) sui flussi che archiviano i segmenti WAL e sui flussi di compattazione del WAL.
I test di prestazione reali nel nostro ambiente su dati sintetici hanno mostrato un lieve aumento (il throughput è aumentato del 10%-15%, la latenza si è ridotta del 10%-15%).
Come attivare e configurare
Versione minima di Apache Ignite: 2.8. L'attivazione e la configurazione vengono eseguite nel seguente modo:
- Nel class-path deve essere presente il modulo ignite-compression. Di default, si trova nella distribuzione di Apache Ignite nella directory libs/optional e non è incluso nel class-path. È possibile semplicemente spostare la directory di un livello verso l'alto nella cartella libs, e quindi, avviando tramite ignite.sh, sarà automaticamente incluso.
- La persistenza deve essere attivata (si attiva tramite
DataRegionConfiguration.setPersistenceEnabled(true)). - Deve essere impostata la modalità di compressione utilizzando il metodo
DataStorageConfiguration.setWalPageCompression(), per default la compressione è disabilitata (modalità DISABLED). - Facoltativamente, puoi impostare il grado di compressione utilizzando il metodo
DataStorageConfiguration.setWalPageCompression(), valori consentiti per ciascuna modalità possono essere trovati nella javadoc del metodo.
Conclusione
I meccanismi di compressione dei dati in Apache Ignite possono essere utilizzati in modo indipendente l'uno dall'altro, ma è consentito anche qualsiasi loro combinazione. Comprendere i principi del loro funzionamento permetterà di stabilire quanto siano adatti alle vostre esigenze nel vostro ambiente e quali compromessi dovrete accettare nel loro utilizzo. La compressione delle pagine disco è progettata per comprimere lo storage principale e può fornire un grado medio di compressione. La compressione dello snapshot delle pagine WAL darà un grado medio di compressione già ai file WAL, aumentando probabilmente anche le prestazioni. La compattazione WAL non influirà positivamente sulle prestazioni, ma ridurrà al massimo la dimensione dei file WAL rimuovendo le registrazioni fisiche.
Fonte: habr.com
