GoSuda

gosuda.org/sqlite3: CGO_ENABLED=0 database/sql-stuurprogramma via gedeeld-geheugen-IPC

By Lemon Mint views ...

gosuda.org/sqlite3 is een CGO_ENABLED=0 database/sql-driver voor Go die de upstream C SQLite 3.54.1 amalgamatie uitvoert binnen een geïsoleerd workerproces via HQQ shared-memory rings. Het registreert zich onder de standaard driver-naam "sqlite3", ondersteunt DSN-queryparameters van github.com/mattn/go-sqlite3, bevat SLSA-geattesteerde vooraf gecompileerde serverbinaries voor zes besturingssysteem- en architectuurdoelen, en gebruikt request pipelining met 64 KiB batch prefetching om in-process cgo te overtreffen bij query's die meerdere rijen opleveren.

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

Waarom nog een Go SQLite-driver?

SQLite is een van de meest gebruikte softwarecomponenten ter wereld, maar het gebruik ervan vanuit Go heeft ingenieurs historisch gedwongen tot een ongemakkelijke keuze tussen eenvoud van de build en fidelity van de engine.

Aan de ene kant staat github.com/mattn/go-sqlite3, de langdurige standaard binnen de gemeenschap. Deze bindt de officiële C SQLite-amalgamatie rechtstreeks via cgo. Men krijgt de echte C-queryplanner, volledige WAL-concurrencysemantiek en voorspelbaar C-geheugenbeheer. De prijs is CGO_ENABLED=1: lokale builds en CI-pijplijnen worden trager, minimale scratch- of distroless-containerimages vereisen overeenkomende C-runtimebibliotheken, cross-compilen naar een ander besturingssysteem of CPU-architectuur vereist een volledige C-cross-toolchain, en elke geheugencorruptie in C deelt de adresruimte van het Go-proces.

Aan de andere kant staan pure-Go SQLite-ports die zijn gegenereerd door de C-amalgamatie mechanisch te vertalen naar Go-broncode over een geëmuleerde C-runtime (libc). Deze herstellen CGO_ENABLED=0, wat builds triviaal maakt. Onder aanhoudende werkbelastingen verhogen de vertaalde C-heap en de allocaties per rij in Go echter de druk op de garbage collector, en subtiele randgevallen in locking, busy timeouts of statement-interruptie kunnen afwijken van native C-builds.

Wij hebben gosuda.org/sqlite3 gebouwd zodat Go-applicaties niet hoeven te kiezen tussen CGO_ENABLED=0 en de echte C SQLite-engine. Door de officiële SQLite 3.54.1 C-amalgamatie te compileren in een toegewijde shared-memory-server (hqq-sqlite-server) en te communiceren via gosuda.org/hqq, wordt uw Go-binary gecompileerd met CGO_ENABLED=0 terwijl ongewijzigde, -O3-geoptimaliseerde C SQLite wordt uitgevoerd over een procesgrens heen.

Configuratievrije uitvoering: van go get tot sql.Open

Het vervangen van github.com/mattn/go-sqlite3 in een bestaande Go-codebase vereist alleen het wijzigen van het blank import-pad. De driver registreert zich als "sqlite3" (en "hqq-sqlite" voor expliciete selectie) en accepteert dezelfde DSN-querystrings:

 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}

Ingebedde SLSA-geattesteerde workerbinaries

Een natuurlijke vraag wanneer men hoort dat een bibliotheek een C-workerproces gebruikt, is: "Moet ik een afzonderlijke C-binary installeren of meeleveren naast mijn Go-programma?"

Nee. gosuda.org/sqlite3 bevat vooraf gecompileerde hqq-sqlite-server-binaries voor alle zes belangrijke doelen, rechtstreeks ingebed in platformgetagde Go-bestanden (internal/embedded/embed_<goos>_<goarch>.go). Omdat de build-constraints van Go alleen het bestand compileren dat overeenkomt met uw doel-GOOS en GOARCH, bevat GOOS=linux GOARCH=arm64 go build automatisch alleen de ongeveer 1,4 MiB grote linux/arm64-serverbinary.

Doel-OSDoel-architectuur (GOARCH)Ingebedde binaryBuildtoolchain en herkomst
Linuxamd64, arm64hqq-sqlite-server_linux_<arch>GCC -O3, SLSA Build L3 Provenance
macOSarm64, amd64hqq-sqlite-server_darwin_<arch>AppleClang -O3, SLSA Build L3 Provenance
Windowsamd64hqq-sqlite-server_windows_amd64.exeMSVC -O2, SLSA Build L3 Provenance
Windowsarm64hqq-sqlite-server_windows_arm64.exezig cc -target aarch64-windows-gnu -O3, SLSA L3

