Bei der Arbeit mit großen Datenmengen kann manchmal das Problem des fehlenden Speicherplatzes auf den Festplatten akut werden. Eine der Möglichkeiten zur Lösung dieses Problems ist die Kompression, durch die man auf derselben Hardware eine Erhöhung des Speicherplatzes erreichen kann. In diesem Artikel werden wir untersuchen, wie die Datenkompression in Apache Ignite funktioniert. Es werden nur die im Produkt implementierten Methoden zur Kompression auf der Festplatte beschrieben. Andere Methoden der Datenkompression (über das Netzwerk, im Speicher), ob implementiert oder nicht, bleiben unberücksichtigt.
Also, im eingeschalteten Persistenzmodus beginnt Ignite infolge von Datenänderungen in den Caches, auf die Festplatte zu schreiben:
- Inhalt der Caches
- Write Ahead Log (WAL)
Für die Kompression von WAL existiert schon seit einiger Zeit ein Mechanismus, der als WAL-Kompression bezeichnet wird. In der kürzlich veröffentlichten Version Apache Ignite 2.8 wurden zwei weitere Mechanismen eingeführt, die es ermöglichen, Daten auf der Festplatte zu komprimieren: die Disk Page Compression zur Kompression des Cache-Inhalts und die WAL Page Snapshot Compression zur Kompression bestimmter WAL-Einträge. Weitere Details zu allen drei Mechanismen finden Sie weiter unten.
Disk Page Compression
Die DANE-Spezifikation ist in
Zunächst eine kurze Erklärung, wie Ignite Daten speichert. Zur Speicherung wird eine seitenbasierte Speicherverwaltung verwendet. Die Seitengröße wird beim Start des Knotens festgelegt und kann in späteren Phasen nicht mehr geändert werden; zudem muss die Seitengröße eine Potenz von zwei sein und ein Vielfaches der Blockgröße des Dateisystems darstellen. Seiten werden bei Bedarf von der Festplatte in den RAM geladen, die Datenmenge auf der Festplatte kann den bereitgestellten RAM übersteigen. Im Falle eines Speicherplatzmangels im RAM zum Laden von Seiten von der Festplatte werden alte, nicht mehr verwendete Seiten aus dem RAM verdrängt.
Auf der Festplatte werden die Daten folgendermaßen gespeichert: Für jede Partition jeder Cache-Gruppe wird eine separate Datei erstellt, in dieser Datei folgen die Seiten der Reihe nach in aufsteigender Indexp Reihenfolge. Die vollständige Seiten-ID enthält die ID der Cache-Gruppe, die Partitionsnummer und den Seitenindex in der Datei. Damit können wir anhand der vollständigen Seiten-ID eindeutig die Datei und den Offset in der Datei für jede Seite bestimmen. Über die Struktur der seitenbasierten Speicherverwaltung kann man in einem Artikel im Apache Ignite Wiki mehr erfahren: .
Der Mechanismus der Seitenkompression, wie aus dem Namen ersichtlich, funktioniert auf Seitenebene. Bei Aktivierung dieses Mechanismus erfolgt der Umgang mit Daten im RAM so, wie sie sind, ohne jegliche Kompression. Doch beim Speichern der Seiten aus dem RAM auf die Festplatte wird eine Kompression durchgeführt.
Es ist jedoch nicht ausreichend, jede Seite einzeln zu komprimieren; es muss auch eine Möglichkeit gefunden werden, die Größe der endgültigen Datendateien zu reduzieren. Wenn die Seitengröße nicht mehr fix ist, können wir die Seiten nicht mehr nacheinander in eine Datei schreiben, da dies zu einer Reihe von Problemen führen kann:
- Wir können mit Hilfe des Seitenindex den Offset, an dem sie in der Datei liegt, nicht berechnen.
- Unklar ist, was mit Seiten zu tun ist, die sich nicht am Ende der Datei befinden und deren Größe sich ändert. Wenn die Seitengröße verringert wird, geht der Platz, den sie freisetzt, verloren. Wenn die Seitengröße zunimmt, muss ein neuer Platz in der Datei für sie gefunden werden.
- Wenn sich eine Seite um eine Anzahl von Bytes verschiebt, die nicht mit der Blockgröße des Dateisystems übereinstimmt, ist es notwendig, beim Lesen oder Schreiben einen zusätzlichen Block des Dateisystems anzusprechen, was zu einer Verschlechterung der Leistung führen kann.
Um diese Probleme nicht selbst auf ihrem Niveau zu lösen, nutzt die Seitenkompression in Apache Ignite einen Mechanismus des Dateisystems namens Sparse Dateien. Eine Sparse Datei ist eine Datei, in der einige mit Nullen gefüllte Bereiche als „Löcher“ gekennzeichnet werden können. In diesem Fall werden keine Blöcke des Dateisystems zur Speicherung dieser Löcher zugewiesen, was zu einer Platzersparnis auf der Festplatte führt.
Es ist logisch, dass zur Freigabe eines Blocks des Dateisystems die Größe des Lochs größer oder gleich der Blockgröße des Dateisystems sein muss, was eine zusätzliche Einschränkung für die Seitengröße in Apache Ignite bedeutet: Damit die Kompression einen gewissen Effekt hat, muss die Seitengröße unbedingt größer als die Blockgröße des Dateisystems sein. Wenn die Seitengröße der Blockgröße entspricht, können wir nie einen Block freigeben, da zur Freigabe eines einzelnen Blocks die komprimierte Seite 0 Byte einnehmen müsste. Wenn jedoch die Seitengröße der Größe von zwei oder vier Blöcken entspricht, können wir mindestens einen Block freigeben, wenn unsere Seite sich mindestens auf 50 % oder 75 % ihrer ursprünglichen Größe komprimiert.
Somit lautet die endgültige Beschreibung der Funktionsweise des Mechanismus: Beim Speichern einer Seite auf der Festplatte wird versucht, die Seite zu komprimieren. Wenn die Größe der komprimierten Seite es erlaubt, einen oder mehrere Blöcke im Dateisystem freizugeben, wird die Seite komprimiert gespeichert, und an der Stelle der freigegebenen Blöcke wird ein „Loch“ gebohrt (Systemaufruf mit dem Flag „punch hole“). Wenn die Größe der komprimierten Seite das Freigeben von Blöcken nicht zulässt, wird die Seite unverändert im unkomprimierten Format gespeichert. Alle Seitenoffsets werden wie ohne Kompression behandelt, indem der Seitenindex mit der Seitengröße multipliziert wird. Es ist keine Neupositionierung der Seiten erforderlich. Seitenoffsets ohne Kompression fallen ebenfalls an die Grenzen der Dateisystemblöcke. fallocate() mit dem Flag „punch hole“). Wenn die Größe der komprimierten Seite das Freigeben von Blöcken nicht zulässt, wird die Seite unverändert im unkomprimierten Format gespeichert. Alle Seitenoffsets werden wie ohne Kompression behandelt, indem der Seitenindex mit der Seitengröße multipliziert wird. Es ist keine Neupositionierung der Seiten erforderlich. Seitenoffsets ohne Kompression fallen ebenfalls an die Grenzen der Dateisystemblöcke.

