GoSuda

gosuda.org/sqlite3: CGO_ENABLED=0 database/sql Driver over Shared-Memory IPC

By Lemon Mint views ...

gosuda.org/sqlite3 er en database/sql-driver for Go med CGO_ENABLED=0 som kjører den offisielle C-SQLite 3.54.1-amalgamasjonen i en isolert arbeidsprosess via HQQ-delt minneringbuffere. Den registreres under det standardiserte driver-navnet "sqlite3", støtter DSN-spørringsparametere fra github.com/mattn/go-sqlite3, inkluderer SLSA-attesterte, forhåndskompilerte serverbinærfiler for seks målplattformer for operativsystem og arkitektur, og bruker forespørselspipelining med forhåndshenting av batcher på 64 KiB for å yte bedre enn in-process cgo ved spørringer som returnerer flere rader.

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

Hvorfor en ny Go-driver for SQLite?

SQLite er en av de mest utbredte programvarekomponentene i verden, men bruk av den fra Go har tradisjonelt tvunget utviklere til et ubehagelig valg mellom byggeenkelhet og trofasthet mot selve databasemotoren.

På den ene siden står github.com/mattn/go-sqlite3, den langvarige standarden i fellesskapet. Den omslutter den offisielle C-SQLite-amalgamasjonen direkte via cgo. Man får den reelle C-spørringsplanleggeren, full WAL-samtidighetssemantikk og forutsigbar minnehåndtering i C. Prisen er CGO_ENABLED=1: lokale bygg og CI-pipelines blir tregere, minimale scratch- eller distroless-containerbilder krever tilsvarende C-kjøretidsbiblioteker, krysskompilering til et annet operativsystem eller en annen CPU-arkitektur krever en fullstendig C-kryssverktøykjede, og eventuell minnekorrupsjon i C deler adresserommet med Go-prosessen.

På den andre siden finnes rene Go-portinger av SQLite som er generert ved mekanisk oversettelse av C-amalgamasjonen til Go-kildekode over et emulert C-kjøretidsmiljø (libc). De gjenoppretter CGO_ENABLED=0, noe som gjør byggene trivielle. Under vedvarende arbeidslast øker imidlertid den oversatte C-heapen og de per-rad-allokeringene i Go belastningen på søppelinnsamlingen, og subtile tilfeller knyttet til låsing, busy-timeouts eller avbrytelse av setninger kan avvike fra native C-bygg.

Vi bygde gosuda.org/sqlite3 for at Go-applikasjoner ikke skal måtte velge mellom CGO_ENABLED=0 og den reelle C-SQLite-motoren. Ved å kompilere den offisielle SQLite 3.54.1 C-amalgamasjonen til en dedikert server med delt minne (hqq-sqlite-server) og kommunisere over gosuda.org/hqq, kompileres din Go-binærfil med CGO_ENABLED=0, samtidig som den kjører uendret, -O3-optimalisert C-SQLite på tvers av en prosessgrense.

Konfigurasjonsfri kjøring: Fra go get til sql.Open

Å erstatte github.com/mattn/go-sqlite3 i en eksisterende Go-kodebase krever kun endring av den blanke importstien. Driveren registreres som "sqlite3" (og "hqq-sqlite" for eksplisitt valg) og godtar de samme DSN-spørrestrengene:

 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}

Innebygde SLSA-attesterte arbeidsbinærfiler

Et naturlig spørsmål når man hører at et bibliotek bruker en C-arbeidsprosess, er: "Må jeg installere eller distribuere en separat C-binærfil sammen med Go-programmet mitt?"

Nei. gosuda.org/sqlite3 inkluderer forhåndskompilerte hqq-sqlite-server-binærfiler for alle seks store målplattformer direkte i plattformmerkede Go-filer (internal/embedded/embed_<goos>_<goarch>.go). Fordi Gos byggebegrensninger kun kompilerer filen som samsvarer med målet ditt GOOS og GOARCH, vil krysskompilering med GOOS=linux GOARCH=arm64 go build automatisk kun inkludere den ca. 1,4 MiB store linux/arm64-serverbinærfilen.

MålsystemMålarkitektur (GOARCH)Innebygd binærfilByggeverktøy og proveniens
Linuxamd64, arm64hqq-sqlite-server_linux_<arch>GCC -O3, SLSA Build L3-proveniens
macOSarm64, amd64hqq-sqlite-server_darwin_<arch>AppleClang -O3, SLSA Build L3-proveniens
Windowsamd64hqq-sqlite-server_windows_amd64.exeMSVC -O2, SLSA Build L3-proveniens
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 innebygde binærfilen i brukerens cachekatalog ($XDG_CACHE_HOME/hqq/bin/<sha256>/), verifiserer dens kryptografiske SHA-256-sammendrag mot det fastlåste ReleaseHashes-kartet i hashes.go via gosuda.org/hqq/secureexec, og starter arbeidsprosessen med --parent-stdin slik at levetiden er strengt knyttet til Go-prosessen din.

