Eine eigene Datenbank erstellen
(nan.fyi)- Erläutert die Grundprinzipien und den Implementierungsprozess einer Key-Value-Datenbank
- Beginnt mit der Dateisystem-basierten dauerhaften Speicherung und Suchweise
- Zeigt Ineffizienzen bei den Operationen Einfügen, Aktualisieren und Löschen auf
- Entwickelt sich schrittweise weiter zu effizienteren Strukturen wie Append-Only-Dateien, Indexen, Sortierung und Segment-Compaction
- Führt schließlich zu modernen Strukturen wie der LSM-Tree und wird in realen Systemen im großen Maßstab eingesetzt
Einleitung: Der Start beim Bau einer eigenen Datenbank
Wenn man annimmt, dass es den Datenbankbegriff nicht gibt, wird schrittweise untersucht, wie man eine eigene Datenbank erstellen kann.
Wir betrachten den Prozess zur direkten Implementierung der einfachsten Form einer Key-Value-Datenbank.
Grundidee: Permanente Speicherung mit Dateien
- Zweck einer Datenbank ist es, Daten dauerhaft zu speichern und sie später effizient zu durchsuchen
- Die häufigste Methode ist, jedes Key-Value-Paar mit Hilfe des Dateisystems in eine Datei zu schreiben
- Beim Speichern wird beispielsweise der Inhalt mit
$ db set 1 "Lorem ipsum"am Ende der Datei angehängt - Zur Suche wird jedes Key-Value-Paar in der Datei sequentiell geprüft, bis der gewünschte Key gefunden wird
- Bei einer Aktualisierung wird der Wert des entsprechenden Schlüssels direkt in der Datei ersetzt, bei einer Löschung wird der Datensatz aus der Datei entfernt
Problem: Ineffiziente In-Place-Änderung
- Direktes Ändern und Löschen in Dateien ist äußerst ineffizient
- Eine Datei ist ein einfacher Byte-Stream, daher ist das Ändern von Daten in der Mitte mit dem Verschieben aller nachfolgenden Daten verbunden
- Ändert man beispielsweise einen bestimmten Datensatz auf einen neuen Wert mit anderer Länge, müssen alle folgenden Inhalte verschoben werden
- Je mehr Daten vorhanden sind, desto stärker wirken sich diese Muster auf Tempo und Effizienz aus
Verbesserung 1: Append-Only-Dateistruktur
- Bei Updates und Löschungen werden bestehende Daten unverändert gelassen und nur neue Datensätze am Dateiende angehängt
- Bei jeder Änderung wird ein neuer Datensatz hinzugefügt und der Suchalgorithmus so angepasst, dass der zuletzt geschriebene Wert eines Keys gelesen wird
- Löschungen werden durch einen speziellen „Tombstone“-Wert (z. B.
null) markiert - Vorteil: Effizientes Schreiben ohne Verschieben bestehender Daten
- Nachteil: Die Datei wächst durch Duplikate und Löschmarkierungen zunehmend
- Die Suchgeschwindigkeit bleibt jedoch langsam, da ein kompletter Durchlauf nötig bleibt
Verbesserung 2: Dateigrößenverwaltung und Compaction
- Um das unbegrenzte Wachstum von Dateien zu verhindern, wird bei Erreichen einer bestimmten Größe auf eine neue Datei (Segment) gewechselt, und im vorherigen Segment werden überflüssige Daten (Duplikate, gelöschte Daten) im Rahmen der Compaction entfernt, um die Größe zu reduzieren
- Bei der Compaction bleiben nur wirklich benötigte Daten erhalten; veraltete Datensätze oder ausschließlich Löschmarkierungen werden entfernt
- Damit entsteht eine Verwaltung mit mehreren Segmentdateien, die bei Bedarf zusammengeführt (merge) werden kann
Verbesserung 3: Schnelle Suche per Index
- Ein In-Memory-Hash-Table-Index mit Dateioffsets je Key wird aufgebaut, um schnelle Suchen zu ermöglichen
- Bei der Suche wird zuerst der Index konsultiert und dann direkt die entsprechende Position in der Datei gelesen
- Trade-off: Die Suchgeschwindigkeit steigt, aber da der Index im Speicher liegt, stößt man bei vielen Keys an Speichergrenzen
- Durch die Indexpflege verschlechtert sich die Schreibleistung etwas
Verbesserung 4: Sortierte String-Tabellen und Sparse Index
- Wenn Daten immer sortiert nach Key gespeichert werden, ist bei Bereichsabfragen (Range Queries) eine hohe Effizienz möglich
- Durch die Sortierung reicht es, den Index nur für einen Teil der Keys (Sparse Index) statt für alle Keys zu speichern
- Durch die Anpassung der Indexdichte kann man den Kompromiss zwischen Speicherverbrauch und Suchgeschwindigkeit steuern
Implementierung: Kombination aus In-Memory und On-Disk plus Persistenzsicherung
- Neue Daten werden zunächst in einer sortierten In-Memory-Liste abgelegt und zusätzlich in einer Append-Only-Datei geschrieben, die als Backup dient
- Wenn die In-Memory-Liste wächst, wird sie als sortierte On-Disk-Datei (SST) gespeichert und der Index aktualisiert
- Bei der Abfrage wird zuerst der Speicher geprüft; fehlt der Key dort, wird mit dem Index auf der Festplatte gesucht
- On-Disk-Dateien werden als immutable verwaltet; Updates und Löschungen werden ebenfalls durch das Anhängen neuer Daten abgewickelt
- Nicht mehr benötigte Duplikate und gelöschte Daten werden regelmäßig im Rahmen der Compaction entfernt, um die Dateigröße zu kontrollieren
Entstehung der LSM-Tree
- Die beschriebenen Weiterentwicklungen werden heute weit verbreitet unter dem Namen LSM-Tree genutzt
- Die Kombination aus In-Memory-Memtable und On-Disk-SST (Sorted String Table) ist schnell und für große Umgebungen geeignet
- In großen Key-Value-Datenbanksystemen wie LevelDB und Amazon DynamoDB ist es die zentrale Datenstruktur
- Es gibt Nachteile und Unterschiede zu anderen Strukturen (z. B. B-Tree-basierte relationale DBs), doch im Hochlast- und Skalierungsszenario zeigt es hervorragende Performance
Fazit
- Aus einem einfachen Dateibasierten Ansatz werden schrittweise bessere Datenbankentwürfe durch Append-Only, Index, Sortierung, Segment-Compaction und die Kombination aus In-Memory und On-Disk
- Durch den eigenen Implementierungsprozess lässt sich das interne Verhalten und die strukturelle Logik einer Datenbank direkt nachvollziehen
- Die LSM-Tree-Architektur übernimmt in modernen großskaligen Datensystemen eine Standardrolle
- In Zukunft bleibt Raum, weitere Ansätze wie die B-Tree-Struktur relationaler Datenbanken zu untersuchen
1 Kommentare
Hacker News Kommentare
flush-Beispiel im Recap-Teil (das zweite), dort ist ebenfalls formuliert, dass in sortiertem Zustand in die Datei geschrieben wird.