42 points par GN⁺ 14 일 전 | Aucun commentaire pour le moment. | Partager sur WhatsApp
  • Toutes les bases de données ne sont au fond qu’un ensemble structuré de fichiers sur un système de fichiers ; pour une application en phase initiale, gérer directement des fichiers peut offrir des performances suffisantes
  • En implémentant le même serveur en Go, Bun et Rust pour comparer trois approches — scan de fichiers, map en mémoire et recherche binaire sur disque —, on peut obtenir un débit élevé même avec un simple accès fichier
  • L’approche avec map en mémoire offre les meilleures performances (jusqu’à 169k req/s), tandis que SQLite reste stable à 25k req/s mais avec un certain surcoût
  • La plupart des services peuvent atteindre jusqu’à 90 millions de DAU avec un simple fichier SQLite, ce qui rend une base de données séparée inutile au début d’un produit
  • Il devient nécessaire d’introduire une base de données lorsque le jeu de données dépasse la RAM ou quand on a besoin de jointures, recherches multicritères, écritures concurrentes et transactions

A-t-on vraiment besoin d’une base de données ?

  • Une base de données n’est au fond qu’un ensemble de fichiers : SQLite est un fichier unique, PostgreSQL repose sur un répertoire et des processus
    • Toutes les bases de données lisent et écrivent sur le système de fichiers, et fonctionnent de la même manière qu’un appel à open() dans le code
    • L’enjeu n’est donc pas de savoir s’il faut « écrire dans des fichiers », mais utiliser les fichiers de la base de données ou les gérer soi-même
    • Pour beaucoup d’applications en phase initiale, une gestion directe peut suffire en termes de performances

Configuration de l’expérience

  • Le même serveur HTTP a été implémenté en Go, Bun (TypeScript) et Rust, avec comparaison de deux stratégies de stockage
    • Utilisation de trois fichiers JSONL : users.jsonl, products.jsonl, orders.jsonl
    • Création via POST /users, consultation via GET /users/:id
    • Seul le chemin de lecture (GET) a été retenu pour le benchmark
  • Approche 1 : lire le fichier à chaque requête

    • À chaque requête, le fichier est ouvert, toutes les lignes sont parcourues, le JSON est parsé puis l’ID est comparé
    • En moyenne, il faut lire la moitié du fichier, soit une complexité en O(n)
    • Plus les données grossissent, plus la vitesse de traitement chute fortement
  • Approche 2 : tout charger en mémoire

    • Au démarrage, le fichier complet est lu et stocké dans une table de hachage indexée par ID
    • Les écritures sont répercutées à la fois dans la map et dans le fichier ; les lectures se font par un simple accès à la map en O(1)
    • Le fichier joue le rôle de stockage persistant, la map celui d’index
    • En Go, sync.RWMutex, et en Rust, RwLock, permettent les lectures parallèles
  • Approche 3 : recherche binaire sur disque

    • Une solution intermédiaire pour obtenir des lectures rapides sans tout charger en RAM
    • Création d’un fichier de données trié par ID et d’un fichier d’index à largeur fixe (58 octets/enregistrement)
    • L’index est parcouru en O(log n) avec ReadAt, puis un seul enregistrement est lu à l’offset correspondant
    • L’ajout de nouveaux enregistrements casse l’ordre de tri, ce qui impose une reconstruction périodique de l’index ou une fusion
    • Ce schéma de fusion ressemble au fonctionnement d’un LSM-tree

Environnement de benchmark

  • Taille des jeux de données : 10k, 100k, 1M enregistrements
  • Outil de charge : wrk, avec requêtes GET aléatoires pendant 10 secondes, sur 4 threads et 50 connexions concurrentes
  • Tests réalisés sur la même machine (Apple M1 Mac mini, macOS 15) avec Go 1.26, Bun 1.3 et Rust 1.94
  • En Go, comparaison supplémentaire avec la recherche binaire (sur disque) et SQLite (modernc.org/sqlite)