Ren-rom-kompatibilitet med DSN for mattn/go-sqlite3

For å fungere som en reell drop-in-erstatning implementerer DSN-parseren (dsn.go) en ren-rom-replika av tilkoblings- og pragma-innstillingene som brukes på tvers av mattn/go-sqlite3-kodebaser:

DSN-spørringsparameterGodtatte verdierSQLite-effekt
_busy_timeout / _timeoutHeltall i millisekunder (f.eks. 5000)Kaller sqlite3_busy_timeout(db, ms) ved åpning av økt.
_journal_mode / _journalDELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFFUtfører PRAGMA journal_mode = <mode>.
_txlockimmediate, exclusive, deferredVelger BEGIN IMMEDIATE, BEGIN EXCLUSIVE eller BEGIN DEFERRED for db.BeginTx.
_foreign_keys / _fkon, true, 1, off, false, 0Utfører PRAGMA foreign_keys = ON/OFF.
_synchronous / _sync0/OFF, 1/NORMAL, 2/FULL, 3/EXTRAUtfører PRAGMA synchronous = <level>.
_cache_sizeHeltall i sider eller -KiB (f.eks. -20000)Utfører PRAGMA cache_size = <val>.
_locauto eller IANA-tidssone (f.eks. UTC, Asia/Seoul)Parser DATETIME/TIMESTAMP-tekstkolonner til *time.Time i målplasseringen.
modero, rw, rwc, memorySetter SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE eller SQLITE_OPEN_MEMORY.
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuumBoolske verdier eller modusheltallAnvender tilsvarende økt-PRAGMA ved initialisering av forbindelsen.

Å gjøre prosessisolasjon til en gjennomstrømningsfordel

Ved første øyekast kan det virke som om kjøring av SQLite i en egen prosess må være tregere enn å kalle sqlite3_step in-process via cgo. I praksis skjer det motsatte så snart en spørring returnerer mer enn én rad.

De skjulte kostnadene ved rad-for-rad-kall i cgo

I en konvensjonell cgo-driver for SQLite driver database/sql iterasjonen én rad og én kolonne om gangen. Lesing av 1 000 rader med 4 kolonner utløser:

  • 1 000 cgo-kall til sqlite3_step
  • 4 000 cgo-kall til sqlite3_column_type og sqlite3_column_int64 / sqlite3_column_text
  • Tusenvis av enkeltstående grensekryssinger mellom Go-skjedulereren og C-stakken

Forespørselspipelining og forhåndshenting av batcher på 64 KiB

gosuda.org/sqlite3 eliminerer grensekryssinger per rad ved å redesigne wire-protokollen rundt to mekanismer i stmt.go, rows.go og sqlite3/server/sqlite3_hooks.c:

 1Go-klient (stmt.go / rows.go)                       C11-arbeider (sqlite3_hooks.c)
 2-----------------------------                       ------------------------------
 31. Pipeline i 1 flyt:
 4   ResetAsync + BindValuesAsync
 5   + StmtInfoAsync + FetchRowsAsync(128)  =======>  reset -> bind -> stmt_info ->
 6                                                    løkke over sqlite3_step() opptil 128 rader
 7                                          <=======  Batch 1 (rad 1..128 pakket i 64 KiB-slot)
 82. Utløs forhåndshenting umiddelbart:
 9   FetchRowsAsync(128) (Batch 2)          =======>  løkke over sqlite3_step() for rad 129..256
10   Mens C steger Batch 2, skanner Go-kalleren
11   rad 1..128 fra Batch 1!                <=======  Batch 2 klar i delt minne
  1. Pipelining av spørringer i én flyt (stmt.go): Når QueryContext eller Stmt.QueryContext kjører en skrivebeskyttet setning, sender s.query() reset, bind_values, stmt_info (ved første kjøring) og fetch_rows asynkront i én enkelt flyt i delt minne. Alle fire operasjonene kjøres etter hverandre på C-arbeideren og returneres i én rundereise.
  2. Stramme C-batchløkker og asynkron forhåndshenting (rows.go): Inne i fetch_rows (sqlite3_hooks.c) kjører C-arbeideren sqlite3_step(stmt) i en nativ C-løkke og koder opptil FetchSize rader (standard 128) direkte inn i responsslotet i delt minne på 64 KiB, inntil batchgrensen er nådd, slotet er fullt eller SQLITE_DONE inntreffer. Så snart rows.ingest() har dekodet Batch 1, utløser den FetchRowsAsync for Batch 2 før den returnerer første rad til kalleren. Mens Go-koden din skanner Batch 1, steger C-arbeideren allerede Batch 2 på en annen CPU-kjerne.

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