Wanneer sql.Open de eerste verbinding tot stand brengt, materialiseert autolaunch.go de ingebedde binary in de gebruikerscachemap ($XDG_CACHE_HOME/hqq/bin/<sha256>/), verifieert het cryptografische SHA-256-digest ervan tegen de vastgepinde ReleaseHashes-map in hashes.go via gosuda.org/hqq/secureexec, en start de worker met --parent-stdin, zodat de levensduur ervan strikt is gekoppeld aan uw Go-proces.

Clean-room mattn/go-sqlite3 DSN-compatibiliteit

Om als werkelijk drop-in-vervanging te fungeren, implementeert de DSN-parser (dsn.go) een clean-room replica van de verbindings- en pragma-instellingen die in mattn/go-sqlite3-codebases worden gebruikt:

DSN-queryparameterGeaccepteerde waardenEffect in SQLite
_busy_timeout / _timeoutGeheel getal in milliseconden (bijv. 5000)Roept sqlite3_busy_timeout(db, ms) aan bij het openen van de sessie.
_journal_mode / _journalDELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFFVoert PRAGMA journal_mode = <mode> uit.
_txlockimmediate, exclusive, deferredSelecteert BEGIN IMMEDIATE, BEGIN EXCLUSIVE of BEGIN DEFERRED voor db.BeginTx.
_foreign_keys / _fkon, true, 1, off, false, 0Voert PRAGMA foreign_keys = ON/OFF uit.
_synchronous / _sync0/OFF, 1/NORMAL, 2/FULL, 3/EXTRAVoert PRAGMA synchronous = <level> uit.
_cache_sizeGeheel aantal pagina's of -KiB (bijv. -20000)Voert PRAGMA cache_size = <val> uit.
_locauto of IANA-tijdzone (bijv. UTC, Asia/Seoul)Parseert DATETIME/TIMESTAMP-tekstkolommen naar *time.Time in de doellocatie.
modero, rw, rwc, memoryStelt SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE of SQLITE_OPEN_MEMORY in.
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuumBoolean of modusgeheeltallenPast de bijbehorende sessie-PRAGMA toe bij initialisatie van de verbinding.

Procesisolatie omzetten in een doorvoervoordeel

Op het eerste gezicht lijkt het uitvoeren van SQLite in een afzonderlijk proces noodzakelijkerwijs trager dan het aanroepen van sqlite3_step in-process via cgo. In de praktijk gebeurt het tegenovergestelde zodra een query meer dan één rij teruggeeft.

De verborgen kosten van rij-voor-rij cgo-aanroepen

In een conventionele cgo-SQLite-driver doorloopt database/sql de resultaten één rij en één kolom tegelijk. Het lezen van 1.000 rijen met 4 kolommen veroorzaakt:

  • 1.000 cgo-aanroepen naar sqlite3_step
  • 4.000 cgo-aanroepen naar sqlite3_column_type en sqlite3_column_int64 / sqlite3_column_text
  • Duizenden afzonderlijke grensovergangen tussen de Go-scheduler en de C-stack

Request pipelining en 64 KiB batch prefetching

gosuda.org/sqlite3 elimineert grensovergangen per rij door het wire-protocol te herontwerpen rond twee mechanismen in stmt.go, rows.go en 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. Single-Flight Query Pipelining (stmt.go): Wanneer QueryContext of Stmt.QueryContext een alleen-lezen statement uitvoert, dient s.query() reset, bind_values, stmt_info (bij eerste uitvoering) en fetch_rows asynchroon in één enkele shared-memory flight in. Alle vier bewerkingen worden achter elkaar op de C-worker uitgevoerd en keren terug in één round trip.
  2. Strakke C-batchlussen en asynchrone prefetch (rows.go): Binnen fetch_rows (sqlite3_hooks.c) voert de C-worker sqlite3_step(stmt) uit in een native C-lus en codeert hij tot FetchSize rijen (standaard 128) rechtstreeks in de 64 KiB shared-memory responseslot, totdat de batchlimiet is bereikt, het slot vol is of SQLITE_DONE optreedt. Zodra rows.ingest() Batch 1 decodeert, vuurt het FetchRowsAsync voor Batch 2 af voordat de eerste rij aan de aanroeper wordt teruggegeven. Terwijl uw Go-code Batch 1 scant, staat de C-worker al op een andere CPU-kern Batch 2 te stappen.

