- Wenn in serverlosen und Edge-Umgebungen mehrere SQLite-Instanzen parallel laufen, erhöht synchrones I/O-Warten die Tail Latency; Forschende aus Helsinki und Cambridge haben untersucht, wie sich dies durch asynchrones I/O und Storage-Disaggregation verringern lässt
- Linux io_uring ermöglicht es Anwendungen über Submission- und Completion-Queues, während I/O-Anfragen weiter andere Aufgaben auszuführen, und bildet damit eine Grundlage, um Thread-Blocking zu reduzieren
- Wenn SQLite während der Ausführung von
sqlite3_step()benötigte B-Tree-Seiten nicht im Cache findet, liest es die Daten per synchronem I/O wie POSIXread()von der Festplatte, wodurch der Thread bis zum Abschluss des I/O anhält - Die Forschenden haben nicht nur POSIX-Aufrufe ersetzt, sondern im Rust-basierten Rewrite-Projekt Limbo VM und BTree an ein asynchrones Ausführungsmodell angepasst
- In Benchmarks sank die p999-Tail-Latenz um bis zu 100-fach, während p90 und p99 nahezu identisch mit SQLite waren; die Bewertung mit mehreren Readern/Writern bleibt eine Aufgabe für die Zukunft
Forschung zur Beschleunigung von SQLite
- Forschende der University of Helsinki und aus Cambridge behandeln in „Serverless Runtime / Database Co-Design With Asynchronous I/O“, wie sich asynchrones I/O und Storage-Disaggregation auf SQLite anwenden lassen
- Dieses Paper wurde zur Grundlage von Limbo, einem Rust-basierten Rewrite-Projekt von SQLite
- Da es sich um ein Workshop-Paper handelt, ist es kurz; der Fokus liegt auf Serverless und Edge Computing
- Der Kernpunkt: Auch wenn SQLite selbst bereits schnell ist, lässt sich die Tail Latency in Multi-Tenant-Umgebungen durch eine Änderung des Ausführungsmodells weiter senken
Wie io_uring I/O-Wartezeiten reduziert
- io_uring im Linux-Kernel stellt eine Schnittstelle für asynchrones I/O bereit
- Der Name stammt von den Ringpuffern, die von User Space und Kernel Space gemeinsam genutzt werden; dadurch sinkt der Overhead durch Pufferkopien zwischen beiden Bereichen
- Eine Anwendung kann nach dem Einreichen einer I/O-Anfrage parallel andere Arbeit erledigen, bis das Betriebssystem den Abschluss meldet
- Der Ablauf ist wie folgt
- Mit dem Systemaufruf
io_uring_setup()werden zwei Speicherbereiche eingerichtet: Submission Queue und Completion Queue - Die Anwendung legt I/O-Anfragen in die Submission Queue und signalisiert dem Betriebssystem mit
io_uring_enter(), die Verarbeitung zu starten - Anders als
read()undwrite()blockiert dies den Thread nicht, sondern gibt die Kontrolle an den User Space zurück - Die Anwendung erledigt andere Aufgaben und pollt regelmäßig die Completion Queue, um den Abschluss des I/O zu prüfen
- Mit dem Systemaufruf
Der synchrone I/O-Flaschenhals bei der SQLite-Query-Ausführung
- Eine SQLite-Anwendung öffnet mit
sqlite3_open()eine Datenbankdatei; dabei werden Low-Level-OS-I/O-Aufrufe wie POSIXopenausgeführt sqlite3_prepare()wandelt SQL-Anweisungen wieSELECTundINSERTin eine Sequenz von Bytecode-Instruktionen umsqlite3_step()führt die Bytecode-Instruktionen aus, bis eine von der Query zu lesende Zeile erzeugt wurde oder die Ausführung beendet ist- Wenn eine lesbare Zeile vorhanden ist, wird
SQLITE_ROWzurückgegeben - Wenn die Anweisung abgeschlossen ist, wird
SQLITE_DONEzurückgegeben
- Wenn eine lesbare Zeile vorhanden ist, wird
- Während der Ausführung wird der Backend-Pager aufgerufen und der B-Tree durchlaufen, der Tabellen und Zeilen repräsentiert
- Wenn eine benötigte B-Tree-Seite nicht im SQLite-Seitencache liegt, kommt es zu einem Festplattenzugriff
- SQLite liest den Seiteninhalt per synchronem I/O wie POSIX
readvon der Festplatte in den Speicher - Währenddessen blockiert
sqlite3_step()den Kernel-Thread - Um während der I/O-Wartezeit parallel arbeiten zu können, muss die Anwendung mehr Threads verwenden
- SQLite liest den Seiteninhalt per synchronem I/O wie POSIX
Warum SQL in Serverless- und Edge-Umgebungen eingebettet werden soll
- Wenn Serverless Computing am Edge ausgeführt wird und sich die Datenbank in einer Cloud-Umgebung befindet, entstehen Netzwerk-Roundtrip-Kosten zwischen Serverless-Funktion und Cloud
- Eine Möglichkeit ist, Daten am Edge mitzuplatzieren; als besserer Ansatz wird jedoch genannt, die Datenbank direkt in die Edge-Runtime einzubetten
- Cloudflare Workers erreichen bereits eine solche Form, stellen jedoch ein KV-Interface bereit
- KV passt nicht zu allen Problembereichen
- Werden tabellarische Daten auf ein KV-Modell abgebildet, verschlechtert sich die Developer Experience
- Außerdem entstehen Kosten für Serialisierung und Deserialisierung
- SQL kann besser geeignet sein, und SQLite kann als Embedded Database direkt in eine Serverless-Runtime integriert werden
Warum sich SQLite nicht einfach auf io_uring umstellen lässt
- SQLite verwendet synchrones I/O auf Basis der traditionellen POSIX-Aufrufe
read()undwrite() - Für kleine Anwendungen ist das nicht unbedingt ein großes Problem, kann aber zum Flaschenhals werden, wenn auf einem Server Hunderte SQLite-Datenbanken laufen
- In Umgebungen, in denen die Auslastung der Serverressourcen maximiert werden muss, wirkt synchrones I/O als Einschränkung
- SQLite hat Probleme mit Nebenläufigkeit und Multi-Tenancy
- Da I/O synchron ist und blockiert, konkurrieren Anwendungen auf derselben Maschine um Ressourcen
- Dadurch steigt die Latenz
- POSIX-I/O-Aufrufe lassen sich nicht einfach durch io_uring ersetzen
- Anwendungen, die blockierendes I/O verwenden, müssen für das asynchrone I/O-Modell von io_uring neu entworfen werden
- Die SQLite-Bibliothek muss in der Lage sein, während laufendem I/O die Kontrolle an die Anwendung zurückzugeben
- Statt nur einzelne Aufrufe in SQLite zu ändern, wählten die Forschenden den Ansatz, SQLite in Rust neu zu schreiben und io_uring zu verwenden
Limbos asynchrones Ausführungsmodell
- Limbo ist ein Projekt, das SQLite in Rust neu schreibt und die Komponenten VM und BTree so verändert, dass sie asynchrones I/O unterstützen
- Synchrone Bytecode-Instruktionen werden durch asynchrone Gegenstücke ersetzt
- Beispielsweise bewegt die Instruktion
Nextden Cursor weiter und holt bei Bedarf die nächste Seite- In der bisherigen synchronen Version wird bei auftretendem Festplatten-I/O blockiert, bis die Seite gelesen und an den Aufrufer zurückgegeben wurde
- In der asynchronen Version kehrt
NextAsyncsofort zurück, nachdem es eingereicht wurde - Der Aufrufer kann anschließend blockieren oder andere Arbeit ausführen
- Asynchrones I/O beseitigt Blocking und verbessert die Nebenläufigkeit
- Um die Ressourcenauslastung weiter zu erhöhen, wird außerdem Storage-Disaggregation vorgeschlagen, also die Trennung von Query Engine und Storage Engine
- Als zugehörige Erklärung wird Disaggregated Storage - a brief introduction verlinkt
Benchmark-Ergebnisse und offene Fragen
- Der Benchmark simuliert eine Multi-Tenant-Serverless-Runtime
- Jeder Tenant besitzt dabei seine eigene eingebettete Datenbank
- Die Anzahl der Tenants wird in Zehnerschritten von 1 bis 100 variiert
- SQLite verwendet für jeden Tenant einen eigenen Thread; gemessen wird, indem in jedem Thread Queries ausgeführt werden
- Die ausgeführte Query lautet
SELECT * FROM users LIMIT 100und wird 1000-mal wiederholt - Limbo führt dasselbe Experiment durch, verwendet dabei jedoch Rust-Coroutinen
- Im Ergebnis sinkt die Tail Latency bei p999 um bis zu 100-fach
- Die SQLite-Query-Latenz verschlechterte sich mit zunehmender Thread-Zahl nicht graduell
- Die Arbeit ist noch im Gange, und im Paper bleiben einige offene Fragen
- Future Work behandelt weitere Benchmarks mit mehreren Readern und Writern
- Der Vorteil zeigt sich erst ab p999 deutlich
- Die Performance bei p90 und p99 ist nahezu identisch mit SQLite
- Der Limbo-Code ist als Open Source verfügbar
- Limbo ist inzwischen ein offizielles Turso-Projekt; auch ein Einführungsbeitrag wurde veröffentlicht
Noch keine Kommentare.