- Kombiniert Datalog-Logikprogrammierung mit der Effizienz von Rust und teilt ausführlich den Entwicklungsprozess einer interaktiven Datalog-Engine, die einfach, benutzerfreundlich und zugleich leistungsorientiert ist
- In einer intuitiven CLI-Umgebung lassen sich Regeln (
rule) und Fakten (fact) in Echtzeit hinzufügen und erweitern, einschließlich Massendaten-Import, dynamischer Regelerfassung und schneller Query-Performance
- Parsing, Datenrepräsentation und Regelauswertung (Planning/Evaluation) werden Schritt für Schritt zusammen mit Rust-Code erklärt, sodass sich die konkrete Implementierung nachvollziehen lässt
- Ausgehend von einer einfachen, nicht optimierten Implementierung werden Leistung und Struktur schrittweise verbessert, wodurch sich auch fortgeschrittene Logik wie Datenparallelisierung und Join-Optimierung erlernen lässt
- Anhand programmanalytischer Beispiele auf großen Datensätzen (Nullability, Aliasing usw.) werden reale Läufe gezeigt und Erkenntnisse zu Performance- und Speicherproblemen, Query-Optimierung und der Verbesserung von Join-Plänen geteilt
Einführung: Experimente mit Datalog-Logikprogrammierung in Rust
- Während des Memorial-Day-Zeitraums fanden auf der Minnowbrook-Konferenz verschiedene Übungen und Diskussionen zu Logikprogrammierung (u. a. Datalog) statt
- Bei bestehenden Datalog-Werkzeugen (Soufflé, ctdal usw.) zeigten sich Grenzen bei praktischer Nutzung, Erweiterbarkeit und Performance, was den Bedarf an einem praxistauglichen Werkzeug deutlich machte
- Der Autor entschied sich daher, selbst in Rust einen Datalog-Interpreter (
datatoad) zu implementieren, der Einfachheit, Nutzbarkeit und Performance zugleich erfüllt
- Projektziel: große Datenmengen schnell per CLI laden, Regeln dynamisch hinzufügen und ändern, Ergebnisse in Echtzeit prüfen und zugleich gute Query-Performance sicherstellen
Grundkonzepte von Datalog
- Datalog basiert auf logischen Ausdrücken in Form von Regeln (Head :- Body) und leitet aus gegebenen Fakten und Regeln automatisch alle ableitbaren Tatsachen her
- Regeln (z. B.
tri(a, b, c) :- edge(a, b), edge(b, c), edge(a, c)) bestehen aus Variablen und Literalen
- Ein fact ist ein ohne Bedingungen wahrer Wert (z. B.
edge(1, 2) :- .)
- Stärken von Datalog: Werden Regeln hinzugefügt, wächst die Menge der ableitbaren Informationen (Monotonie), und unabhängig von der Reihenfolge von Regeln und Fakten entsteht dasselbe Ergebnis (Konvergenz)
- In Rust werden Regeln und Fakten als Strukturen wie
Atom/Rule/Term dargestellt, während Faktmengen pro Relation verwaltet werden
Entwurf der Kernstrukturen
Datenrepräsentation
- Fact wurde zunächst als
Vec<String> modelliert, Faktmengen anfangs etwa mit BTreeMap<String, Vec<Fact>> verwaltet
- Zur Optimierung großer Datenmengen wurde eine columnar orientierte Datenstruktur eingeführt, um den Alloc-Overhead zu minimieren
- FactContainer: sortierte und deduplizierte Faktmengen in einer append-only-Struktur
- FactLSM: LSM-Ansatz (Log-structured merge-tree), der mehrere Ebenen von
FactContainer verwaltet und Einfügen sowie Sortieren/Zusammenführen effizienter macht
- FactSet: verwaltet den Lebenszyklus von Fakten (neu hinzugefügt, kürzlich abgeleitet, stabilisierte Fakten), um doppelte Berechnungen und unnötigen Speicherverbrauch zu vermeiden
Regelanwendung und Inferenz
- Für jede Regel wird ein JoinPlan erzeugt, auf dessen Basis mit geeigneter Spaltenreihenfolge und Schlüsselkombination per Merge Join inferiert wird
- Merge Join: Die Schlüssels palten der Body-Atome werden sortiert; neue Fakten werden nur dann abgeleitet, wenn die Join-Schlüssel übereinstimmen, was die Performance maximiert
- Über die stable/recent/to_add-Struktur von
FactSet werden bereits abgeleitete und neue Fakten getrennt gehalten, um unnötige Neuberechnungen zu vermeiden (differentielle Auswertung)
- In der
.update()-Schleife werden alle Regeln wiederholt angewendet, bis keine neuen Fakten mehr entstehen und ein Fixpunkt erreicht ist
Parser-Implementierung
- Unterstützt eine Soufflé-ähnliche Syntax (
?var, :-, ., , usw.); Tokenizer und Parser wurden direkt in Rust geschrieben
- Tritt ein Fehler auf, wird sicher
None zurückgegeben; ein einfacher Parser passend für ein experimentelles Umfeld
Performance-Optimierung und praktische Analysebeispiele
Nullability-Analyse (Reachability)
- Auf großen Datensätzen (z. B.
httpd_df) werden Datalog-Regeln zur Verfolgung von Wertkopien und Verschiebungspfaden definiert und ihre Performance gemessen
- Je nach Schreibmuster der Regeln fallen die Performance-Unterschiede stark aus, etwa durch Variablenbindung, Spaltenreihenfolge oder das Entstehen temporärer Relationen durch Join-Pläne
- Je nach Ausgangsform der Daten und Join-Strategie können Laufzeit und Speicherverbrauch um ein Vielfaches differieren, was die Bedeutung der Query-Optimierung unmittelbar zeigt
- Mit Optimierungen wurde gegenüber bestehenden C++-basierten Werkzeugen (Graspan usw.) eine 10- bis über 80-fache Leistungssteigerung bestätigt
Aliasing-Analyse (Points-to)
- Komplexe Datalog-Muster für Alias- und Pointer-Tracking-Analysen werden implementiert; dieselben Queries wie in Arbeiten zu Graspan, Zheng-Rugina usw. werden ausgeführt
- Wiederholung (
^*), Option (^?) und Transposition (^T) innerhalb von Datalog-Regeln werden in explizite Rekursion bzw. Union erweitert
- Je nach Benennung und Wiederverwendungsdesign von Zwischenergebnissen (Relationsalias, temporäre Joins usw.) unterscheiden sich Effizienz und Ressourcenverbrauch des gesamten Query-Plans erheblich
- Werden ohne Query-Plan-Optimierung große Zwischenergebnisse erzeugt, führt das zu Leistungseinbußen und explosionsartigem Speicherverbrauch (z. B. bei der
V-Relation)
- Diskutiert wird auch ein „demand-driven“-Ansatz, der nur benötigte Ergebnisse erzeugt (Magic-Set-Transformation), sowie das Potenzial realer Query-Plan-Anpassungen und Performance-Verbesserungen
Erkenntnisse aus eigenen Experimenten mit Rust
- Der Schlüssel zur Performance einer Datalog-Engine liegt in Datenrepräsentation (columnar/LSM), differentieller Inferenz und der Optimierung des Join-Plans
- Werden Regeln nur mechanisch geschrieben, führt das leicht zu unnötigen Zwischendaten und Ressourcenverschwendung, Optimierung ist daher notwendig
- Eigene Experimente in Rust zeigen, dass sich auf realen Datensätzen Millionen bis zig Millionen Faktzeilen effizient verwalten lassen, bei gleichzeitiger Skalierbarkeit und hoher Inferenzgeschwindigkeit
- In einer CLI-Umgebung lassen sich große Datenmengen laden, Regeln in Echtzeit ergänzen und Ergebnisse bequem prüfen
- Die Rolle des Query-Optimizers, Bushy-Tree-Joins zur Nutzung von Zwischenergebnissen und die Gewohnheit, unnötige Relationen gar nicht erst zu erzeugen, liefern wichtige praktische Einsichten für das Schreiben und Betreiben von Datalog
Zukünftige Erweiterungsaufgaben
- Offene Forschungsaufgaben bleiben etwa Disk-Spilling, verteilte Skalierung über mehrere Worker/Prozesse, Streaming-Joins und benutzerdefinierte Compiler-Optimierungen
- Für große Programmanalysen, Graph- und Relationsinferenz, statische Analyse, Data-Flow-Tracking und andere Praxisfelder erscheint das Einsatzpotenzial von Rust-Datalog hoch
1 Kommentare
Hacker-News-Kommentare