In der aktuellen Implementierung kann Ignite nur unter Linux mit Sparse-Dateien arbeiten, weshalb die Disk Page Compression nur bei der Verwendung von Ignite auf diesem Betriebssystem aktiviert werden kann.
Die für die Disk Page Compression verwendbaren Kompressionsalgorithmen sind: ZSTD, LZ4, Snappy. Außerdem gibt es einen Betriebsmodus (SKIP_GARBAGE), bei dem nur der in der Seite nicht genutzte Platz ohne Anwendung von Kompression auf den verbleibenden Daten herausgeworfen wird, was die CPU-Belastung im Vergleich zu den zuvor genannten Algorithmen verringert.
Einfluss auf die Leistung
Leider habe ich keine tatsächlichen Leistungsmesungen auf realen Testständen durchgeführt, da wir nicht vorhaben, diesen Mechanismus in der Produktion zu verwenden, aber wir können theoretisch darüber nachdenken, wo wir verlieren und wo wir gewinnen werden.
Dafür müssen wir uns daran erinnern, wie das Lesen und Schreiben von Seiten abläuft:
- Bei der Leseoperation wird zunächst in RAM nach ihr gesucht. Wenn die Suche erfolglos ist, wird die Seite von der Festplatte vom gleichen Thread, der das Lesen durchführt, in den RAM geladen.
- Bei der Schreiboperation wird die Seite im RAM als schmutzig markiert, wobei die physische Speicherung der Seite auf der Festplatte im selben Thread, der das Schreiben durchführt, nicht sofort erfolgt. Alle schmutzigen Seiten werden später in einem Prozess beim Checkpointing von separaten Threads auf die Festplatte gespeichert.
Somit hat der Einfluss auf Leseoperationen:
- Positiv (Disk IO), da die Anzahl der gelesenen Dateisystemblöcke reduziert wird.
- Negativ (CPU) aufgrund der zusätzlichen Belastung, die das Betriebssystem für die Arbeit mit Sparse-Dateien benötigt. Es ist auch möglich, dass hier implizit zusätzliche IO-Operationen auftreten, um eine komplexere Struktur einer Sparse-Datei zu speichern (mit den Details zur Funktionsweise von Sparse-Dateien kenne ich mich leider nicht aus).
- Negativ (CPU) aufgrund der Notwendigkeit der Dekompression von Seiten.
- Es gibt keinen Einfluss auf Schreiboperationen.
- Einfluss auf den Checkpoint-Prozess (hier ist alles wie bei Lesevorgängen):
- Positiv (Disk-IO) aufgrund der Verringerung der Anzahl der geschriebenen Blöcke im Dateisystem.
- Negativ (CPU, möglicherweise Disk-IO) aufgrund der Arbeit mit Sparse-Dateien.
- Negativ (CPU) aufgrund der Notwendigkeit, Seiten zu komprimieren.
Welcher Waagschale wird überwiegen? Das hängt stark von der Umgebung ab, aber ich tendiere dazu zu glauben, dass die Disk-Seitenkompression in den meisten Systemen eher zu einer Leistungsminderung führen wird. Zumal Tests mit anderen DBMS, die einen ähnlichen Ansatz mit Sparse-Dateien verwenden, zeigen, dass die Leistung bei aktivierter Kompression abnimmt.
Wie man es aktiviert und konfiguriert
Wie bereits erwähnt, die minimale Version von Apache Ignite, die die Disk-Seitenkompression unterstützt: 2.8, und sie wird nur auf dem Betriebssystem Linux unterstützt. Die Aktivierung und Konfiguration erfolgt wie folgt:
- Im Class-Path muss das Modul ignite-compression vorhanden sein. Standardmäßig befindet es sich im Apache Ignite-Verzeichnis libs/optional und wird nicht in den Class-Path aufgenommen. Man kann das Verzeichnis einfach in das libs-Verzeichnis auf einer Ebene höher verschieben, und beim Start über ignite.sh wird es automatisch berücksichtigt.
- Persistence muss aktiviert sein (wird über
DataRegionConfiguration.setPersistenceEnabled(true)) aktiviert.. - Die Seitengröße muss größer als die Blockgröße des Dateisystems sein (kann über
DataStorageConfiguration.setPageSize() festgelegt werden.). - Für jeden Cache, dessen Daten komprimiert werden sollen, muss in der Konfiguration die Komprimierungsmethode und (optional) der Komprimierungsgrad konfiguriert werden (Methoden
CacheConfiguration.setDiskPageCompression(), CacheConfiguration.setDiskPageCompressionLevel()).
WAL-Kompaktierung
Die DANE-Spezifikation ist in
Was ist WAL und warum wird es benötigt? Kurz gesagt: es ist ein Protokoll, in das alle Ereignisse eingehen, die letztendlich den Seiten-Speicher verändern. Es wird in erster Linie benötigt, um eine Wiederherstellung im Falle eines Ausfalls zu ermöglichen. Jede Operation muss, bevor sie die Kontrolle an den Benutzer zurückgibt, zunächst ein Ereignis in WAL protokollieren, um im Falle eines Ausfalls die Möglichkeit zu haben, das Protokoll wieder abzuspielen und alle Operationen wiederherzustellen, für die der Benutzer eine positive Rückmeldung erhalten hat, selbst wenn diese Operationen noch nicht im Seiten-Speicher auf der Festplatte reflektiert wurden (bereits erwähnt, dass die tatsächliche Aufzeichnung im Seiten-Speicher im Prozess erfolgt, der als „Checkpoint“ bezeichnet wird, mit einer gewissen Verzögerung durch separate Threads).
Die Einträge im WAL werden in logische und physische unterteilt. Logische sind die eigentlichen Schlüssel und Werte. Physische spiegeln die Änderungen der Seiten im Seiten-Speicher wider. Wenn logische Einträge auch für andere Fälle nützlich sein können, sind physische Einträge nur zur Wiederherstellung im Falle eines Ausfalls erforderlich und es werden nur Einträge ab dem letzten erfolgreichen Checkpoint benötigt. Wir werden hier nicht ins Detail gehen und erklären, warum das genau so funktioniert, aber Interessierte können auf den bereits erwähnten Artikel im Apache Ignite Wiki zurückgreifen: .
Auf einen logischen Eintrag entfallen oft mehrere physische Einträge. Das bedeutet, dass beispielsweise eine Put-Operation im Cache mehrere Seiten im Seiten-Speicher betrifft (die Seite mit den eigentlichen Daten, Seiten mit Indizes, Seiten mit Freilisten). In einigen synthetischen Tests stellte ich fest, dass physische Einträge bis zu 90 % des Volumens der WAL-Datei einnahmen. Dabei werden sie nur für eine sehr kurze Zeit benötigt (der Standardzeitraum zwischen Checkpoints beträgt 3 Minuten). Es wäre logisch, sich von diesen Daten nach deren Verlust der Aktualität zu trennen. Genau das führt der WAL-Compaction-Mechanismus durch, indem er sich von physischen Einträgen trennt und die verbleibenden logischen Einträge mithilfe von ZIP komprimiert, wobei die Dateigröße erheblich reduziert wird (manchmal um das Zehnfache).
Physisch besteht WAL aus mehreren Segmenten (standardmäßig 10) fester Größe (standardmäßig 64MB), die im Kreis überschrieben werden. Sobald das aktuelle Segment voll ist, wird ihm das nächste Segment zugewiesen, und das gefüllte Segment wird in einem separaten Thread archiviert. Die WAL-Kompaktion arbeitet bereits mit archivierten Segmenten. Auch in einem separaten Thread verfolgt sie die Durchführung von Checkpoints und beginnt mit der Komprimierung von archivierten Segmenten, deren physische Aufzeichnungen nicht mehr benötigt werden.

