GoSuda

gosuda.org/sqlite3: Driver database/sql cu CGO_ENABLED=0 prin IPC bazat pe memorie partajată

By Lemon Mint views ...

gosuda.org/sqlite3 este un driver database/sql pentru Go, cu CGO_ENABLED=0, care rulează amalgamarea C SQLite 3.54.1 din upstream într-un proces worker izolat, prin inele de memorie partajată HQQ. Se înregistrează sub numele standard de driver "sqlite3", acceptă parametrii de interogare DSN din github.com/mattn/go-sqlite3, încorporează binare de server precompilate, atestate SLSA, pentru șase ținte de sistem de operare și arhitectură, și utilizează pipelining al cererilor și prefetch de loturi de 64 KiB pentru a depăși cgo în proces la interogările cu mai multe rânduri.

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

De ce încă un driver SQLite pentru Go?

SQLite este unul dintre cele mai utilizate produse software din lume, totuși utilizarea sa din Go a obligat, în mod tradițional, inginerii să aleagă între simplitatea compilării și fidelitatea motorului.

Pe de o parte se află github.com/mattn/go-sqlite3, standardul comunitar de lungă durată. Acesta încapsulează direct amalgamarea oficială C SQLite prin cgo. Obțineți planificatorul real de interogări C, semantica completă de concurență WAL și gestionarea predictibilă a memoriei C. Costul constă în CGO_ENABLED=1: compilările locale și pipeline-urile CI se încetinesc, imaginile de container minimale (scratch sau distroless) necesită biblioteci runtime C compatibile, compilarea încrucișată către alt sistem de operare sau altă arhitectură CPU necesită un lanț complet de instrumente C pentru compilare încrucișată, iar orice coruperea memoriei în C împarte spațiul de adrese al procesului Go.

Pe de altă parte există porturi SQLite pur Go, generate prin traducerea mecanică a amalgamării C în cod sursă Go, peste un runtime C emulat (libc). Acestea restabilesc CGO_ENABLED=0, ceea ce face compilarea banală. În sarcini susținute, totuși, heap-ul C tradus și alocările Go pentru fiecare rând sporesc presiunea colectării de gunoi, iar cazuri-limită subtile în blocare, în timeout-uri de ocupare sau în întreruperea instrucțiunilor pot diverge de la compilările C native.

Am construit gosuda.org/sqlite3 pentru ca aplicațiile Go să nu fie nevoite să aleagă între CGO_ENABLED=0 și motorul real C SQLite. Prin compilarea amalgamării C oficiale SQLite 3.54.1 într-un server dedicat cu memorie partajată (hqq-sqlite-server) și prin comunicarea prin gosuda.org/hqq, binarul dumneavoastră Go se compilează cu CGO_ENABLED=0, executând în același timp SQLite C nemodificat, optimizat cu -O3, peste o frontieră de proces.

Execuție fără configurare: de la go get la sql.Open

Înlocuirea github.com/mattn/go-sqlite3 într-o bază de cod Go existentă necesită modificarea doar a căii de import anonim. Driverul se înregistrează ca "sqlite3" (și ca "hqq-sqlite" pentru selecție explicită) și acceptă aceleași șiruri de interogare 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}

Binare worker încorporate, atestate SLSA

O întrebare firească, atunci când aflați că o bibliotecă utilizează un proces worker C, este: „Trebuie să instalez sau să livrez un binar C separat alături de programul meu Go?”

Nu. gosuda.org/sqlite3 încorporează binare hqq-sqlite-server precompilate pentru toate cele șase ținte principale, direct în fișiere Go etichetate pe platformă (internal/embedded/embed_<goos>_<goarch>.go). Deoarece constrângerile de build ale Go compilează doar fișierul corespunzător țintei dumneavoastră GOOS și GOARCH, compilarea încrucișată GOOS=linux GOARCH=arm64 go build încorporează automat doar binarul de server linux/arm64, de aproximativ 1,4 MiB.

