GoSuda

gosuda.org/sqlite3: database/sql-driver uden CGO (CGO_ENABLED=0) over delt hukommelse-IPC

By Lemon Mint views ...

gosuda.org/sqlite3 er en database/sql-driver til Go med CGO_ENABLED=0, som kører det upstream C SQLite 3.54.1-amalgamation i en isoleret workerproces over HQQ-shared-memory-ringbuffere. Den registreres under det standardiserede driver-navn "sqlite3", understøtter DSN-forespørgselsparametre fra github.com/mattn/go-sqlite3, indlejrer SLSA-attesterede, prækompilerede serverbinære filer for seks operativsystem- og arkitekturmål og anvender request pipelining med 64 KiB batch-prefetching for at yde bedre end in-process cgo ved forespørgsler, der returnerer flere rækker.

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

Hvorfor endnu en Go SQLite-driver?

SQLite er et af de mest udbredte softwareprodukter i verden, men brugen heraf fra Go har historisk tvunget udviklere til et ubehageligt valg mellem byggesimpilicitet og motorens troværdighed.

På den ene side står github.com/mattn/go-sqlite3, den mangeårige fællesskabsstandard. Den indpakker den officielle C SQLite-amalgamation direkte via cgo. Man opnår den reelle C-forespørgselsplanlægger, fuld WAL-samtidighedssemantik og forudsigelig C-hukommelseshåndtering. Prisen er CGO_ENABLED=1: lokale builds og CI-pipelines bliver langsommere, minimale scratch- eller distroless-containerbilleder kræver matchende C-runtimebiblioteker, krydskompilering til et andet operativsystem eller en anden CPU-arkitektur kræver en fuld C-krydsværktøjskæde, og enhver hukommelseskorruption i C deler adresserummet med Go-processen.

På den anden side findes rene Go-porteringer af SQLite, som er genereret ved mekanisk at oversætte C-amalgamationen til Go-kildekode over et emuleret C-runtime (libc). De genetablerer CGO_ENABLED=0, hvilket gør builds trivielle. Under vedvarende arbejdsbyrder øger den oversatte C-heap og allokeringer pr. række i Go dog presset på garbage collection, og subtile randtilfælde i låsning, busy-timeouts eller afbrydelse af statements kan afvige fra native C-builds.

Vi byggede gosuda.org/sqlite3, så Go-applikationer ikke behøver at vælge mellem CGO_ENABLED=0 og den reelle C SQLite-motor. Ved at kompilere den officielle SQLite 3.54.1 C-amalgamation til en dedikeret shared-memory-server (hqq-sqlite-server) og kommunikere over gosuda.org/hqq kompileres din Go-binær med CGO_ENABLED=0, mens uændret, -O3-optimeret C SQLite eksekveres på tværs af en procesgrænse.

Konfigurationsfri eksekvering: Fra go get til sql.Open

Udskiftning af github.com/mattn/go-sqlite3 i en eksisterende Go-kodebase kræver kun ændring af den blanke import-sti. Driveren registreres som "sqlite3" (og "hqq-sqlite" for eksplicit valg) og accepterer de samme DSN-forespørgselsstrenge:

 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}

Indlejrede SLSA-attesterede worker-binære filer

Et naturligt spørgsmål, når man hører, at et bibliotek anvender en C-workerproces, er: "Skal jeg installere eller distribuere en separat C-binær fil sammen med mit Go-program?"

Nej. gosuda.org/sqlite3 indlejrer prækompilerede hqq-sqlite-server-binære filer for alle seks største mål direkte i platformsmærkede Go-filer (internal/embedded/embed_<goos>_<goarch>.go). Da Gos build-begrænsninger kun kompilerer filen, der matcher dit mål-GOOS og GOARCH, indlejrer krydskompilering med GOOS=linux GOARCH=arm64 go build automatisk kun den ca. 1,4 MiB store linux/arm64-serverbinær.

MålsystemMålarkitektur (GOARCH)Indlejret binærBuild-værktøjskæde & proveniens
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

Når sql.Open etablerer sin første forbindelse, materialiserer autolaunch.go den indlejrede binær i brugerens cache-mappe ($XDG_CACHE_HOME/hqq/bin/<sha256>/), verificerer dens kryptografiske SHA-256-digest mod den fastlåste ReleaseHashes-map i hashes.go via gosuda.org/hqq/secureexec og starter workeren med --parent-stdin, så dens levetid er strengt bundet til din Go-proces.

