GoSuda

gosuda.org/sqlite3: Un pilote database/sql en substitution directe exécutant du SQLite en C natif sur mémoire partagée

By Lemon Mint
views ...

gosuda.org/sqlite3 est un pilote database/sql avec CGO_ENABLED=0 pour Go qui exécute l'amalgamation C amont de SQLite 3.54.1 au sein d'un processus ouvrier isolé via des anneaux de mémoire partagée HQQ. Il s'enregistre sous le nom de pilote standard "sqlite3", prend en charge les paramètres de requête DSN de github.com/mattn/go-sqlite3, intègre des binaires de serveur préconstruits attestés par SLSA pour six cibles de systèmes d'exploitation et d'architectures, et utilise le pipelining de requêtes avec un préchargement par lots de 64 KiB pour surpasser cgo intra-processus sur les requêtes à lignes multiples.

1go get gosuda.org/sqlite3@v0.3.3540100

Pourquoi un autre pilote SQLite pour Go ?

SQLite est l'un des logiciels les plus déployés au monde, pourtant son utilisation depuis Go a historiquement contraint les ingénieurs à un choix inconfortable entre la simplicité de compilation et la fidélité du moteur.

D'un côté se trouve github.com/mattn/go-sqlite3, le standard de la communauté de longue date. Il encapsule directement l'amalgamation C officielle de SQLite via cgo. Vous obtenez le véritable planificateur de requêtes en C, une sémantique complète de concurrence WAL et une gestion prévisible de la mémoire en C. Le coût est CGO_ENABLED=1 : les compilations locales et les pipelines CI ralentissent, les images de conteneurs minimaux, vierges ou distroless nécessitent des bibliothèques d'exécution C correspondantes, la compilation croisée vers un autre système d'exploitation ou une autre architecture de processeur nécessite une chaîne d'outils croisée C complète, et toute corruption de mémoire en C partage l'espace d'adressage du processus Go.

De l'autre côté se trouvent les ports SQLite en pur Go générés par la traduction mécanique de l'amalgamation C en code source Go sur un environnement d'exécution C émulé (libc). Ils rétablissent CGO_ENABLED=0, ce qui rend les compilations triviales. Cependant, sous des charges de travail soutenues, le tas C traduit et les allocations Go par ligne augmentent la pression du ramasse-miettes, et des cas limites subtils dans le verrouillage, les délais d'attente d'occupation ou l'interruption d'instructions peuvent diverger des compilations C natives.

Nous avons conçu gosuda.org/sqlite3 afin que les applications Go n'aient pas à choisir entre CGO_ENABLED=0 et le véritable moteur C de SQLite. En compilant l'amalgamation C officielle de SQLite 3.54.1 dans un serveur de mémoire partagée dédié (hqq-sqlite-server) et en communiquant via gosuda.org/hqq, votre binaire Go se compile avec CGO_ENABLED=0 tout en exécutant du code C SQLite non modifié et optimisé par -O3 par-delà une frontière de processus.

Exécution sans configuration : De go get à sql.Open

