GoSuda

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

By Lemon Mint views ...

gosuda.org/sqlite3 je ovladač database/sql pro Go s CGO_ENABLED=0, který spouští upstream C SQLite 3.54.1 amalgamaci uvnitř izolovaného pracovního procesu přes sdílené paměťové kruhové fronty HQQ. Registruje se pod standardním názvem ovladače "sqlite3", podporuje parametry DSN dotazu z github.com/mattn/go-sqlite3, obsahuje vložené předkompilované serverové binárky s SLSA atestací pro šest cílových operačních systémů a architektur a využívá pipelining požadavků s předběžným načítáním dávek o velikosti 64 KiB, díky čemuž překonává in-process cgo u dotazů vracejících více řádků.

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

Proč další ovladač SQLite pro Go?

SQLite patří mezi nejrozšířenější software na světě, přesto se její používání v Go historicky vynucovalo volbu mezi jednoduchostí sestavení a věrností enginu, která engineery stavěla do nepohodlné situace.

Na jedné straně stojí github.com/mattn/go-sqlite3, dlouhodobý komunitní standard. Přímo obaluje oficiální C amalgamaci SQLite prostřednictvím cgo. Získáváte skutečný C plánovač dotazů, plnou sémantiku souběžnosti WAL a předvídatelnou správu paměti v C. Cenou je CGO_ENABLED=1: lokální sestavení a CI pipeline se zpomalují, minimální scratch nebo distroless kontejnerové obrazy vyžadují odpovídající C běhové knihovny, křížová kompilace pro jiný operační systém nebo architekturu CPU vyžaduje kompletní C křížový toolchain a jakákoli poškozená paměť v C sdílí adresový prostor procesu Go.

Na druhé straně stojí čistě Go porty SQLite, vytvořené mechanickým překladem C amalgamace do zdrojového kódu Go nad emulovaným C runtime (libc). Obnovují CGO_ENABLED=0, což činí sestavení triviálním. Při dlouhodobé zátěži však přeložená C halda a alokace Go pro každý řádek zvyšují tlak na garbage collector a jemné okrajové případy při zamykání, časových limitech busy nebo přerušování příkazů se mohou odchylovat od nativních C sestavení.

Vytvořili jsme gosuda.org/sqlite3, aby Go aplikace nemusely volit mezi CGO_ENABLED=0 a skutečným C enginem SQLite. Kompilací oficiální C amalgamace SQLite 3.54.1 do vyhrazeného serveru se sdílenou pamětí (hqq-sqlite-server) a komunikací přes gosuda.org/hqq se vaše Go binárka sestavuje s CGO_ENABLED=0, zatímco vykonává nezměněnou C SQLite optimalizovanou pomocí -O3 napříč hranicí procesu.

Konfigurace bez nastavení: od go get k sql.Open

Nahrazení github.com/mattn/go-sqlite3 v existující Go kódové základně vyžaduje změnit pouze cestu k prázdnému importu. Ovladač se registruje jako "sqlite3" (a "hqq-sqlite" pro explicitní výběr) a přijímá stejné řetězce 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}

Vložené pracovní binárky s SLSA atestací

Přirozenou otázkou, když se dozvíte, že knihovna používá pracovní proces v C, je: „Musím vedle svého Go programu instalovat nebo distribuovat samostatnou C binárku?“

Ne. gosuda.org/sqlite3 vkládá předkompilované binárky hqq-sqlite-server pro všech šest hlavních cílů přímo do souborů Go označených podle platformy (internal/embedded/embed_<goos>_<goarch>.go). Protože build constraints Go kompilují pouze soubor odpovídající vašemu cílovému GOOS a GOARCH, křížová kompilace příkazem GOOS=linux GOARCH=arm64 go build automaticky vloží pouze binárku serveru linux/arm64 o velikosti přibližně 1,4 MiB.

Cílový OSCílová architektura (GOARCH)Vložená binárkaBuild toolchain a provenance
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

Když sql.Open naváže své první spojení, autolaunch.go materializuje vloženou binárku do uživatelského cache adresáře ($XDG_CACHE_HOME/hqq/bin/<sha256>/), ověří její kryptografický digest SHA-256 proti připnuté mapě ReleaseHashes v hashes.go prostřednictvím gosuda.org/hqq/secureexec a spustí pracovní proces s parametrem --parent-stdin, takže jeho životnost je přísně svázána s vaším Go procesem.

Kompatibilita DSN s mattn/go-sqlite3 v čistém provedení

