2024-03-22
Abhängigkeitsverwaltung in DuckDB-Extensions
Sam Ansmink
Einleitung
Seit der Geburt von DuckDB ist eines seiner Hauptpfeiler die strikte Philosophie ohne externe Abhängigkeiten. Frei nach diesem SIGMOD-Paper von 2019 zu DuckDB: Um die Anforderung praktischer „Einbettbarkeit“ und Portabilität zu erfüllen, muss die Datenbank in jeder Umgebung laufen, in der der Host läuft. Abhängigkeiten von externen Bibliotheken (z. B. openssh) zur Compile- oder Laufzeit haben sich als problematisch erwiesen.
In diesem Blogpost behandeln wir, wie DuckDB dieser Philosophie treu bleibt, ohne DuckDB-Entwickler auf den Pfad vollständiger Abstinenz zu zwingen. Unterwegs zeigen wir praktische Beispiele, wie externe Abhängigkeiten möglich sind und wie Sie das beim Erstellen Ihrer eigenen DuckDB-Extension nutzen können.
Die Schwierigkeiten vollständiger Abstinenz
Keine externen Abhängigkeiten zu haben ist konzeptionell sehr einfach. In einem realen System mit realen Anforderungen ist das aber schwer zu erreichen. Viele Features erfordern komplexe Implementierungen von Protokollen und Algorithmen, und es gibt viele hochwertige Bibliotheken, die sie umsetzen. Für DuckDB (und die meisten anderen Systeme) gibt es im Wesentlichen drei Optionen, mit Anforderungen umzugehen, die potenzielle externe Abhängigkeiten mitbringen:
- Externen Code einbetten (Inlining)
- Die externe Abhängigkeit neu schreiben
- Die No-Dependency-Regel brechen
Die ersten beiden Optionen sind ziemlich klar: Um nicht von fremder Software abzuhängen, machen Sie sie einfach zum Teil der Codebasis. Damit ist die Unvorhersehbarkeit, von jemand anderem abzuhängen, beseitigt! DuckDB hat sowohl Inlining als auch Neuimplementierung genutzt, um Abhängigkeiten zu vermeiden. Zum Beispiel sind der Postgres-Parser und die MbedTLS-Bibliotheken in DuckDB eingebettet, während die S3-Unterstützung über eine eigene Implementierung des AWS-S3-Protokolls kommt.
Super – Problem gelöst, oder? Nun, nicht so schnell. Die meisten mit etwas Software-Engineering-Erfahrung merken, dass Inlining und Neuimplementierung ernsthafte Nachteile haben. Das grundlegendste Problem hängt wahrscheinlich mit der Codewartung zusammen. Jede nennenswerte Software braucht ein gewisses Maß an Wartung. Von Bugfixes bis zum Umgang mit sich ändernden (Build-)Umgebungen oder Anforderungen: Code muss geändert werden, um funktionsfähig und relevant zu bleiben. Beim Einbetten/Neuschreiben von Abhängigkeiten wird auch die Wartungslast mitkopiert.
Für DuckDB bedeutete das historisch, dass für jede Abhängigkeit sehr sorgfältig abgewogen wurde, ob die erhöhte Wartungslast die Notwendigkeit der Abhängigkeit rechtfertigt. Eine Abhängigkeit aufzunehmen bedeutete, sie zu warten, und diese Entscheidung wurde nie leichtfertig getroffen. Das funktioniert in vielen Fällen gut und hat den zusätzlichen Vorteil, dass Entwickler kritisch darüber nachdenken, eine Abhängigkeit aufzunehmen, statt gedankenlos Bibliothek an Bibliothek anzuschrauben. Für manche Abhängigkeiten funktioniert das aber einfach nicht. Nehmen Sie zum Beispiel die SDKs großer Cloud-Anbieter. Die sind oft ziemlich massiv, werden sehr häufig aktualisiert und stecken voller Funktionen, die für eine zunehmend reife analytische Datenbank argumentierbar wesentlich sind. Das lässt eine unangenehme Wahl: entweder diese wesentlichen Features nicht anbieten oder die No-Dependency-Regel brechen.
DuckDB-Extensions
Hier kommen Extensions ins Spiel. Extensions bieten eine elegante Lösung für das Abhängigkeitsdilemma, indem sie feingranulares Brechen der No-Dependency-Regel erlauben. Abhängigkeiten aus DuckDBs Kern in Extensions zu verschieben, lässt die Kern-Codebasis abhängigkeitsfrei bleiben – und das bleibt sie. Das bedeutet, dass DuckDBs „praktische Einbettbarkeit und Portabilität“ ungefährdet bleibt. Andererseits kann DuckDB trotzdem Features bieten, die unweigerlich von einer Drittanbieterbibliothek abhängen. Außerdem kann jede Extension durch das Verschieben von Abhängigkeiten unterschiedliche Grade der Exposition gegenüber Instabilität aus Abhängigkeiten haben. Manche Extensions können sich zum Beispiel nur auf sehr reife, stabile Bibliotheken mit guter Portabilität stützen, andere experimentellere Abhängigkeiten mit begrenzter Portabilität einbeziehen. Diese Wahl wird dann an den Nutzer weitergegeben, der wählen kann, welche Extension er nutzt.
Bei DuckDB kam diese Erkenntnis zur Bedeutung von Extensions und ihrem Verhältnis zur No-Dependency-Regel sehr früh, und Erweiterbarkeit ist seit den frühen Tagen in DuckDBs Design verankert. Heute lassen sich viele Teile von DuckDB erweitern. Sie können zum Beispiel Funktionen (Table, Scalar, Copy, Aggregation), Dateisysteme, Parser, Optimizer-Regeln und vieles mehr hinzufügen. Viele neue Features, die DuckDB bekommt, entstehen in Extensions und sind nach Funktionalität oder nach Abhängigkeitsmenge gruppiert. Beispiele für Extensions sind die SQLite-Extension zum Lesen/Schreiben von/zu SQLite-Dateien oder die Spatial-Extension, die eine breite Palette geospatialer Verarbeitungsfeatures bietet. DuckDBs Extensions werden als ladbare Binaries für die meisten großen Plattformen verteilt (einschließlich DuckDB-Wasm), sodass Laden und Installieren mit zwei einfachen SQL-Anweisungen möglich ist:
INSTALL spatial;LOAD spatial;Für die meisten vom DuckDB-Team gepflegten Core-Extensions gibt es sogar Auto-Install und Auto-Load: Die nötigen Extensions für eine SQL-Anweisung werden erkannt und automatisch installiert und geladen. Eine ausführliche Beschreibung, welche Extensions verfügbar sind und wie man sie nutzt, finden Sie in der Dokumentation.
Abhängigkeitsverwaltung
Bisher haben wir gesehen, wie DuckDB externe Abhängigkeiten in der Kern-Codebasis vermeidet, indem es sie aus dem Kern-Repository in Extensions verschiebt. Wir sind aber noch nicht aus dem Schneider. Da DuckDB in C++ geschrieben ist, ist der natürlichste Weg, Extensions zu schreiben, C++. In C++ gibt es aber kein Standard-Tooling wie einen Paketmanager, und die Antwort auf die Frage, wie man Abhängigkeitsverwaltung in C++ macht, war viele Jahre: „Mit viel Schmerz und Qual.“ Angesichts von DuckDBs Fokus auf Portabilität und Unterstützung vieler Plattformen ist manuelles Verwalten von Abhängigkeiten nicht machbar: Abhängigkeiten werden in der Regel aus dem Quellcode gebaut, jede mit eigenen Feinheiten, die spezielle Build-Flags und Konfiguration für verschiedene Plattformen brauchen. Bei einem wachsenden Extension-Ökosystem würde das schnell zu einem unwartbaren Chaos.
Zum Glück hat sich in der C++-Landschaft in den letzten Jahren viel geändert. Heute gibt es gute Dependency-Manager. Einer davon ist Microsofts vcpkg. Es ist unter C++-Dependency-Managern ein sehr bemerkenswerter Player geworden, belegt durch über 20.000 GitHub-Sterne und native Unterstützung von CLion und Visual Studio. vcpkg enthält über 2000 Abhängigkeiten wie Apache Arrow, yyjson und verschiedene Cloud-Provider- SDKs.
Für alle, die schon einmal einen Paketmanager genutzt haben, fühlt sich vcpkg ziemlich natürlich an. Abhängigkeiten stehen in einer Datei vcpkg.json, und vcpkg wird ins Build-System eingehängt. Beim Bauen stellt vcpkg sicher, dass die in vcpkg.json angegebenen Abhängigkeiten gebaut und verfügbar sind. vcpkg unterstützt die Integration mit mehreren Build-Systemen, mit Fokus auf nahtlose CMake-Integration.
vcpkg mit DuckDB nutzen
Nachdem wir DuckDB-Extensions und vcpkg behandelt haben, haben wir gezeigt, wie DuckDB Abhängigkeiten verwalten kann, ohne Portabilität, Wartbarkeit und Stabilität mehr als nötig zu opfern. Als Nächstes machen wir das greifbarer, indem wir uns eine von DuckDBs Extensions anschauen und wie sie vcpkg zur Verwaltung ihrer Abhängigkeiten nutzt.
Beispiel: Azure-Extension
Die Azure-Extension bietet Funktionen rund um Microsoft Azure, einen der großen Cloud-Anbieter. DuckDBs Azure-Extension hängt vom Azure C++ SDK ab, um direktes Lesen aus Azure Storage zu unterstützen. Dazu fügt sie ein eigenes Dateisystem und einen Secret-Typ hinzu, mit dem sich authentifizierte Azure-Container leicht abfragen lassen:
CREATE SECRET az1 ( TYPE azure, CONNECTION_STRING '⟨redacted⟩');SELECT column_a, column_bFROM 'az://my-container/some-file.parquet';Um diese Features umzusetzen, hängt die Azure-Extension von verschiedenen Teilen des Azure SDK ab. Die stehen in der
vcpkg.json der Azure-Extension:
{ "dependencies": [ "azure-identity-cpp", "azure-storage-blobs-cpp", "azure-storage-files-datalake-cpp" ]}In der Datei CMakelists.txt der Azure-Extension finden wir dann folgende Zeilen:
find_package(azure-identity-cpp CONFIG)find_package(azure-storage-blobs-cpp CONFIG)find_package(azure-storage-files-datalake-cpp CONFIG)
target_link_libraries(${EXTENSION_NAME} Azure::azure-identity Azure::azure-storage-blobs Azure::azure-storage-files-datalake)target_include_directories(${EXTENSION_NAME} PRIVATE Azure::azure-identity Azure::azure-storage-blobs Azure::azure-storage-files-datalake)Und das ist im Wesentlichen alles! Jedes Mal, wenn die Azure-Extension gebaut wird, wird zuerst vcpkg aufgerufen, um sicherzustellen, dass azure-identity-cpp, azure-storage-blobs-cpp und azure-storage-files-datalake-cpp mit den korrekten plattformspezifischen Flags gebaut und in CMake über find_package verfügbar sind.
Eine eigene DuckDB-Extension bauen
Bisher haben wir die Abhängigkeitsverwaltung aus Sicht der Entwickler der DuckDB-Kernbeitragenden betrachtet. All das gilt aber für jeden, der eine Extension bauen will. DuckDB pflegt ein C++-Extension-Template, das alle nötigen Build-Skripte, CI/CD-Pipeline und vcpkg-Konfiguration enthält, um eine DuckDB-Extension in Minuten zu bauen, zu testen und bereitzustellen. Es kann die ladbaren Extension-Binaries automatisch für alle verfügbaren Plattformen bauen, einschließlich Wasm.
Das Extension-Template einrichten
Um zu zeigen, wie einfach das ist, gehen wir alle Schritte durch, eine DuckDB-Extension von Grund auf zu bauen, einschließlich einer von vcpkg verwalteten externen Abhängigkeit.
Zuerst müssen Sie vcpkg installieren:
git clone https://github.com/Microsoft/vcpkg.git./vcpkg/bootstrap-vcpkg.shexport VCPKG_TOOLCHAIN_PATH=`pwd`/vcpkg/scripts/buildsystems/vcpkg.cmakeDann legen Sie ein GitHub-Repository auf Basis des Templates an, indem Sie auf „Use this template“ klicken.
Nun klonen Sie Ihr neu erzeugtes Extension-Repo (einschließlich Submodules) und initialisieren das Template:
git clone --recurse-submodules \ https://github.com/⟨your_username⟩/⟨your_extension_repo⟩cd your-extension-repo./scripts/bootstrap-template.py url_parserUm zu bestätigen, dass alles wie erwartet funktioniert, führen Sie die Tests aus:
make testFunktionalität hinzufügen
Im aktuellen Zustand ist die Extension natürlich etwas langweilig. Also fügen wir etwas Funktionalität hinzu! Um es
einfach zu halten, fügen wir eine Skalarfunktion hinzu, die eine URL parst und das Schema zurückgibt. Wir nennen die
Funktion url_scheme. Zuerst fügen wir in unserer Datei vcpkg.json eine Abhängigkeit zur Boost-URL-Bibliothek hinzu:
{ "dependencies": [ "boost-url" ]}Dann passen wir unsere CMakelists.txt an, damit die Abhängigkeiten korrekt ins Build einbezogen werden.
find_package(Boost REQUIRED COMPONENTS url)target_link_libraries(${EXTENSION_NAME} Boost::url)target_link_libraries(${LOADABLE_EXTENSION_NAME} Boost::url)In src/url_parser_extension.cpp entfernen wir die Standard-Beispielfunktionen und ersetzen sie durch unsere
Implementierung der Funktion url_scheme:
inline void UrlParserScalarFun(DataChunk &args, ExpressionState &state, Vector &result) { auto &name_vector = args.data[0]; UnaryExecutor::Execute<string_t, string_t>( name_vector, result, args.size(), [&](string_t url) { string url_string = url.GetString(); boost::system::result<boost::urls::url_view> parse_result = boost::urls::parse_uri( url_string ); if (parse_result.has_error() || !parse_result.value().has_scheme()) { return string_t(); } string scheme = parse_result.value().scheme(); return StringVector::AddString(result, scheme); });}
static void LoadInternal(DatabaseInstance &instance) { auto url_parser_scalar_function = ScalarFunction("url_scheme", {LogicalType::VARCHAR}, LogicalType::VARCHAR, UrlParserScalarFun); ExtensionUtil::RegisterFunction(instance, url_parser_scalar_function);}Mit geschriebener Extension können wir make ausführen, um DuckDB und die Extension zu bauen. Nach dem Build
sind wir bereit, unsere Extension auszuprobieren. Da der Build-Prozess auch eine frische DuckDB-Binary mit automatisch geladener Extension baut,
müssen wir nur ./build/release/duckdb ausführen und können unsere neu hinzugefügte Skalarfunktion nutzen:
SELECT url_scheme('https://github.com/duckdb/duckdb');Als wohlverhaltende Entwickler fügen wir schließlich Tests hinzu, indem wir den Standardtest test/sql/url_parser.test
überschreiben mit:
require url_parser
# Confirm the extension worksquery ISELECT url_scheme('https://github.com/duckdb/duckdb')----https
# On parser errors or not finding a scheme, the result is also an empty stringquery ISELECT url_scheme('not:\a/valid_url')----(empty)Nun bleibt nur noch, mit make test zu bestätigen, dass alles wie erwartet funktioniert, und diese Änderungen ins Remote-
Repository zu pushen. Dann übernimmt GitHub Actions und stellt sicher, dass die Extension für alle von DuckDB unterstützten
Plattformen gebaut wird.
Weitere Details finden Sie im Template-Repository. Die Beispiel-Extension, die wir in diesem Blog gebaut haben, ist auf GitHub veröffentlicht. Beachten Sie, dass in der Demo die Wasm- und MinGW-Builds deaktiviert wurden wegen offener Probleme mit der boost-url-Abhängigkeit beim Bauen auf diesen Plattformen. Sobald diese Probleme upstream behoben sind, ist das Wiederaktivieren ihrer Builds für die Extension sehr einfach. Als Autor dieser Extension könnte es natürlich sehr sinnvoll sein, diese Compile-Probleme selbst in vcpkg zu beheben – nicht nur für diese Extension, sondern für die ganze Open-Source-Community!
Fazit
In diesem Blogpost haben wir DuckDBs Weg zur Verwaltung von Abhängigkeiten in seinem Extension-Ökosystem erkundet, während die Kernphilosophie null externer Abhängigkeiten gewahrt bleibt. Durch die Kraft von Extensions kann DuckDB Portabilität und Einbettbarkeit halten und trotzdem wesentliche Features bieten, die externe Abhängigkeiten brauchen. Um die Abhängigkeitsverwaltung zu vereinfachen, ist Microsofts vcpkg in DuckDBs Extension-Build-Systeme integriert, sowohl für von DuckDB gepflegte Extensions als auch für Third-Party-Extensions.
Wenn dieser Blogpost Ihr Interesse am Erstellen einer eigenen DuckDB-Extension geweckt hat, schauen Sie sich das C++-Extension-Template, die DuckDB-Dokumentation zu Extensions und das sehr nützliche duckdb-extension-radar-Repository an, das öffentliche DuckDB-Extensions verfolgt. Außerdem hat DuckDB einen Discord-Server, auf dem Sie Hilfe zu Extensions oder allem DuckDB-bezogenen allgemein bekommen können.