Sistem de operare țintăArhitectură țintă (GOARCH)Binar încorporatLanț de compilare și proveniență
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

Când sql.Open stabilește prima conexiune, autolaunch.go materializează binarul încorporat în directorul cache al utilizatorului ($XDG_CACHE_HOME/hqq/bin/<sha256>/), verifică digestul criptografic SHA-256 al acestuia în raport cu harta fixată ReleaseHashes din hashes.go, prin intermediul gosuda.org/hqq/secureexec, și lansează worker-ul cu --parent-stdin, astfel încât durata sa de viață să fie strict legată de procesul dumneavoastră Go.

Compatibilitate DSN mattn/go-sqlite3 realizată prin reimplementare independentă

Pentru a funcționa ca un înlocuitor real, fără modificări, parserul DSN (dsn.go) implementează o replică realizată prin reimplementare independentă a parametrilor de conexiune și de pragma utilizați în bazele de cod mattn/go-sqlite3:

Parametru de interogare DSNValori acceptateEfect SQLite
_busy_timeout / _timeoutNumăr întreg de milisecunde (ex. 5000)Apelează sqlite3_busy_timeout(db, ms) la deschiderea sesiunii.
_journal_mode / _journalDELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFFExecută PRAGMA journal_mode = <mode>.
_txlockimmediate, exclusive, deferredSelectează BEGIN IMMEDIATE, BEGIN EXCLUSIVE sau BEGIN DEFERRED pentru db.BeginTx.
_foreign_keys / _fkon, true, 1, off, false, 0Execută PRAGMA foreign_keys = ON/OFF.
_synchronous / _sync0/OFF, 1/NORMAL, 2/FULL, 3/EXTRAExecută PRAGMA synchronous = <level>.
_cache_sizeNumăr întreg de pagini sau -KiB (ex. -20000)Execută PRAGMA cache_size = <val>.
_locauto sau fus orar IANA (ex. UTC, Asia/Seoul)Analizează coloanele text DATETIME/TIMESTAMP și le convertește în *time.Time în locația țintă.
modero, rw, rwc, memorySetează SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE sau SQLITE_OPEN_MEMORY.
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuumValori booleene sau numere întregi de modAplică PRAGMA de sesiune corespunzător la inițializarea conexiunii.

Transformarea izolării proceselor într-un avantaj de debit

La prima vedere, rularea SQLite într-un proces separat pare să fie obligatoriu mai lentă decât apelarea sqlite3_step în proces prin cgo. În practică, se întâmplă exact opusul de îndată ce o interogare returnează mai mult de un singur rând.

Costul ascuns al apelurilor cgo rând cu rând

Într-un driver SQLite convențional bazat pe cgo, database/sql parcurge rezultatele câte un rând și câte o coloană. Citirea a 1.000 de rânduri cu 4 coloane declanșează:

  • 1.000 de apeluri cgo către sqlite3_step
  • 4.000 de apeluri cgo către sqlite3_column_type și sqlite3_column_int64 / sqlite3_column_text
  • mii de treceri individuale ale frontierei între planificatorul Go și stiva C

Pipelining al cererilor și prefetch de loturi de 64 KiB

gosuda.org/sqlite3 elimină trecerile de frontieră pe rând prin reproiectarea protocolului de transport în jurul a două mecanisme din stmt.go, rows.go și 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 al interogărilor pe un singur zbor (stmt.go): Când QueryContext sau Stmt.QueryContext rulează o instrucțiune doar pentru citire, s.query() trimite asincron reset, bind_values, stmt_info (la prima execuție) și fetch_rows într-un singur zbor prin memoria partajată. Toate cele patru operații se execută una după alta pe worker-ul C și revin într-o singură rundă de comunicare.
  2. Bucle compacte de lot în C și prefetch asincron (rows.go): În interiorul fetch_rows (sqlite3_hooks.c), worker-ul C execută sqlite3_step(stmt) într-o buclă C nativă, codificând până la FetchSize rânduri (implicit 128) direct în slotul de răspuns de 64 KiB din memoria partajată, până la atingerea limitei de lot, până la umplerea slotului sau până la apariția SQLITE_DONE. Imediat ce rows.ingest() decodifică Lotul 1, trimite FetchRowsAsync pentru Lotul 2, înainte de a returna primul rând apelantului. Cât timp codul dumneavoastră Go parcurge Lotul 1, worker-ul C execută deja pașii pentru Lotul 2 pe un alt nucleu CPU.