Arbeidslastgosuda.org/sqlite3 (CGO_ENABLED=0)mattn/go-sqlite3 (CGO_ENABLED=1)Relativ hastighet
Punktvalg (QueryRow på PK)3.52 µs/op (9 B, 1 alloc)3.56 µs/op (232 B, 11 allok.)1,01x (paritet, 11x færre allok.)
Skanning av 100 rader (rows.Next)20.41 µs/op (30.5 KB, 211 allok.)36.34 µs/op (21.8 KB, 309 allok.)1,78x raskere
Skanning av 1 000 rader (rows.Next)125.92 µs/op (296 KB, 2 030 allok.)349.10 µs/op (216 KB, 3 009 allok.)2,77x raskere
Forberedt innsetting (autocommit)5.33 µs/op (0 B, 0 allok.)5.88 µs/op (120 B, 5 allok.)1,10x raskere (0 allok.)
Transaksjonsbatch med 100 rader189.99 µs/op (16 B, 1 alloc)191.50 µs/op (12.1 KB, 503 allok.)1,01x (500x færre allok.)

Kansellering, stisandboxing og gjenoppretting av tilkoblingspool

Å kjøre SQLite i en isolert arbeider styrker også måten driveren håndterer tidsavbrudd, upålitelig SQL og filsystemgrenser på:

  • Deterministisk kansellering av kontekst (conn.go og errors.go): Hvert driverkall aktiverer en lettvekts context.AfterFunc-vakt (cn.guard). Dersom kallerens context.Context kanselleres, eller fristen utløper, mens en treg rekursiv spørring eller låsevent kjører på serveren, markerer vakten tilkoblingen som ugyldig, dreper den lokale sesjonsforbindelsen og sender en Cancel()-ramme utenfor båndet. C-arbeideren kaller sqlite3_interrupt(db) umiddelbart, slik at sqlite3_step frigjøres, mens Go-driveren oversetter den resulterende SQLITE_INTERRUPT-statusen til context.Canceled eller context.DeadlineExceeded og instruerer database/sql om å erstatte tilkoblingen i poolen.
  • Katalogsandboxing (sqlsrv_pathrules.c): C-arbeideren håndhever --allow-dir-stiregler ved hvert open_v2-kall, løser symbolske lenker og avviser relativ ..-traversering utenfor det tillatte katalogen. sqlite3_load_extension og SQLITE_LIMIT_ATTACHED er deaktivert som standard, slik at SQL-injeksjon ikke kan unnslippe databasekatalogen eller laste inn vilkårlige delte objekter.

Repositoriets struktur

Repositoriet er strukturert slik at pakken på toppnivå leverer den drop-in database/sql-driveren, mens launcher/ og sqlite3/ forblir tilgjengelige for avanserte distribusjoner med egendefinert server:

 1gosuda.org/sqlite3
 2├── driver.go / api.go         # Registrering av database/sql-driver ("sqlite3") og Connector-API
 3├── dsn.go                     # Ren-rom DSN-parser for mattn/go-sqlite3 og pragma-konfigurasjon
 4├── conn.go / tx.go            # Livssyklus for økt, kontekstvakt og tilstandsmaskin for transaksjoner
 5├── stmt.go / rows.go          # Pipelining av forespørsler i én flyt og forhåndshenting av batcher på 64 KiB
 6├── errors.go                  # Typede primære og utvidede SQLite-feilkoder og matching av sentinelverdier
 7├── autolaunch.go / hashes.go  # Konfigurasjonsfri materialisering av innebygd server og SHA-256-fastlåsing
 8├── internal/embedded/         # Plattformmerkede //go:embed-binærfiler (linux, darwin, windows)
 9├── launcher/                  # Konfigurerbar prosessovervåker for egendefinerte serverdistribusjoner
10└── sqlite3/                   # Generert hqq-bindgen Go-klient og C11-kildekode for SQLite-server
11    └── server/                # C11-serverimplementasjon (sqlite3_hooks.c, sqlsrv_policy.c)

Kom i gang

Installer modulen med go get og åpne databasen din med sql.Open("sqlite3", dsn):

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