Suppression de Protobuf et amélioration des performances par 5 grâce à des liaisons directes Rust↔C
(pgdog.dev)- PgDog, un proxy d’extension pour PostgreSQL, a introduit des liaisons directes Rust au lieu de la sérialisation Protobuf afin d’améliorer les performances de parsing SQL
- L’ancienne architecture basée sur Protobuf a été remplacée par une conversion directe C–Rust (bindgen + wrappers générés par Claude), avec à la clé des performances 5,45× supérieures en parsing et 9,64× en déparsing
- Le goulot d’étranglement de performance a été identifié dans la fonction pg_query_parse_protobuf ; après des tentatives de mise en cache, l’équipe a procédé à un changement structurel pour obtenir un gain durable
- En utilisant le LLM Claude, l’équipe a généré automatiquement 6 000 lignes de code de conversion Rust–C et les a appliquées aux fonctions clés comme
parse,deparse,fingerprintetscan - Grâce à cette optimisation, l’utilisation CPU et la latence de PgDog ont diminué, ce qui améliore fortement son efficacité comme proxy de scalabilité horizontale pour PostgreSQL
PgDog et les limites de Protobuf
- PgDog est un proxy destiné à faire monter PostgreSQL en charge, et utilise en interne libpg_query pour parser les requêtes SQL
- Écrit en Rust, il communiquait auparavant avec la bibliothèque C via la sérialisation/désérialisation Protobuf
- Protobuf est rapide, mais les liaisons directes le sont davantage
- L’équipe de PgDog a forké
pg_query.rspour supprimer Protobuf et implémenter des liaisons directes C–Rust - Résultat : le parsing des requêtes est devenu 5,45 fois plus rapide, et le déparsing 9,64 fois plus rapide
- L’équipe de PgDog a forké
Résultats des benchmarks
- Les benchmarks peuvent être reproduits dans le dépôt forké de PgDog
pg_query::parse(Protobuf) : 613 QPSpg_query::parse_raw(C–Rust direct) : 3357 QPSpg_query::deparse(Protobuf) : 759 QPSpg_query::deparse_raw(Rust–C direct) : 7319 QPS
Analyse du goulot d’étranglement et tentative de cache
- L’analyse du temps CPU avec le profiler samply a montré que la fonction pg_query_parse_protobuf constituait le principal goulot d’étranglement
- Une amélioration partielle a été tentée via le cache
- Utilisation d’un cache hashmap LRU stockant l’AST avec le texte de la requête comme clé
- Réutilisation possible dans le cas des prepared statements
- Cependant, certains ORM généraient des milliers de requêtes uniques, et d’anciens drivers PostgreSQL ne prenaient pas en charge les prepared statements, ce qui limitait fortement l’efficacité du cache
Suppression de Protobuf à l’aide d’un LLM
- L’équipe de PgDog a utilisé le LLM Claude pour générer les liaisons Rust sans Protobuf
- L’IA s’est montrée efficace sur une tâche au périmètre clair et vérifiable
- À partir de la spécification Protobuf de
libpg_query, Claude a mappé les structures C vers des structures Rust- Après 2 jours d’itérations, cela a abouti à 6 000 lignes de code Rust récursif
- L’approche a été appliquée aux fonctions
parse,deparse,fingerprintetscan, avec un gain de performance de 25 % selonpgbench
Détails d’implémentation
- Les conversions entre Rust et C utilisent des fonctions
unsafepour mapper directement les structures- Les structures C sont transmises à l’API Postgres pour générer l’AST, puis converties récursivement en Rust
- Chaque nœud de l’AST est traité par la fonction convert_node, qui mappe les centaines de tokens de la grammaire SQL
- Des fonctions de conversion séparées existent pour chaque type de nœud, comme SELECT, INSERT, etc.
- Le résultat de conversion réutilise la structure Protobuf existante (
protobuf::ParseResult), ce qui permet une validation par comparaison octet par octet lors des tests - L’algorithme récursif est plus rapide qu’une implémentation itérative, car il effectue moins d’allocations mémoire et exploite mieux le cache CPU
- Une implémentation itérative s’est révélée plus lente à cause d’allocations mémoire inutiles et de recherches dans une hashmap
Conclusion
- En réduisant la surcharge du parseur Postgres, PgDog diminue à la fois la latence, la mémoire et l’utilisation CPU
- Grâce à cette optimisation, PgDog devient un proxy d’extension PostgreSQL plus rapide et moins coûteux à exploiter
- PgDog recrute actuellement des ingénieurs pour construire avec lui la prochaine itération de la scalabilité horizontale de PostgreSQL
3 commentaires
Il se peut que je déforme le sens du texte original, mais j’ai l’impression que, dans les articles sur Rust en particulier, on écrit souvent comme si c’était « parce que c’est du Rust » que c’était devenu plus rapide, en laissant de côté l’essentiel.
Le point principal de cet article, c’est pourtant que les performances se sont améliorées en réduisant une surcharge de sérialisation inutile.
En le relisant maintenant, je n’ai pas non plus l’impression que c’est un article qui fait particulièrement l’éloge de Rust, mais est-ce que c’est parce que d’autres articles m’ont laissé une perception négative ?
Moi aussi, j’ai trouvé que le titre original était un peu trop centré sur Rust par rapport au contenu réel, au point de donner l’impression que l’accent était mis sur le gain de performances, donc je l’ai légèrement modifié.
On retrouve assez souvent cette tendance dans les articles sur Rust, donc j’ai l’impression qu’il faut les lire en gardant un petit filtre.
Commentaires sur Hacker News
Le titre donne l’impression que Rust a apporté une amélioration de performances de 5x, alors qu’en réalité la situation était devenue plus lente, ce qui est assez ironique
Le problème venait du fait qu’un logiciel écrit en Rust devait utiliser
libpg_query, une bibliothèque en C, mais comme ils ne pouvaient pas la relier directement, ils ont utilisé des bindings Rust–C basés sur ProtobufCette approche étant lente, ils ont finalement réécrit des bindings nouveaux, non portables mais bien mieux optimisés, avec l’aide d’un LLM
Si tout avait été écrit en C dès le départ, cette étape de conversion n’aurait pas été nécessaire. Autrement dit, un titre comme « on a réduit la perte de performances causée par l’usage de Rust » aurait été plus exact
Les couches de conversion apportent de la portabilité et de la sécurité, mais à force de copies, conversions et sérialisations, elles finissent aussi par ralentir les applications
Appeler une bibliothèque C depuis Rust est très simple, et il existe déjà beaucoup de wrappers sûrs
Une architecture avec Protobuf au milieu est quelque chose qu’on voit rarement, et c’était là le goulot d’étranglement
Le titre ressemble surtout à un mème du type « réécrit en Rust » destiné à attirer des clics
À l’origine, la bibliothèque reposait sur une mauvaise conception avec sérialisation/désérialisation répétée, et c’est la suppression de cette couche qui a fait la différence
Un titre du genre « remplacer Protobuf par une API classique l’a rendu 5x plus rapide » serait plus précis
Les bindings C sont parmi les plus simples en Rust, et tant que l’API n’est pas énorme, cela reste assez direct
Protobuf me semble être un outil inadapté pour échanger des données en mémoire
Avec les LLM, j’ai l’impression qu’on va voir exploser les portages vers toutes sortes de langages
Le titre est quelque peu trompeur
En pratique, c’est surtout « on a retiré l’étape de sérialisation Protobuf, donc c’est devenu plus rapide »
Cela permet au client et au serveur d’être mis à jour indépendamment tout en continuant à fonctionner, et facilite la communication entre plusieurs langages
Dans les grands systèmes, ce type de flexibilité est très important
memcpyoummapsont bien plus rapides, mais dans l’écosystème Rust, ce genre de méthodes non sûres est généralement mal vuLa lenteur vient peut-être moins de Rust que de l’usage de Protobuf comme format de stockage généralisé
Au fond, l’essentiel a été de simplifier l’implémentation pour l’adapter à un objectif précis
Le fait d’avoir mis Rust dans le titre ressemble à un choix purement destiné à générer des clics
L’auteur original de pg_query explique le contexte
À l’origine, chez pganalyze, l’outil servait à analyser des requêtes Postgres pour trouver les références aux tables, puis à les réécrire et les formater
Au début, ils utilisaient JSON, puis ils sont passés à Protobuf afin de proposer plus facilement des bindings typés dans plusieurs langages (Ruby, Go, Rust, Python, etc.)
Pour un langage comme Rust, la FFI est préférable, mais pour d’autres langages, le coût de maintenance est plus élevé
Il soutient l’approche de Lev et prévoit d’ajouter à l’avenir des fonctions permettant d’accéder directement à libpg_query via la FFI
Cela dit, lorsque les performances ne sont pas critiques, Protobuf reste encore un choix plus pratique
Le « 5x plus rapide » rappelle la blague de Cap’n Proto, « infiniment plus rapide »
Le titre est exagéré, mais le travail réalisé est impressionnant
Ils n’ont pas complètement supprimé Protobuf, ils ont surtout optimisé la façon de l’utiliser
La formule « on a remplacé X et c’est devenu 5x plus rapide » veut souvent dire en réalité « on a corrigé une implémentation bancale »
Les principales leçons sont :
La FFI Rust a elle aussi un coût, donc le vrai gain vient moins du langage que de la refonte du flux de données et du travail d’optimisation
FlatBuffers est plus rapide, mais si Protobuf reste utilisé, c’est parce qu’il est maintenu par une grande entreprise
Au final, l’idée que « c’est sûr parce que c’est fait par Google » n’a pas vraiment de fondement
code.google.com) pour voir ensuite le service disparaîtreUne simple structure zero-copy avec mémoire partagée et champs de version suffirait largement, il n’y a pas forcément de raison d’utiliser Protobuf
Les performances de Protobuf sont, à mon avis, une plaisanterie
Il faut utiliser des formats zero-copy où la sérialisation est quasiment gratuite
Par exemple, Lite³, que j’ai créé, est 242 fois plus rapide que FlatBuffers
Si Protobuf continue d’être utilisé, c’est pour tout un ensemble de raisons très concrètes : l’écosystème, les schémas, l’outillage selon les langages, etc.
En réalité, le problème ne venait ni de Rust ni de Protobuf, mais d’une implémentation de sérialisation inefficace dans la couche d’abstraction PostgreSQL
pgdog a supprimé cette couche et a transmis les données directement à l’API C
Éliminer des fonctionnalités inutiles rend forcément les choses plus rapides
Mais certaines personnes ont encore besoin de sérialisation dans leur cas d’usage
Pour elles, un titre disant « passez à Rust » envoie un mauvais message
Dans la plupart des cas, JSON suffit, et si l’on a vraiment besoin d’aller plus vite, il vaut mieux éviter la sérialisation elle-même
C’est une comparaison injuste
Utiliser un protocole de sérialisation pour des communications IPC implique forcément un surcoût
C’est un cas qui illustre bien la formule : « si c’est 20 % plus rapide, c’est une amélioration ; si c’est 10 fois plus rapide, c’est que c’était mal conçu dès le départ »