Aby mohl fungovat jako skutečná náhrada bez nutnosti úprav, parser DSN (dsn.go) implementuje čistě navržený (clean-room) model parametrů připojení a pragma nastavení používaných napříč kódovými základnami mattn/go-sqlite3:

Parametr DSN dotazuPřijímané hodnotyÚčinek v SQLite
_busy_timeout / _timeoutCelé číslo v milisekundách (např. 5000)Volá sqlite3_busy_timeout(db, ms) při otevření relace.
_journal_mode / _journalDELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFFProvede PRAGMA journal_mode = <mode>.
_txlockimmediate, exclusive, deferredVybírá BEGIN IMMEDIATE, BEGIN EXCLUSIVE nebo BEGIN DEFERRED pro db.BeginTx.
_foreign_keys / _fkon, true, 1, off, false, 0Provede PRAGMA foreign_keys = ON/OFF.
_synchronous / _sync0/OFF, 1/NORMAL, 2/FULL, 3/EXTRAProvede PRAGMA synchronous = <level>.
_cache_sizeCelý počet stránek nebo -KiB (např. -20000)Provede PRAGMA cache_size = <val>.
_locauto nebo IANA časové pásmo (např. UTC, Asia/Seoul)Převádí textové sloupce DATETIME/TIMESTAMP na *time.Time v cílové lokalitě.
modero, rw, rwc, memoryNastavuje SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE nebo SQLITE_OPEN_MEMORY.
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuumBooleovská hodnota nebo celočíselný režimAplikuje odpovídající relační PRAGMA při inicializaci připojení.

Proměna izolace procesů ve výhodu propustnosti

Na první pohled se může zdát, že spouštění SQLite v odděleném procesu musí být pomalejší než volání sqlite3_step in-process přes cgo. V praxi se však jako první ukáže opak, jakmile dotaz vrátí více než jeden řádek.

Skrytá cena volání cgo řádek po řádku

V konvenčním ovladači SQLite založeném na cgo database/sql řídí iteraci po jednom řádku a jednom sloupci. Čtení 1 000 řádků se čtyřmi sloupci vyvolá:

  • 1 000 volání cgo na sqlite3_step
  • 4 000 volání cgo na sqlite3_column_type a sqlite3_column_int64 / sqlite3_column_text
  • Tisíce jednotlivých přechodů hranic mezi plánovačem Go a zásobníkem C

Pipelining požadavků a předběžné načítání dávek po 64 KiB

gosuda.org/sqlite3 odstraňuje přechody hranic u každého řádku tím, že přepracovává protokol na úrovni drátu kolem dvou mechanismů v stmt.go, rows.go a sqlite3/server/sqlite3_hooks.c:

 1Go klient (stmt.go / rows.go)                       C11 worker (sqlite3_hooks.c)
 2-----------------------------                       ----------------------------
 31. Pipeline v 1 letu:
 4   ResetAsync + BindValuesAsync
 5   + StmtInfoAsync + FetchRowsAsync(128)  =======>  reset -> bind -> stmt_info ->
 6                                                    smyčka sqlite3_step() až 128 řádků
 7                                          <=======  Dávka 1 (řádky 1..128 zabalené ve slotu 64 KiB)
 82. Okamžitě spustit předběžné načtení:
 9   FetchRowsAsync(128) (Dávka 2)          =======>  smyčka sqlite3_step() pro řádky 129..256
10   Zatímco C krokuje Dávku 2, Go volající
11   čte řádky 1..128 z Dávky 1!            <=======  Dávka 2 připravena ve sdílené paměti
  1. Pipelining dotazu v jednom letu (stmt.go): Když QueryContext nebo Stmt.QueryContext spustí příkaz pouze pro čtení, s.query() odešle reset, bind_values, stmt_info (při prvním spuštění) a fetch_rows asynchronně v jediném letu přes sdílenou paměť. Všechny čtyři operace se vykonávají za sebou na pracovním procesu C a vrátí se v jednom round tripu.
  2. Úzké dávkové smyčky v C a asynchronní předběžné načítání (rows.go): Uvnitř fetch_rows (sqlite3_hooks.c) pracovní proces C krokuje sqlite3_step(stmt) v nativní smyčce v C a kóduje až FetchSize řádků (výchozí 128) přímo do odpovědního slotu sdílené paměti o velikosti 64 KiB, dokud není dosažen limit dávky, slot se nezaplní nebo nenastane SQLITE_DONE. Jakmile rows.ingest() dekóduje Dávku 1, spustí FetchRowsAsync pro Dávku 2 ještě předtím, než vrátí první řádek volajícímu. Zatímco váš Go kód prochází Dávku 1, pracovní proces C již krokuje Dávku 2 na jiném jádru CPU.

