GoSuda

gosuda.org/sqlite3 : pilote database/sql avec CGO_ENABLED=0 reposant sur l'IPC par mémoire partagée

By Lemon Mint views ...

gosuda.org/sqlite3 est un pilote database/sql pour Go avec CGO_ENABLED=0 qui exécute l'amalgamation C SQLite 3.54.1 en amont dans un processus de travail 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, embarque des binaires serveur préconstruits et attestés SLSA pour six cibles de systèmes d'exploitation et d'architectures, et utilise un pipelining des requêtes avec un préchargement par lots de 64 KiB afin de surpasser le cgo en processus sur les requêtes renvoyant plusieurs lignes.

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 simplicité de construction et fidélité au moteur.

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

De l'autre côté se trouvent les portages purs Go de SQLite, générés par une traduction mécanique de l'amalgamation C en code source Go, sur un runtime C émulé (libc). Ils rétablissent CGO_ENABLED=0, ce qui rend la compilation triviale. Sous des charges soutenues, cependant, le tas C traduit et les allocations Go par ligne augmentent la pression sur le ramasse-miettes, et des cas limites subtils liés au verrouillage, aux délais d'attente occupés (busy timeouts) 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 moteur C SQLite réel. En compilant l'amalgamation C officielle de SQLite 3.54.1 dans un serveur dédié à la mémoire partagée (hqq-sqlite-server) et en communiquant via gosuda.org/hqq, votre binaire Go se compile avec CGO_ENABLED=0 tout en exécutant un SQLite C non modifié, optimisé avec -O3, à travers 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 ne demande que de modifier le chemin d'importation anonyme. Le pilote s'enregistre sous le nom "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 de Travail Embarqués et Attestés SLSA

Une question naturelle lorsqu'on apprend qu'une bibliothèque utilise un processus de travail C est : « Dois-je installer ou livrer un binaire C séparé avec mon programme Go ? »

Non. gosuda.org/sqlite3 embarque des binaires hqq-sqlite-server préconstruits pour les six cibles principales, directement dans des fichiers Go marqués par plateforme (internal/embedded/embed_<goos>_<goarch>.go). Comme les contraintes de compilation de Go n'incluent que le fichier correspondant à vos GOOS et GOARCH cibles, une compilation croisée GOOS=linux GOARCH=arm64 go build n'embarque automatiquement que le binaire serveur linux/arm64 d'environ 1,4 MiB.

Système cibleArchitecture cible (GOARCH)Binaire embarquéChaîne 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 embarqué dans le répertoire de cache utilisateur ($XDG_CACHE_HOME/hqq/bin/<sha256>/), vérifie son empreinte cryptographique SHA-256 par rapport à la table ReleaseHashes épinglée dans hashes.go via gosuda.org/hqq/secureexec, puis lance le travailleur avec --parent-stdin afin que sa durée de vie soit strictement liée à votre processus Go.

Compatibilité DSN Clean-Room avec mattn/go-sqlite3

Pour fonctionner comme un véritable remplacement direct, l'analyseur DSN (dsn.go) implémente une réplique clean-room des options de connexion et des pragmas utilisés dans les bases de code mattn/go-sqlite3 :