Einfluss auf die Leistung
Da die WAL-Kompaktion in einem separaten Thread arbeitet, sollte sie keine direkten Auswirkungen auf die ausgeführten Operationen haben. Sie erzeugt jedoch zusätzliche Hintergrundlast auf der CPU (Komprimierung) und der Festplatte (Lesen jedes WAL-Segments aus dem Archiv und Schreiben der komprimierten Segmente). Wenn das System also am Limit arbeitet, führt dies auch zu einer Leistungsverschlechterung.
Wie man es aktiviert und konfiguriert
Die WAL-Kompaktion kann durch die Eigenschaft WalCompactionEnabled in DataStorageConfiguration (DataStorageConfiguration.setWalCompactionEnabled(true)). Darüber hinaus kann mit der Methode DataStorageConfiguration.setWalCompactionLevel() der Komprimierungsgrad festgelegt werden, wenn der Standardwert (BEST_SPEED) nicht ausreicht.
WAL-Seiten-Snapshot-Komprimierung
Die DANE-Spezifikation ist in
Früher wurde bereits festgestellt, dass WAL-Datensätze in logische und physische unterteilt sind. Bei jeder Änderung jeder Seite im Seiten-Speicher wird ein physischer WAL-Datensatz erstellt. Physische Datensätze teilen sich wiederum in zwei Unterarten: Page Snapshot Record und Delta Record. Jedes Mal, wenn wir etwas auf einer Seite ändern und sie von einem sauberen in einen schmutzigen Zustand versetzen, wird eine vollständige Kopie dieser Seite im WAL gespeichert (Page Snapshot Record — Snapshot der Seite). Selbst wenn wir nur ein Byte ändern, wird im WAL ein Datensatz gespeichert, der etwas größer als die Seitengröße ist. Wenn wir hingegen etwas auf einer bereits schmutzigen Seite ändern, wird im WAL ein Delta Record erstellt, der nur die Änderungen im Vergleich zum vorherigen Zustand der Seite widerspiegelt, jedoch nicht die gesamte Seite. Da der Zustand von Seiten von schmutzig nach sauber im Verlauf eines Checkpoints zurückgesetzt wird, werden unmittelbar nach Beginn des Checkpoints fast alle physischen Datensätze ausschließlich aus Snapshots der Seiten bestehen (da alle Seiten unmittelbar nach Beginn des Checkpoints sauber sind). Mit der Annäherung an den nächsten Checkpoint beginnt der Anteil der Delta Records zu wachsen und wird zu Beginn des nächsten Checkpoints wieder zurückgesetzt. Messungen in einigen synthetischen Tests zeigten, dass der Anteil der Snapshots der Seiten im Gesamtvolumen der physischen Datensätze bis zu 90 % erreicht.
Die Idee der WAL Page Snapshot-Kompression besteht darin, Snapshots der Seiten mithilfe eines bereits vorhandenen Werkzeugs zur Seitenkompression zu komprimieren (siehe Disk Page Compression). Dabei werden die WAL-Datensätze sequenziell im Append-Only-Modus gespeichert, und es besteht keine Notwendigkeit, die Datensätze an die Grenzen von Dateisystemblöcken zu binden. Daher benötigen wir hier, im Gegensatz zum Mechanismus der Disk Page Compression, keinerlei Sparse-Dateien, weshalb dieser Mechanismus nicht nur auf Linux-Betriebssystemen funktioniert. Darüber hinaus ist es uns egal, wie stark wir die Seite komprimieren konnten. Selbst wenn wir 1 Byte freigemacht haben, ist das bereits ein positives Ergebnis und wir können komprimierte Daten im WAL speichern, anders als bei der Disk Page Compression, bei der wir eine komprimierte Seite nur speichern, wenn wir mehr als 1 Block des Dateisystems freigemacht haben.
Seiten sind gut komprimierbare Daten, und ihr Anteil am gesamten WAL-Volumen ist sehr hoch. So können wir durch Beibehaltung des WAL-Dateiformats eine signifikante Reduzierung seiner Größe erreichen. Die Komprimierung von logischen Einträgen würde eine Änderung des Formats und den Verlust der Kompatibilität erfordern, z. B. für externe Verbraucher, die an logischen Einträgen interessiert sein könnten, ohne eine wesentliche Verringerung der Dateigröße zu bringen.
Wie bei der Kompression von Disk-Seiten können auch für die Kompression von WAL-Seiten-Snapshots die Kompressionsalgorithmen ZSTD, LZ4, Snappy sowie der SKIP_GARBAGE-Modus verwendet werden.
Einfluss auf die Leistung
Wie leicht zu erkennen ist, wirkt sich die direkte Aktivierung der Kompression von WAL-Seiten-Snapshots nur auf die Threads aus, die Daten in den Seiten-Speicher schreiben, das heißt auf jene Threads, die Daten in den Caches ändern. Das Lesen aus den WAL-physikalischen Einträgen erfolgt nur einmalig, wenn der Knoten nach einem Absturz hochgefahren wird (und nur im Fall eines Absturzes während des Checkpoints).
Auf die Threads, die Daten ändern, wirkt sich dies folgendermaßen aus: Wir haben einen negativen Effekt (CPU) aufgrund der Notwendigkeit, jede Seite vor dem Schreiben auf die Festplatte zu komprimieren, und einen positiven Effekt (Disk IO) durch die Verringerung der geschriebenen Datenmenge. Dementsprechend ist es hier einfach: wenn die Systemleistung vom CPU begrenzt wird, erfahren wir einen leichten Rückgang, wenn es vom Festplattenein-/-ausgabe abhängt – einen Anstieg.
Indirekt wirkt sich die Verringerung der WAL-Größe auch positiv auf die Threads aus, die WAL-Segmente in Archive ablegen, sowie auf die WAL-Komprimierungsthreads.
Reale Leistungstests in unserer Umgebung mit synthetischen Daten zeigten einen geringen Anstieg (der Durchsatz stieg um 10%-15%, die Latenz verringerte sich um 10%-15%).
Wie man es aktiviert und konfiguriert
Minimale Version von Apache Ignite: 2.8. Die Aktivierung und Konfiguration erfolgt wie folgt:
- Im Class-Path muss das Modul ignite-compression vorhanden sein. Standardmäßig befindet es sich im Apache Ignite-Verzeichnis libs/optional und wird nicht in den Class-Path aufgenommen. Man kann das Verzeichnis einfach in das libs-Verzeichnis auf einer Ebene höher verschieben, und beim Start über ignite.sh wird es automatisch berücksichtigt.
- Persistence muss aktiviert sein (wird über
DataRegionConfiguration.setPersistenceEnabled(true)). - Der Komprimierungsmodus muss mit der Methode festgelegt werden
DataStorageConfiguration.setWalPageCompression(), standardmäßig ist die Komprimierung deaktiviert (Modus DISABLED). - Optional kann der Komprimierungsgrad mit der Methode festgelegt werden
DataStorageConfiguration.setWalPageCompression(), zulässige Werte für jede der Modi finden Sie in der Javadoc zur Methode.
Fazit
Die betrachteten Datenkomprimierungsmechanismen in Apache Ignite können unabhängig voneinander verwendet werden, aber auch beliebige Kombinationen sind zulässig. Das Verständnis der Funktionsprinzipien ermöglicht es, zu bestimmen, wie gut sie für Ihre Anforderungen in Ihrer Umgebung geeignet sind und auf was verzichtet werden muss, wenn sie eingesetzt werden. Die Disk-Seitenkompression ist für die Komprimierung des Hauptspeichers vorgesehen und kann eine mittlere Komprimierungsrate liefern. Die WAL-Seiten-Snapshot-Kompression erzielt eine mittlere Komprimierungsrate der WAL-Dateien und könnte wahrscheinlich sogar die Leistung steigern. Die WAL-Kompression hat keinen positiven Einfluss auf die Leistung, reduziert jedoch die Größe der WAL-Dateien erheblich, indem physische Einträge gelöscht werden.
Quelle: habr.com