Na Apple M4 (darwin/arm64) ukazuje srovnání gosuda.org/sqlite3 (CGO_ENABLED=0) s github.com/mattn/go-sqlite3 (CGO_ENABLED=1) nad touže databází SQLite efekt dávkování a pipeliningu:

Zátěžgosuda.org/sqlite3 (CGO_ENABLED=0)mattn/go-sqlite3 (CGO_ENABLED=1)Relativní rychlost
Bodový výběr (QueryRow podle PK)3.52 µs/op (9 B, 1 alokace)3.56 µs/op (232 B, 11 alokací)1.01x (parita, 11x méně alokací)
Čtení 100 řádků (rows.Next)20.41 µs/op (30.5 KB, 211 alokací)36.34 µs/op (21.8 KB, 309 alokací)1.78x rychlejší
Čtení 1 000 řádků (rows.Next)125.92 µs/op (296 KB, 2 030 alokací)349.10 µs/op (216 KB, 3 009 alokací)2.77x rychlejší
Připravený insert (autocommit)5.33 µs/op (0 B, 0 alokací)5.88 µs/op (120 B, 5 alokací)1.10x rychlejší (0 alokací)
Dávka 100 řádků v transakci189.99 µs/op (16 B, 1 alokace)191.50 µs/op (12.1 KB, 503 alokací)1.01x (500x méně alokací)

Zrušení, sandboxing cest a obnova poolu připojení

Spouštění SQLite v izolovaném pracovním procesu také posiluje způsob, jakým ovladač zpracovává časové limity, nedůvěryhodné SQL a hranice souborového systému:

  • Deterministické zrušení kontextu (conn.go a errors.go): Každé volání ovladače aktivuje lehkou ochranu context.AfterFunc (cn.guard). Pokud je context.Context volajícího zrušen nebo jeho termín vyprší, zatímco na serveru běží pomalý rekurzivní dotaz nebo čekání na zámek, ochrana označí připojení jako chybné, ukončí lokální odkaz relace a odešle mimopásmový rámec Cancel(). Pracovní proces C okamžitě zavolá sqlite3_interrupt(db), čímž uvolní sqlite3_step, zatímco ovladač Go mapuje výsledný stav SQLITE_INTERRUPT na context.Canceled nebo context.DeadlineExceeded a pokyne database/sql, aby nahradil připojení v poolu.
  • Sandboxing adresářů (sqlsrv_pathrules.c): Pracovní proces C vynucuje pravidla cest --allow-dir při každém volání open_v2, rozlišuje symbolické odkazy a odmítá relativní průchod .. mimo povolený adresář. sqlite3_load_extension a SQLITE_LIMIT_ATTACHED jsou ve výchozím nastavení zakázány, aby SQL injection nemohla uniknout z adresáře databáze nebo načíst libovolné sdílené objekty.

Struktura repozitáře

Repozitář je uspořádán tak, že hlavní balíček poskytuje ovladač database/sql pro přímou náhradu, zatímco launcher/ a sqlite3/ zůstávají přístupné pro pokročilá nasazení s vlastním serverem:

 1gosuda.org/sqlite3
 2├── driver.go / api.go         # Registrace ovladače database/sql ("sqlite3") a Connector API
 3├── dsn.go                     # Parser DSN mattn/go-sqlite3 v čistém provedení a konfigurace pragma
 4├── conn.go / tx.go            # Životní cyklus relace, ochrana kontextu a stavový automat transakcí
 5├── stmt.go / rows.go          # Pipelining požadavků v jednom letu a předběžné načítání dávek po 64 KiB
 6├── errors.go                  # Typované primární a rozšířené chybové kódy SQLite a shoda sentinelů
 7├── autolaunch.go / hashes.go  # Zero-config materializace vloženého serveru a připnutí SHA-256
 8├── internal/embedded/         # Binárky //go:embed označené platformou (linux, darwin, windows)
 9├── launcher/                  # Konfigurovatelný supervisor procesů pro vlastní nasazení serveru
10└── sqlite3/                   # Generovaný hqq-bindgen Go klient a zdrojový kód serveru C11 SQLite
11    └── server/                # Implementace serveru C11 (sqlite3_hooks.c, sqlsrv_policy.c)

Začínáme

Nainstalujte modul příkazem go get a otevřete databázi pomocí sql.Open("sqlite3", dsn):

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