Clean-room-kompatibilitet med mattn/go-sqlite3 DSN

For at fungere som en reel drop-in-erstatning implementerer DSN-parseren (dsn.go) en clean-room-replika af de forbindelses- og pragma-indstillinger, der anvendes på tværs af mattn/go-sqlite3-kodebaser:

DSN-forespørgselsparameterAccepterede værdierSQLite-effekt
_busy_timeout / _timeoutHeltal i millisekunder (f.eks. 5000)Kalder sqlite3_busy_timeout(db, ms) ved sessionsåbning.
_journal_mode / _journalDELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFFUdfører PRAGMA journal_mode = <mode>.
_txlockimmediate, exclusive, deferredVælger BEGIN IMMEDIATE, BEGIN EXCLUSIVE eller BEGIN DEFERRED for db.BeginTx.
_foreign_keys / _fkon, true, 1, off, false, 0Udfører PRAGMA foreign_keys = ON/OFF.
_synchronous / _sync0/OFF, 1/NORMAL, 2/FULL, 3/EXTRAUdfører PRAGMA synchronous = <level>.
_cache_sizeHeltal i sider eller -KiB (f.eks. -20000)Udfører PRAGMA cache_size = <val>.
_locauto eller IANA-tidszone (f.eks. UTC, Asia/Seoul)Parser DATETIME/TIMESTAMP-tekstkolonner til *time.Time i målplaceringen.
modero, rw, rwc, memorySætter SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE eller SQLITE_OPEN_MEMORY.
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuumBoolesk værdi eller tilstandsheltalAnvender den tilsvarende session-PRAGMA ved forbindelsesinitialisering.

Gør procesisolation til en gennemstrømningsfordel

Ved første øjekast lyder det, som om kørsel af SQLite i en separat proces nødvendigvis må være langsommere end at kalde sqlite3_step in-process via cgo. I praksis sker det modsatte, så snart en forespørgsel returnerer mere end en enkelt række.

De skjulte omkostninger ved rækkevise cgo-kald

I en konventionel cgo-SQLite-driver driver database/sql iterationen en række og en kolonne ad gangen. Læsning af 1.000 rækker med 4 kolonner udløser:

  • 1.000 cgo-kald til sqlite3_step
  • 4.000 cgo-kald til sqlite3_column_type og sqlite3_column_int64 / sqlite3_column_text
  • Tusindvis af individuelle grænseovergange mellem Go-scheduleren og C-stakken

Request pipelining og 64 KiB batch-prefetching

gosuda.org/sqlite3 eliminerer grænseovergange pr. række ved at omdesigne wire-protokollen omkring to mekanismer i stmt.go, rows.go og sqlite3/server/sqlite3_hooks.c:

 1Go-klient (stmt.go / rows.go)                       C11-worker (sqlite3_hooks.c)
 2-----------------------------                       ----------------------------
 31. Pipeline i 1 flight:
 4   ResetAsync + BindValuesAsync
 5   + StmtInfoAsync + FetchRowsAsync(128)  =======>  reset -> bind -> stmt_info ->
 6                                                    loop sqlite3_step() op til 128 rækker
 7                                          <=======  Batch 1 (rækker 1..128 pakket i 64 KiB slot)
 82. Udløs straks prefetch:
 9   FetchRowsAsync(128) (Batch 2)          =======>  loop sqlite3_step() for rækker 129..256
10   Mens C kører trin for Batch 2, scanner
11   Go-kalderen rækkerne 1..128 fra Batch 1!  <=======  Batch 2 klar i shared memory
  1. Single-flight forespørgselspipelining (stmt.go): Når QueryContext eller Stmt.QueryContext kører et skrivebeskyttet statement, indsender s.query() asynkront reset, bind_values, stmt_info (ved første udførelse) og fetch_rows i ét enkelt shared-memory-flight. Alle fire operationer udføres lige efter hinanden på C-workeren og returneres i én round trip.
  2. Stramme C-batch-løkker og asynkron prefetch (rows.go): Inde i fetch_rows (sqlite3_hooks.c) kører C-workeren sqlite3_step(stmt) i en native C-løkke og koder op til FetchSize rækker (standard 128) direkte ind i det 64 KiB store shared-memory-svarslot, indtil batchgrænsen er nået, slotten er fuld, eller SQLITE_DONE opstår. Så snart rows.ingest() har afkodet Batch 1, udløser den FetchRowsAsync for Batch 2, før den returnerer den første række til kalderen. Mens din Go-kode scanner Batch 1, kører C-workeren allerede trin for Batch 2 på en anden CPU-kerne.