Remplacer github.com/mattn/go-sqlite3 dans une base de code Go existante nécessite uniquement de modifier le chemin d'importation aveugle. Le pilote s'enregistre en tant que "sqlite3" (et "hqq-sqlite" pour une sélection explicite) et accepte les mêmes chaînes de requête DSN :

 1package main
 2
 3import (
 4	"context"
 5	"database/sql"
 6	"fmt"
 7	"log"
 8
 9	_ "gosuda.org/sqlite3"
10)
11
12func main() {
13	dsn := "file:app.db?_journal_mode=WAL&_busy_timeout=5000&_txlock=immediate&_foreign_keys=on"
14	db, err := sql.Open("sqlite3", dsn)
15	if err != nil {
16		log.Fatal(err)
17	}
18	defer db.Close()
19
20	ctx := context.Background()
21	if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)`); err != nil {
22		log.Fatal(err)
23	}
24	if _, err := db.ExecContext(ctx, `INSERT INTO users (name) VALUES (?)`, "Ada Lovelace"); err != nil {
25		log.Fatal(err)
26	}
27
28	var id int64
29	var name string
30	if err := db.QueryRowContext(ctx, `SELECT id, name FROM users WHERE name = ?`, "Ada Lovelace").Scan(&id, &name); err != nil {
31		log.Fatal(err)
32	}
33	fmt.Printf("user #%d: %s\n", id, name)
34}

Binaires d'ouvriers intégrés et attestés par SLSA

Une question naturelle lorsque l'on apprend qu'une bibliothèque utilise un processus ouvrier en C est : "Dois-je installer ou distribuer un binaire C séparé aux côtés de mon programme Go ?"

Non. gosuda.org/sqlite3 intègre des binaires préconstruits hqq-sqlite-server pour les six principales cibles directement à l'intérieur de fichiers Go étiquetés par plateforme (internal/embedded/embed_<goos>_<goarch>.go). Les contraintes de compilation de Go ne compilant que le fichier correspondant à vos GOOS et GOARCH cibles, la compilation croisée GOOS=linux GOARCH=arm64 go build intègre automatiquement uniquement le binaire de serveur linux/arm64 d'environ 1,4 Mio.

Système d'exploitation cibleArchitecture cible (GOARCH)Binaire intégréChaîne d'outils de compilation et provenance
Linuxamd64, arm64hqq-sqlite-server_linux_<arch>GCC -O3, Provenance SLSA Build L3
macOSarm64, amd64hqq-sqlite-server_darwin_<arch>AppleClang -O3, Provenance SLSA Build L3
Windowsamd64hqq-sqlite-server_windows_amd64.exeMSVC -O2, Provenance SLSA Build L3
Windowsarm64hqq-sqlite-server_windows_arm64.exezig cc -target aarch64-windows-gnu -O3, SLSA L3

Lorsque sql.Open établit sa première connexion, autolaunch.go matérialise le binaire intégré dans le répertoire de cache utilisateur ($XDG_CACHE_HOME/hqq/bin/<sha256>/), vérifie son résumé cryptographique SHA-256 par rapport à la table de correspondances épinglée ReleaseHashes dans hashes.go via gosuda.org/hqq/secureexec, et lance l'ouvrier avec --parent-stdin afin que sa durée de vie soit strictement liée à votre processus Go.

Compatibilité DSN en salle blanche pour mattn/go-sqlite3

Afin de fonctionner comme un substitut transparent authentique, l'analyseur syntaxique DSN (dsn.go) implémente une réplique en salle blanche des options de connexion et de pragma utilisées dans les bases de code mattn/go-sqlite3 :

Paramètre de requête DSNValeurs acceptéesEffet sur SQLite
_busy_timeout / _timeoutMillisecondes entières (par ex. 5000)Appelle sqlite3_busy_timeout(db, ms) à l'ouverture de la session.
_journal_mode / _journalDELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFFExécute PRAGMA journal_mode = <mode>.
_txlockimmediate, exclusive, deferredSélectionne BEGIN IMMEDIATE, BEGIN EXCLUSIVE ou BEGIN DEFERRED pour db.BeginTx.
_foreign_keys / _fkon, true, 1, off, false, 0Exécute PRAGMA foreign_keys = ON/OFF.
_synchronous / _sync0/OFF, 1/NORMAL, 2/FULL, 3/EXTRAExécute PRAGMA synchronous = <level>.
_cache_sizePages entières ou -KiB (par ex. -20000)Exécute PRAGMA cache_size = <val>.
_locauto ou fuseau horaire IANA (par ex. UTC, Asia/Seoul)Analyse les colonnes textuelles DATETIME/TIMESTAMP dans un objet *time.Time au sein de l'emplacement cible.
modero, rw, rwc, memoryDéfinit SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE ou SQLITE_OPEN_MEMORY.
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuumBooléens ou entiers de modeApplique le PRAGMA de session correspondant lors de l'initialisation de la connexion.

Transformer l'isolation des processus en un avantage de débit

À première vue, exécuter SQLite dans un processus séparé semble nécessairement plus lent que d'appeler sqlite3_step au sein du processus via cgo. En pratique, l'inverse se produit dès qu'une requête renvoie plus d'une seule ligne.

Le coût caché des appels cgo ligne par ligne

Dans un pilote SQLite conventionnel basé sur cgo, database/sql pilote l'itération une ligne et une colonne à la fois. La lecture de 1 000 lignes comportant 4 colonnes déclenche :

  • 1 000 appels cgo à sqlite3_step
  • 4 000 appels cgo à sqlite3_column_type et sqlite3_column_int64 / sqlite3_column_text
  • Des milliers de franchissements de frontières individuels entre l'ordonnanceur Go et la pile C

Pipelining de requêtes et préchargement par lots de 64 KiB

gosuda.org/sqlite3 élimine les franchissements de frontières par ligne en redessinant le protocole réseau autour de deux mécanismes dans stmt.go, rows.go et sqlite3/server/sqlite3_hooks.c :

 1Go Client (stmt.go / rows.go)                       C11 Worker (sqlite3_hooks.c)
 2-----------------------------                       ----------------------------
 31. Pipeline in 1 flight:
 4   ResetAsync + BindValuesAsync
 5   + StmtInfoAsync + FetchRowsAsync(128)  =======>  reset -> bind -> stmt_info ->
 6                                                    loop sqlite3_step() up to 128 rows
 7                                          <=======  Batch 1 (rows 1..128 packed in 64 KiB slot)
 82. Immediately fire prefetch:
 9   FetchRowsAsync(128) (Batch 2)          =======>  loop sqlite3_step() for rows 129..256
10   While C steps Batch 2, Go caller
11   scans rows 1..128 from Batch 1!        <=======  Batch 2 ready in shared memory
  1. Pipelining de requêtes en un seul vol (stmt.go) : Lorsque QueryContext ou Stmt.QueryContext exécute une instruction en lecture seule, s.query() soumet reset, bind_values, stmt_info (lors de la première exécution) et fetch_rows de manière asynchrone en un seul vol en mémoire partagée. Ces quatre opérations s'exécutent consécutivement sur l'ouvrier C et retournent en un seul aller-retour.
  2. Boucles de lots C serrées et préchargement asynchrone (rows.go) : À l'intérieur de fetch_rows (sqlite3_hooks.c), l'ouvrier C exécute sqlite3_step(stmt) dans une boucle C native, encodant jusqu'à FetchSize lignes (par défaut 128) directement dans l'emplacement de réponse en mémoire partagée de 64 KiB jusqu'à ce que la limite du lot soit atteinte, que l'emplacement soit plein ou que SQLITE_DONE se produise. Dès que rows.ingest() décode le Lot 1, il déclenche FetchRowsAsync pour le Lot 2 avant de renvoyer la première ligne à l'appelant. Pendant que votre code Go analyse le Lot 1, l'ouvrier C traite déjà le Lot 2 sur un autre cœur de processeur.

Sur Apple M4 (darwin/arm64), comparer gosuda.org/sqlite3 (CGO_ENABLED=0) à github.com/mattn/go-sqlite3 (CGO_ENABLED=1) sur la même base de données SQLite montre l'effet du regroupement par lots et du pipelining :

Charge de travailgosuda.org/sqlite3 (CGO_ENABLED=0)mattn/go-sqlite3 (CGO_ENABLED=1)Vitesse relative
Sélection ponctuelle (QueryRow par clé primaire)3.52 µs/op (9 B, 1 alloc)3.56 µs/op (232 B, 11 allocs)1,01x (parité, 11x moins d'allocations)
Balayage de 100 lignes (rows.Next)20.41 µs/op (30.5 KB, 211 allocs)36.34 µs/op (21.8 KB, 309 allocs)1,78x plus rapide
Balayage de 1 000 lignes (rows.Next)125.92 µs/op (296 KB, 2 030 allocs)349.10 µs/op (216 KB, 3 009 allocs)2,77x plus rapide
Insertion préparée (Validation automatique)5.33 µs/op (0 B, 0 allocs)5.88 µs/op (120 B, 5 allocs)1,10x plus rapide (0 allocation)
Lot de transactions de 100 lignes189.99 µs/op (16 B, 1 alloc)191.50 µs/op (12.1 KB, 503 allocs)1,01x (500x moins d'allocations)

Annulation, mise en bac à sable des chemins et récupération du pool de connexions

L'exécution de SQLite dans un ouvrier isolé renforce également la manière dont le pilote gère les délais d'attente, le code SQL non sécurisé et les limites du système de fichiers :

  • Annulation de contexte déterministe (conn.go et errors.go) : Chaque appel de pilote arme une protection légère context.AfterFunc (cn.guard). Si le context.Context de l'appelant est annulé ou si son délai expire pendant qu'une requête récursive lente ou une attente de verrouillage s'exécute sur le serveur, la protection marque la connexion comme défectueuse, interrompt la liaison de session locale et envoie une trame hors bande Cancel(). L'ouvrier C invoque immédiatement sqlite3_interrupt(db), débloquant sqlite3_step, tandis que le pilote Go associe l'état SQLITE_INTERRUPT résultant à context.Canceled ou context.DeadlineExceeded et demande à database/sql de remplacer la connexion dans le pool.
  • Mise en bac à sable des répertoires (sqlsrv_pathrules.c) : L'ouvrier C applique des règles de chemin --allow-dir à chaque appel de open_v2, résolvant les liens symboliques et rejetant le parcours relatif .. en dehors du répertoire autorisé. sqlite3_load_extension et SQLITE_LIMIT_ATTACHED sont désactivés par défaut afin qu'une injection SQL ne puisse pas s'échapper du répertoire de la base de données ni charger d'objets partagés arbitraires.

Organisation du dépôt

Le dépôt est structuré de telle sorte que le paquet de niveau supérieur fournit le pilote database/sql transparent tandis que launcher/ et sqlite3/ restent accessibles pour les déploiements de serveurs personnalisés avancés :

 1gosuda.org/sqlite3
 2├── driver.go / api.go         # database/sql driver registration ("sqlite3") and Connector API
 3├── dsn.go                     # Clean-room mattn/go-sqlite3 DSN parser and pragma configuration
 4├── conn.go / tx.go            # Session lifecycle, context guard, and transaction state machine
 5├── stmt.go / rows.go          # Single-flight request pipelining and 64 KiB batch prefetching
 6├── errors.go                  # Typed SQLite primary/extended error codes and sentinel matching
 7├── autolaunch.go / hashes.go  # Zero-config embedded server materialization and SHA-256 pinning
 8├── internal/embedded/         # Platform-tagged //go:embed binaries (linux, darwin, windows)
 9├── launcher/                  # Configurable process supervisor for custom server deployments
10└── sqlite3/                   # Generated hqq-bindgen Go client and C11 SQLite server source
11    └── server/                # C11 server implementation (sqlite3_hooks.c, sqlsrv_policy.c)

Prise en main

Installez le module avec go get et ouvrez votre base de données avec sql.Open("sqlite3", dsn) :

1go get gosuda.org/sqlite3@v0.3.3540100