Principaux résultats

  • Dégradation des performances en scan linéaire : à 1M d’enregistrements, Go tombe à 23 req/s et Bun à 19 req/s
  • Recherche binaire (sur disque) : de 10k à 1M d’enregistrements, le débit ne baisse que de 15 %, de 45k à 38k req/s
    • Grâce au cache de pages de l’OS, les parties hautes de l’index restent toujours en mémoire
  • SQLite : 25k req/s avec une latence moyenne de 2 ms, pour des performances constantes
  • La recherche binaire est environ 1,7× plus rapide que SQLite, qui garde un surcoût sur une simple lecture par clé primaire
  • L’approche avec map en mémoire est la plus performante : 97k à 169k req/s, avec une latence inférieure à 0,5 ms
  • Bun est plus rapide que Go : Bun à 106k req/s contre 97k req/s pour Go
    • Bun s’appuie sur JavaScriptCore + Zig(uWebSockets) et contourne libuv
  • Rust domine largement en scan linéaire : 3 à 6 fois plus rapide que Go, probablement grâce à l’efficacité du parsing JSON et des E/S
  • Meilleur choix selon le cas d’usage

    • Débit absolu maximal : map en mémoire Rust (169k req/s)
    • Meilleur sans chargement en RAM : recherche binaire Go (~40k req/s)
    • Si SQL est nécessaire : SQLite (25k req/s)
    • Implémentation la plus simple : scan linéaire Go (~20 lignes de code)

Ce que signifient 25 000 req/s

  • Le trafic web classique repose sur une hypothèse de rapport pic:moyenne = 2:1
    • 12 500 req/s en moyenne → 25 000 req/s au pic
  • En supposant qu’un utilisateur actif effectue 10 consultations par heure et qu’au pic 10 % des utilisateurs sont connectés simultanément
    • Formule des requêtes de pointe : DAU × 0.000278
  • Résultat du calcul du DAU à saturation pour chaque approche
    • Scan linéaire Go : 2.8M
    • Recherche binaire Go : 144M
    • SQLite : 90M
    • Map en mémoire Go : 349M
    • Map en mémoire Bun : 381M
    • Map en mémoire Rust : 608M
  • La plupart des produits n’atteignent jamais ces chiffres
    • Exemples : 10 000 clients SaaS → 3 req/s, application à 100 000 DAU → 30 req/s
  • En conclusion, la plupart des produits en phase initiale n’ont pas besoin de base de données
    • Et si besoin, un simple fichier SQLite peut tenir jusqu’à 90 millions de DAU

Quand une base de données devient nécessaire

  • Quand le jeu de données ne tient plus en RAM

    • Au-delà de plusieurs dizaines de millions d’enregistrements, rien que les index peuvent nécessiter plusieurs Go
    • Il faut alors paginer les données, ce qu’une base de données gère automatiquement
  • Quand il faut interroger sur d’autres champs que l’ID

    • Les recherches multicritères imposent soit un scan de fichiers, soit des maps supplémentaires
    • Maintenir plusieurs maps revient en pratique à réimplémenter soi-même un moteur de requêtes
  • Quand des jointures sont nécessaires

    • Il faut lire et combiner plusieurs fichiers, et SQL devient plus efficace
  • En cas d’écritures concurrentes depuis plusieurs processus

    • Les maps en mémoire de chaque instance sont séparées, ce qui fait perdre la cohérence
    • Il faut une source externe unique de vérité → le rôle d’une base de données
  • Lorsqu’il faut des écritures atomiques entre entités

    • Il faut garantir que la création d’une commande et la décrémentation du stock réussissent ou échouent ensemble
    • Cela nécessite d’implémenter un journal de transactions séparé, alors qu’une base de données le gère via ACID
    • Les outils internes, side projects et produits en phase initiale qui n’ont pas ces contraintes
    • peuvent fonctionner correctement dans la RAM d’un seul serveur
    • Les fichiers JSONL peuvent ensuite être facilement migrés vers une base de données

Annexe et code fourni

  • Code serveur Go, Bun et Rust inclus
  • Fourniture séparée des données de seed et du script d’exécution du benchmark (run_bench.sh)
  • Le fichier ZIP contient go-server/, bun-server/, rust-server/, seed.ts
  • Le script initialise les données aux trois tailles, lance les tests de charge avec wrk, puis s’arrête

Informations sur DB Pro

  • DB Pro** est un client de base de données pour Mac, Windows et Linux**

    • Il intègre requêtes, exploration et administration
    • Avec plateforme web collaborative et IA intégrée
    • La dernière version prend en charge la connexion aux bases SQLite de Val Town
    • La v1.3.0 ajoute la création de bases, un éditeur multi-requêtes et la connexion à PlanetScale Vitess

Aucun commentaire pour le moment.

Aucun commentaire pour le moment.