Pe Apple M4 (darwin/arm64), compararea gosuda.org/sqlite3 (CGO_ENABLED=0) cu github.com/mattn/go-sqlite3 (CGO_ENABLED=1) pe aceeași bază de date SQLite arată efectul grupării în loturi și al pipelining-ului:

Sarcină de lucrugosuda.org/sqlite3 (CGO_ENABLED=0)mattn/go-sqlite3 (CGO_ENABLED=1)Viteză relativă
Point Select (QueryRow după PK)3.52 µs/op (9 B, 1 alocare)3.56 µs/op (232 B, 11 alocări)1.01x (paritate, cu 11x mai puține alocări)
Parcurgere de 100 de rânduri (rows.Next)20.41 µs/op (30.5 KB, 211 alocări)36.34 µs/op (21.8 KB, 309 alocări)1.78x mai rapid
Parcurgere de 1.000 de rânduri (rows.Next)125.92 µs/op (296 KB, 2.030 alocări)349.10 µs/op (216 KB, 3.009 alocări)2.77x mai rapid
Insert pregătit (Autocommit)5.33 µs/op (0 B, 0 alocări)5.88 µs/op (120 B, 5 alocări)1.10x mai rapid (0 alocări)
Lot de tranzacție de 100 de rânduri189.99 µs/op (16 B, 1 alocare)191.50 µs/op (12.1 KB, 503 alocări)1.01x (de 500x mai puține alocări)

Anulare, sandboxing al căilor și recuperarea pool-ului de conexiuni

Rularea SQLite într-un worker izolat consolidează și modul în care driverul gestionează timeout-urile, SQL-ul nesigur și frontierele sistemului de fișiere:

  • Anulare deterministă a contextului (conn.go și errors.go): Fiecare apel al driverului armează o gardă ușoară context.AfterFunc (cn.guard). Dacă context.Context al apelantului este anulat sau își expiră termenul în timp ce o interogare recursivă lentă sau o așteptare pentru blocare rulează pe server, garda marchează conexiunea ca defectuoasă, închide legătura locală de sesiune și trimite un cadru Cancel() out-of-band. Worker-ul C invocă imediat sqlite3_interrupt(db), deblocând sqlite3_step, în timp ce driverul Go mapează statusul rezultat SQLITE_INTERRUPT la context.Canceled sau context.DeadlineExceeded și îi cere database/sql să înlocuiască conexiunea în pool.
  • Sandboxing al directoarelor (sqlsrv_pathrules.c): Worker-ul C aplică regulile de cale --allow-dir la fiecare apel open_v2, rezolvând legăturile simbolice și respingând traversarea relativă .. în afara directorului permis. sqlite3_load_extension și SQLITE_LIMIT_ATTACHED sunt dezactivate implicit, astfel încât injecția SQL nu poate scăpa din directorul bazei de date sau nu poate încărca obiecte partajate arbitrare.

Structura depozitului

Depozitul este structurat astfel încât pachetul de nivel superior să furnizeze driverul database/sql gata de înlocuire, în timp ce launcher/ și sqlite3/ rămân accesibile pentru implementări avansate cu server personalizat:

 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)

Primii pași

Instalați modulul cu go get și deschideți baza de date cu sql.Open("sqlite3", dsn):

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