På Apple M4 (darwin/arm64) viser sammenligningen mellem gosuda.org/sqlite3 (CGO_ENABLED=0) og github.com/mattn/go-sqlite3 (CGO_ENABLED=1) på den samme SQLite-database effekten af batching og pipelining:

Arbejdsbyrdegosuda.org/sqlite3 (CGO_ENABLED=0)mattn/go-sqlite3 (CGO_ENABLED=1)Relativ hastighed
Punktselect (QueryRow på PK)3,52 µs/op (9 B, 1 alloc)3,56 µs/op (232 B, 11 allocs)1,01x (paritet, 11x færre allocs)
100-rækkers scanning (rows.Next)20,41 µs/op (30,5 KB, 211 allocs)36,34 µs/op (21,8 KB, 309 allocs)1,78x hurtigere
1.000-rækkers scanning (rows.Next)125,92 µs/op (296 KB, 2.030 allocs)349,10 µs/op (216 KB, 3.009 allocs)2,77x hurtigere
Prepared insert (autocommit)5,33 µs/op (0 B, 0 allocs)5,88 µs/op (120 B, 5 allocs)1,10x hurtigere (0 allocs)
100-rækkers transaktionsbatch189,99 µs/op (16 B, 1 alloc)191,50 µs/op (12,1 KB, 503 allocs)1,01x (500x færre allocs)

Annullering, stisandboxing og genopretning af connection pool

At køre SQLite i en isoleret worker styrker også, hvordan driveren håndterer timeouts, utroværdig SQL og filsystemgrænser:

  • Deterministisk annullering af context (conn.go og errors.go): Hvert driverkald aktiverer en letvægts context.AfterFunc-vagt (cn.guard). Hvis kaldekonteksten context.Context annulleres, eller dens deadline udløber, mens en langsom rekursiv forespørgsel eller låsevent kører på serveren, markerer vagten forbindelsen som ubrugelig, lukker den lokale sessionsforbindelse og sender en out-of-band Cancel()-ramme. C-workeren kalder straks sqlite3_interrupt(db), hvilket frigør sqlite3_step, mens Go-driveren oversætter den resulterende SQLITE_INTERRUPT-status til context.Canceled eller context.DeadlineExceeded og instruerer database/sql om at erstatte forbindelsen i poolen.
  • Mappesandboxing (sqlsrv_pathrules.c): C-workeren håndhæver --allow-dir-stiregler ved hvert open_v2-kald, løser symlinks og afviser relativ ..-traversering uden for den tilladte mappe. sqlite3_load_extension og SQLITE_LIMIT_ATTACHED er deaktiveret som standard, så SQL-injektion ikke kan undslippe databasemappen eller indlæse vilkårlige delte objekter.

Repository-struktur

Repositoryet er struktureret således, at pakken på øverste niveau leverer den drop-in database/sql-driver, mens launcher/ og sqlite3/ forbliver tilgængelige for avancerede brugerdefinerede serverdeployments:

 1gosuda.org/sqlite3
 2├── driver.go / api.go         # Registrering af database/sql-driver ("sqlite3") og Connector-API
 3├── dsn.go                     # Clean-room DSN-parser for mattn/go-sqlite3 og pragma-konfiguration
 4├── conn.go / tx.go            # Sessionslivscyklus, kontekstvagt og transaktionstilstandsmaskine
 5├── stmt.go / rows.go          # Single-flight-forespørgselspipelining og 64 KiB batch-prefetching
 6├── errors.go                  # Typede SQLite primære/udvidede fejlkoder og sentinel-matching
 7├── autolaunch.go / hashes.go  # Konfigurationsfri materialisering af indlejret server og SHA-256-fastlåsning
 8├── internal/embedded/         # Platformsmærkede //go:embed-binære filer (linux, darwin, windows)
 9├── launcher/                  # Konfigurerbar procesovervågning til brugerdefinerede serverdeployments
10└── sqlite3/                   # Genereret hqq-bindgen Go-klient og C11 SQLite-serverkilde
11    └── server/                # C11-serverimplementering (sqlite3_hooks.c, sqlsrv_policy.c)

Kom i gang

Installer modulet med go get og åbn din database med sql.Open("sqlite3", dsn):

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