Op Apple M4 (darwin/arm64) toont een vergelijking van gosuda.org/sqlite3 (CGO_ENABLED=0) met github.com/mattn/go-sqlite3 (CGO_ENABLED=1) op dezelfde SQLite-database het effect van batching en pipelining:

Werkbelastinggosuda.org/sqlite3 (CGO_ENABLED=0)mattn/go-sqlite3 (CGO_ENABLED=1)Relatieve snelheid
Point Select (QueryRow op PK)3,52 µs/op (9 B, 1 alloc)3,56 µs/op (232 B, 11 allocs)1,01x (gelijk, 11x minder allocs)
100-rijen scan (rows.Next)20,41 µs/op (30,5 KB, 211 allocs)36,34 µs/op (21,8 KB, 309 allocs)1,78x sneller
1.000-rijen scan (rows.Next)125,92 µs/op (296 KB, 2.030 allocs)349,10 µs/op (216 KB, 3.009 allocs)2,77x sneller
Prepared Insert (Autocommit)5,33 µs/op (0 B, 0 allocs)5,88 µs/op (120 B, 5 allocs)1,10x sneller (0 allocs)
100-rijen transactiebatch189,99 µs/op (16 B, 1 alloc)191,50 µs/op (12,1 KB, 503 allocs)1,01x (500x minder allocs)

Annulering, padsandboxing en herstel van de connection pool

Het uitvoeren van SQLite in een geïsoleerde worker versterkt ook de manier waarop de driver omgaat met time-outs, onbetrouwbare SQL en bestandssysteemgrenzen:

  • Deterministische contextannulering (conn.go en errors.go): Elke driveraanroep activeert een lichtgewicht context.AfterFunc-guard (cn.guard). Als de context.Context van de aanroeper wordt geannuleerd of de deadline verstrijkt terwijl een trage recursieve query of lock-wachttijd op de server draait, markeert de guard de verbinding als defect, verbreekt de lokale sessieverbinding en stuurt een out-of-band Cancel()-frame. De C-worker roept onmiddellijk sqlite3_interrupt(db) aan, waardoor sqlite3_step wordt ontblokkeerd, terwijl de Go-driver de resulterende SQLITE_INTERRUPT-status vertaalt naar context.Canceled of context.DeadlineExceeded en database/sql opdraagt de verbinding in de pool te vervangen.
  • Directorysandboxing (sqlsrv_pathrules.c): De C-worker handhaaft --allow-dir-padregels bij elke open_v2-aanroep, lost symbolische koppelingen op en weigert relatieve ..-doorloop buiten de toegestane map. sqlite3_load_extension en SQLITE_LIMIT_ATTACHED zijn standaard uitgeschakeld, zodat SQL-injectie niet uit de databasemap kan ontsnappen of willekeurige gedeelde objecten kan laden.

Repositorystructuur

De repository is zo gestructureerd dat het pakket op het hoogste niveau de drop-in database/sql-driver biedt, terwijl launcher/ en sqlite3/ toegankelijk blijven voor geavanceerde implementaties met aangepaste servers:

 1gosuda.org/sqlite3
 2├── driver.go / api.go         # Registratie van de database/sql-driver ("sqlite3") en Connector-API
 3├── dsn.go                     # Clean-room mattn/go-sqlite3 DSN-parser en pragma-configuratie
 4├── conn.go / tx.go            # Sessielevenscyclus, contextguard en transactiestatusmachine
 5├── stmt.go / rows.go          # Single-flight request pipelining en 64 KiB batch prefetching
 6├── errors.go                  # Getypeerde SQLite primaire/uitgebreide foutcodes en sentinelmatching
 7├── autolaunch.go / hashes.go  # Configuratievrije materialisatie van de ingebedde server en SHA-256-pinning
 8├── internal/embedded/         # Platformgetagde //go:embed-binaries (linux, darwin, windows)
 9├── launcher/                  # Configureerbare processupervisor voor aangepaste serverimplementaties
10└── sqlite3/                   # Gegenereerde hqq-bindgen Go-client en C11 SQLite-serverbroncode
11    └── server/                # C11-serverimplementatie (sqlite3_hooks.c, sqlsrv_policy.c)

Aan de slag

Installeer de module met go get en open uw database met sql.Open("sqlite3", dsn):

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