Protobuf entfernt und Leistung durch direkte Rust↔C-Bindings um das Fünffache verbessert
(pgdog.dev)- Der PostgreSQL-Erweiterungs-Proxy PgDog hat zur Verbesserung der SQL-Parsing-Performance direkte Rust-Bindings anstelle der Protobuf-Serialisierung eingeführt
- Die bisherige Protobuf-basierte Struktur wurde durch direkte C–Rust-Konvertierung (bindgen + von Claude erzeugte Wrapper) ersetzt, was 5,45-fach schnelleres Parsing und 9,64-fach schnelleres Deparsing brachte
- Der Performance-Engpass wurde in der Funktion pg_query_parse_protobuf gefunden; nach Caching-Versuchen wurde für eine grundlegende Verbesserung die Struktur geändert
- Mithilfe von Claude LLM wurden 6.000 Zeilen Rust–C-Konvertierungscode automatisch erzeugt und auf zentrale Funktionen wie
parse,deparse,fingerprintundscanangewendet - Durch diese Optimierung wurden CPU-Auslastung und Latenz von PgDog reduziert, wodurch die Effizienz als PostgreSQL-Proxy für horizontale Skalierung deutlich verbessert wurde
PgDog und die Grenzen von Protobuf
- PgDog ist ein Proxy zur Skalierung von PostgreSQL und verwendet intern libpg_query, um SQL-Abfragen zu parsen
- Es ist in Rust geschrieben und kommunizierte bisher über Protobuf-Serialisierung/Deserialisierung mit der C-Bibliothek
- Protobuf ist schnell, aber direkte Bindings sind noch schneller
- Das PgDog-Team hat
pg_query.rsgeforkt, Protobuf entfernt und direkte C–Rust-Bindings implementiert - Dadurch wurde das Query-Parsing 5,45-mal und das Deparsing 9,64-mal schneller
- Das PgDog-Team hat
Benchmark-Ergebnisse
- Die Benchmarks lassen sich im Fork-Repository von PgDog reproduzieren
pg_query::parse(Protobuf): 613 QPSpg_query::parse_raw(direkt C–Rust): 3357 QPSpg_query::deparse(Protobuf): 759 QPSpg_query::deparse_raw(direkt Rust–C): 7319 QPS
Analyse des Performance-Engpasses und Caching-Versuche
- Die Analyse der CPU-Zeit mit dem Profiler samply zeigte, dass die Funktion pg_query_parse_protobuf der Engpass ist
- Über Caching wurde eine teilweise Verbesserung versucht
- Verwendet wurde ein Hashmap-Cache auf Basis eines LRU-Algorithmus, der den AST mit dem Query-Text als Schlüssel speichert
- Bei der Verwendung vorbereiteter Statements ist eine Wiederverwendung möglich
- Einige ORMs erzeugten jedoch Tausende eindeutige Queries, und ältere PostgreSQL-Treiber unterstützten vorbereitete Statements nicht, wodurch die Cache-Effizienz gering blieb
Entfernung von Protobuf mithilfe eines LLM
- Das PgDog-Team nutzte Claude LLM, um Rust-Bindings ohne Protobuf zu erzeugen
- Innerhalb eines klaren und überprüfbaren Aufgabenbereichs arbeitete die KI effektiv
- Claude mappt auf Basis der Protobuf-Spezifikation von
libpg_queryC-Strukturen auf Rust-Strukturen- Nach zwei Tagen iterativer Arbeit wurden 6.000 Zeilen rekursiven Rust-Codes fertiggestellt
- Die Lösung wurde auf die Funktionen
parse,deparse,fingerprintundscanangewendet; dabei wurde laut pgbench eine Leistungssteigerung von 25 % bestätigt
Details zur Implementierung
- Die Konvertierung zwischen Rust und C verwendet unsafe-Funktionen, um Strukturen direkt zu mappen
- C-Strukturen werden an die Postgres-API übergeben, um den AST zu erzeugen, und anschließend rekursiv nach Rust konvertiert
- Jeder AST-Knoten wird von der Funktion convert_node verarbeitet, die Hunderte von Tokens der SQL-Syntax mappt
- Für Knotentypen wie SELECT, INSERT usw. gibt es jeweils eigene Konvertierungsfunktionen
- Für das Konvertierungsergebnis wird die bestehende Protobuf-Struktur (
protobuf::ParseResult) wiederverwendet, wodurch sich bei Tests eine Verifikation per Byte-für-Byte-Vergleich durchführen lässt - Der rekursive Algorithmus benötigt wenig Speicherzuweisungen und nutzt den CPU-Cache effizient, wodurch er schneller ist als eine iterative Implementierung
- Eine iterative Implementierung war wegen unnötiger Speicherzuweisungen und Hashmap-Lookups sogar langsamer
Fazit
- Durch die Verringerung des Overheads des Postgres-Parsers wurden bei PgDog Latenz, Speicherbedarf und CPU-Auslastung gesenkt
- Mit dieser Optimierung entwickelt sich PgDog zu einem schnelleren und kostengünstiger betreibbaren PostgreSQL-Proxy für Skalierung
- PgDog sucht derzeit Ingenieure, die gemeinsam an der horizontalen Skalierung (next iteration) von PostgreSQL arbeiten
3 Kommentare
Vielleicht missverstehe ich den Originaltext, aber gerade bei Artikeln über Rust wirkt es oft so, als würden sie am eigentlichen Punkt vorbeigehen und so schreiben, als sei es „wegen Rust“ schneller geworden.
Der Hauptpunkt dieses Artikels ist doch, dass sich die Leistung verbessert hat, weil unnötiger Serialisierungs-Overhead reduziert wurde.
Wenn ich jetzt noch einmal draufsehe, ist es wohl doch kein Artikel, der Rust übermäßig lobpreist — oder habe ich durch andere Artikel einfach schon eine negative Wahrnehmung entwickelt?
Ich fand auch, dass der Originaltitel im Vergleich zum tatsächlichen Inhalt zu stark nach Rust klang und dadurch wirkte, als läge der Fokus auf der Leistungsverbesserung, deshalb habe ich ihn leicht angepasst.
Bei Rust-Artikeln sieht man diese Tendenz recht häufig, daher scheint es sinnvoll, sie mit einem kleinen Filter zu lesen.
Hacker-News-Kommentare
Der Titel wirkt so, als hätte Rust für eine 5-fache Leistungssteigerung gesorgt, ironischerweise wurde es in Wirklichkeit aber langsamer
Das Problem war, dass die in Rust geschriebene Software
libpg_queryin C nutzen musste, aber nicht direkt anbinden konnte und deshalb Rust–C-Bindings auf Protobuf-Basis verwendeteDieser Ansatz war langsam, also schrieb man schließlich mit Hilfe eines LLM neue Bindings, die zwar weniger portabel, aber deutlich stärker optimiert waren
Hätte man von Anfang an in C geschrieben, wäre dieser Umwandlungsprozess nicht nötig gewesen. Genauer wäre also ein Titel wie „Den durch den Einsatz von Rust verursachten Performanceverlust reduziert“ gewesen
Solche Konvertierungsschichten bringen Portabilität und Sicherheit, führen aber letztlich zu wiederholtem Kopieren, Umwandeln und Serialisieren und sind damit einer der Gründe, warum Apps langsamer werden
Aus Rust heraus C-Bibliotheken aufzurufen ist sehr einfach, und es gibt bereits viele sichere Wrapper
Eine Architektur mit Protobuf als Zwischenschicht sieht man fast nie, und genau das war der Flaschenhals
Der Titel wirkt eher wie ein einfacher „in Rust neu geschrieben“-Meme, um Klicks zu erzeugen
Die ursprüngliche Bibliothek hatte schlicht ein schlechtes Design mit wiederholter Serialisierung/Deserialisierung, und entscheidend war, genau das zu entfernen
Treffender wäre der Titel „Protobuf durch eine normale API ersetzt und dadurch 5-mal schneller geworden“
C-Bindings in Rust gehören zum Einfachsten, und solange die API nicht riesig ist, bleibt es überschaubar
Protobuf halte ich für ein ungeeignetes Werkzeug für den Datenaustausch im Speicher
Dank LLMs dürfte die Zahl von Portierungen in viele verschiedene Sprachen künftig explosionsartig steigen
Der Titel ist etwas irreführend
Im Grunde heißt es nur: „Nachdem man den Protobuf-Serialisierungsschritt entfernt hat, wurde es schneller“
Dadurch können Client und Server unabhängig voneinander aktualisiert werden und trotzdem weiter funktionieren, außerdem wird die Kommunikation zwischen vielen Sprachen erleichtert
In großen Systemen ist solche Flexibilität sehr wichtig
memcpyodermmapsind deutlich schneller, aber im Rust-Umfeld meidet man solche unsicheren Verfahren eherNicht Rust, sondern der Einsatz von Protobuf als generalisierter Speicher-/Ablageformat könnte die Ursache für die Langsamkeit gewesen sein
Letztlich war entscheidend, die Lösung auf den konkreten Zweck zuzuschneiden und zu vereinfachen
Dass Rust im Titel auftaucht, wirkt wie eine bewusste Entscheidung zur Klickerzeugung
Der ursprüngliche Autor von
pg_queryerklärt den HintergrundUrsprünglich wurde es bei pganalyze verwendet, um Postgres-Abfragen zu parsen, Tabellenreferenzen zu finden und Queries umzuschreiben oder zu formatieren
Anfangs nutzte man JSON, wechselte später aber zu Protobuf, um in mehreren Sprachen (Ruby, Go, Rust, Python usw.) leichter typsichere Bindings anbieten zu können
Für Sprachen wie Rust ist FFI zwar besser, bei anderen Sprachen ist der Wartungsaufwand aber höher
Er unterstützt Levs Ansatz, und künftig sollen Funktionen für direkten FFI-Zugriff auf
libpg_queryergänzt werdenWenn Performance allerdings nicht entscheidend ist, bleibt Protobuf weiterhin die bequemere Wahl
Die Formulierung „5-mal schneller“ erinnert an den Witz von Cap’n Proto mit „unendlich schnell“
Der Titel ist übertrieben, aber die eigentliche Arbeit ist beeindruckend
Protobuf wurde nicht komplett entfernt, sondern die Art der Nutzung optimiert
Die Formulierung „Auf X umgestellt und dadurch 5-mal schneller geworden“ bedeutet meist nur: „Eine vorher chaotische Implementierung wurde repariert“
Die wichtigste Lehre ist:
Auch Rust-FFI hat Overhead, daher lag der eigentliche Gewinn nicht an der Sprache, sondern an der Neugestaltung des Datenflusses und der Optimierungsarbeit
FlatBuffers ist zwar schneller, aber Protobuf wird genutzt, weil es von großen Unternehmen gepflegt wird
Am Ende ist die Vorstellung „von Google gemacht, also sicher“ unbegründet
code.google.com) veröffentlicht und erlebt, wie das gescheitert istEine einfache Zero-Copy-Struktur mit gemeinsamem Speicher und Versionsfeldern würde genügen; einen zwingenden Grund für Protobuf sehe ich nicht
Ich halte die Performance von Protobuf für ein Witzniveau
Man sollte ein Zero-Copy-Format verwenden, bei dem Serialisierung praktisch kostenlos ist
Zum Beispiel ist mein eigenes Lite³ 242-mal schneller als FlatBuffers
Für Protobuf sprechen viele praktische Gründe wie Ökosystem, Schema und sprachspezifisches Tooling
Das eigentliche Problem war weder Rust noch Protobuf, sondern die ineffiziente Serialisierungsimplementierung der PostgreSQL-Abstraktionsschicht
pgdoghat diese Schicht entfernt und die Daten direkt über die C-API übergebenWenn man unnötige Funktionen entfernt, wird es natürlich schneller
Für manche Menschen gibt es aber weiterhin Situationen, in denen Serialisierung nötig ist
Für sie sendet ein Titel wie „Wechselt zu Rust“ die falsche Botschaft
In den meisten Fällen reicht am Ende JSON, und wenn man wirklich mehr Geschwindigkeit braucht, sollte man Serialisierung ganz vermeiden
Das ist ein unfairer Vergleich
Wenn man für IPC-Kommunikation ein Serialisierungsprotokoll verwendet, entsteht natürlich Overhead
Das passt perfekt zu dem Spruch: „20 % schneller ist eine Verbesserung, 10-mal schneller bedeutet, dass es von Anfang an falsch gebaut war“