Paramètre de requête DSNValeurs acceptéesEffet SQLite
_busy_timeout / _timeoutEntier en millisecondes (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 = <niveau>.
_cache_sizeEntier en pages ou -KiB (ex. -20000)Exécute PRAGMA cache_size = <val>.
_locauto ou fuseau horaire IANA (ex. UTC, Asia/Seoul)Analyse les colonnes texte DATETIME/TIMESTAMP en *time.Time dans le fuseau 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éen ou entiers de modeApplique le PRAGMA de session correspondant lors de l'initialisation de la connexion.

Transformer l'Isolation des Processus en Avantage de Débit

À première vue, exécuter SQLite dans un processus séparé semble forcément plus lent que d'appeler sqlite3_step en processus via cgo. En pratique, c'est l'inverse dès qu'une requête renvoie plus d'une ligne.

Le Coût Caché des Appels cgo Ligne par Ligne

Dans un pilote SQLite classique basé sur cgo, database/sql pilote l'itération une ligne et une colonne à la fois. La lecture de 1 000 lignes à 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ère individuels entre l'ordonnanceur Go et la pile C

Pipelining des Requêtes et Préchargement par Lots de 64 KiB

gosuda.org/sqlite3 élimine les franchissements de frontière par ligne en repensant le protocole filaire autour de deux mécanismes situés dans stmt.go, rows.go et sqlite3/server/sqlite3_hooks.c :

 1Client Go (stmt.go / rows.go)                       Travailleur C11 (sqlite3_hooks.c)
 2-----------------------------                       ---------------------------------
 31. Pipeline en 1 envoi :
 4   ResetAsync + BindValuesAsync
 5   + StmtInfoAsync + FetchRowsAsync(128)  =======>  reset -> bind -> stmt_info ->
 6                                                    boucle sqlite3_step() jusqu'à 128 lignes
 7                                          <=======  Lot 1 (lignes 1..128 empaquetées dans un slot de 64 KiB)
 82. Déclenchement immédiat du préchargement :
 9   FetchRowsAsync(128) (Lot 2)            =======>  boucle sqlite3_step() pour les lignes 129..256
10   Pendant que C exécute le Lot 2, l'appelant
11   Go parcourt les lignes 1..128 du Lot 1 !  <=======  Lot 2 prêt en mémoire partagée
  1. Pipelining de requête en vol unique (stmt.go) : Lorsque QueryContext ou Stmt.QueryContext exécute une instruction en lecture seule, s.query() soumet de manière asynchrone reset, bind_values, stmt_info (à la première exécution) et fetch_rows dans un seul envoi en mémoire partagée. Les quatre opérations s'exécutent successivement sur le travailleur C et reviennent en un seul aller-retour.
  2. Boucles de lots C serrées et préchargement asynchrone (rows.go) : Dans fetch_rows (sqlite3_hooks.c), le travailleur C exécute sqlite3_step(stmt) dans une boucle C native, encodant jusqu'à FetchSize lignes (par défaut 128) directement dans le slot de réponse de 64 KiB en mémoire partagée, jusqu'à atteindre la limite du lot, le remplissage du slot ou SQLITE_DONE. 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 parcourt le Lot 1, le travailleur C exécute déjà le Lot 2 sur un autre cœur de processeur.

Sur Apple M4 (darwin/arm64), la comparaison entre gosuda.org/sqlite3 (CGO_ENABLED=0) et 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 PK)3,52 µs/op (9 B, 1 alloc)3,56 µs/op (232 B, 11 allocs)1,01x (parité, 11x moins d'allocs)
Parcours 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
Parcours 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 (Autocommit)5,33 µs/op (0 B, 0 allocs)5,88 µs/op (120 B, 5 allocs)1,10x plus rapide (0 allocs)
Lot de transaction de 100 lignes189,99 µs/op (16 B, 1 alloc)191,50 µs/op (12,1 KB, 503 allocs)1,01x (500x moins d'allocs)

Annulation, Cloisonnement des Chemins et Récupération du Pool de Connexions

L'exécution de SQLite dans un travailleur isolé renforce également la manière dont le pilote gère les délais d'attente, le SQL non fiable et les frontières du système de fichiers :

  • Annulation de contexte déterministe (conn.go et errors.go) : Chaque appel au pilote arme une garde 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 qu'une attente de verrou s'exécute sur le serveur, la garde marque la connexion comme défectueuse, ferme le lien de session local et envoie une trame Cancel() hors bande. Le travailleur C invoque immédiatement sqlite3_interrupt(db), débloquant sqlite3_step, tandis que le pilote Go traduit le statut SQLITE_INTERRUPT résultant en context.Canceled ou context.DeadlineExceeded et demande à database/sql de remplacer la connexion dans le pool.
  • Cloisonnement des répertoires (sqlsrv_pathrules.c) : Le travailleur C applique les règles de chemin --allow-dir à chaque appel open_v2, en résolvant les liens symboliques et en rejetant les traversées relatives .. hors 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 ni sortir du répertoire de la base de données ni charger d'objets partagés arbitraires.

Disposition du Dépôt

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

 1gosuda.org/sqlite3
 2├── driver.go / api.go         # Enregistrement du pilote database/sql ("sqlite3") et API Connector
 3├── dsn.go                     # Analyseur DSN clean-room mattn/go-sqlite3 et configuration des pragmas
 4├── conn.go / tx.go            # Cycle de vie de session, garde de contexte et machine d'état des transactions
 5├── stmt.go / rows.go          # Pipelining de requêtes en vol unique et préchargement par lots de 64 KiB
 6├── errors.go                  # Codes d'erreur SQLite primaires/étendus typés et correspondance de sentinelles
 7├── autolaunch.go / hashes.go  # Matérialisation du serveur embarqué sans configuration et épinglage SHA-256
 8├── internal/embedded/         # Binaires //go:embed marqués par plateforme (linux, darwin, windows)
 9├── launcher/                  # Superviseur de processus configurable pour déploiements de serveurs personnalisés
10└── sqlite3/                   # Client Go hqq-bindgen généré et code source du serveur C11 SQLite
11    └── server/                # Implémentation du serveur C11 (sqlite3_hooks.c, sqlsrv_policy.c)

Premiers Pas

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