GoSuda

gosuda.org/sqlite3: A Drop-In database/sql Driver Running Native C SQLite Over Shared Memory

By Lemon Mint
views ...

gosuda.org/sqlite3 je ovladač database/sql pro Go s nastavením CGO_ENABLED=0, který spouští upstreamový C SQLite 3.54.1 amalgam uvnitř izolovaného procesního workeru prostřednictvím kruhových vyrovnávacích pamětí se sdílenou pamětí HQQ. Registruje se pod standardním názvem ovladače "sqlite3", podporuje parametry dotazů DSN z knihovny github.com/mattn/go-sqlite3, vkládá předem sestavené binární soubory serveru ověřené pomocí SLSA pro šest cílových operačních systémů a architektur a využívá pipelining požadavků s dávkovým prefetchingem o velikosti 64 KiB, čímž 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 je jedním z nejrozšířenějších softwarových produktů na světě, přesto jeho používání z jazyka Go historicky nutilo inženýry k nepříjemné volbě mezi jednoduchostí sestavení a věrností enginu.

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

Na druhé straně stojí čistě goovské implementace SQLite generované mechanickým převodem C amalgamu do zdrojového kódu Go nad emulovaným C runtime (libc). Obnovují CGO_ENABLED=0, což činí sestavení triviálním. Avšak při dlouhodobém zatížení zvyšují přeložená C halda a alokace v Go na jeden řádek tlak na garbage collector a jemné okrajové případy v zamykání, timeoutech při zaneprázdnění nebo přerušení příkazů se mohou lišit od nativních C sestavení.

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

Bezkonfigurační spuštění: Od go get po sql.Open

Nahrazení github.com/mattn/go-sqlite3 v existující kódové základně Go vyžaduje změnu pouze prázdné cesty importu. Ovladač se registruje jako "sqlite3" (a "hqq-sqlite" pro explicitní výběr) a akceptuje stejné řetězce dotazů 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é binární soubory workeru ověřené pomocí SLSA

Přirozenou otázkou při zjištění, že knihovna používá C proces workeru, je: "Musím instalovat nebo distribuovat samostatný C binární soubor společně s mým programem v Go?"

Ne. gosuda.org/sqlite3 vkládá předem sestavené binární soubory hqq-sqlite-server pro všech šest hlavních cílů přímo uvnitř souborů Go označených platformou (internal/embedded/embed_<goos>_<goarch>.go). Vzhledem k tomu, že sestavovací omezení jazyka Go kompilují pouze soubor odpovídající vašemu cílovému GOOS a GOARCH, cross-kompilace pomocí GOOS=linux GOARCH=arm64 go build automaticky vložení pouze binárního souboru serveru linux/arm64 o velikosti cca 1,4 MiB.

Cílový OSCílová arch (GOARCH)Vložený binární souborSestavovací toolchain a provenience
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í připojení, soubor autolaunch.go materializuje vložený binární soubor do adresáře mezipaměti uživatele ($XDG_CACHE_HOME/hqq/bin/<sha256>/), ověří jeho kryptografický součet SHA-256 oproti připnuté mapě ReleaseHashes v souboru hashes.go prostřednictvím gosuda.org/hqq/secureexec a spustí worker s přepínačem --parent-stdin, takže jeho životnost je striktně vázána na váš proces Go.

Čistá kompatibilita DSN pro mattn/go-sqlite3

Aby fungoval jako skutečná náhrada typu drop-in, analyzátor DSN (dsn.go) implementuje čistou repliku připojení a pragma přepínačů používaných napříč kódovými základnami mattn/go-sqlite3:

Parametr dotazu DSNAkceptované hodnotyÚčinek v SQLite
_busy_timeout / _timeoutCelá čísla v milisekundách (např. 5000)Při otevření relace zavolá sqlite3_busy_timeout(db, ms).
_journal_mode / _journalDELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFFProvede PRAGMA journal_mode = <mode>.
_txlockimmediate, exclusive, deferredZvolí 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é číslo stránek nebo -KiB (např. -20000)Provede PRAGMA cache_size = <val>.
_locauto nebo časové pásmo IANA (např. UTC, Asia/Seoul)Analyzuje textové sloupcové hodnoty DATETIME/TIMESTAMP do *time.Time v cílovém umístění.
modero, rw, rwc, memoryNastaví SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE nebo SQLITE_OPEN_MEMORY.
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuumBooleanové hodnoty nebo celá čísla režimuAplikuje odpovídající relaci PRAGMA při inicializaci připojení.

Přeměna izolace procesů v propustnostní výhodu

Na první pohled se zdá, že spuštění SQLite v samostatném procesu musí být pomalejší než volání sqlite3_step v rámci procesu pomocí cgo. V praxi dochází k opačnému jevu, jakmile dotaz vrací více než jediný řádek.

Skryté náklady na volání cgo řádek po řádku

V konvenčním ovladači SQLite založeném na cgo řídí database/sql iteraci po jednom řádku a jednom sloupci. Přečtení 1 000 řádků se 4 sloupci spouští:

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

Pipelining požadavků a dávkový prefetching o velikosti 64 KiB

gosuda.org/sqlite3 eliminuje přechody hranic na úrovni jednotlivých řádků přepracováním síťového protokolu kolem dvou mechanismů v souborech stmt.go, rows.go a 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. Jednosměrný pipelining dotazů (stmt.go): Když QueryContext nebo Stmt.QueryContext spustí příkaz pouze pro čtení, s.query() odešle asynchronně reset, bind_values, stmt_info (při prvním spuštění) a fetch_rows v jediném přenosu sdílené paměti. Všechny čtyři operace se provedou bezprostředně za sebou na C workeru a vrátí se v jedné zpáteční cestě.
  2. Úzké dávkové smyčky C a asynchronní prefetching (rows.go): Uvnitř fetch_rows (sqlite3_hooks.c) provádí C worker kroky sqlite3_step(stmt) v nativní smyčce jazyka C, přičemž kóduje až FetchSize řádků (výchozí hodnota 128) přímo do slotu odpovědi se sdílenou pamětí o velikosti 64 KiB, dokud není dosaženo limitu dávky, slot není plný nebo nenastane SQLITE_DONE. Jakmile rows.ingest() dešifruje dávku Batch 1, spustí FetchRowsAsync pro dávku Batch 2 před vrácením prvního řádku volajícímu. Zatímco váš kód v Go prohledává dávku Batch 1, C worker již zpracovává dávku Batch 2 na jiném jádře CPU.

Na Apple M4 (darwin/arm64) porovnání gosuda.org/sqlite3 (CGO_ENABLED=0) s github.com/mattn/go-sqlite3 (CGO_ENABLED=1) na stejné databázi SQLite ukazuje vliv dávkování a pipeliningu:

Pracovní 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í)
Prohledání 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ší
Prohledání 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ý vklad (Autocommit)5.33 µs/op (0 B, 0 alokací)5.88 µs/op (120 B, 5 alokací)1,10x rychlejší (0 alokací)
Dávka transakce 100 řádků189.99 µs/op (16 B, 1 alokace)191.50 µs/op (12,1 KB, 503 alokací)1,01x (500x méně alokací)

Zrušení, sandboxování cest a obnova fondu připojení

Spouštění SQLite v izolovaném workeru rovněž posiluje způsob, jakým ovladač zvládá timeouty, nedůvěryhodné SQL dotazy 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 kontext context.Context volajícího zrušen nebo vyprší jeho časový limit (deadline) během probíhajícího pomalého rekurzivního dotazu nebo čekání na zámek na serveru, ochrana označí připojení jako vadné, ukončí místní odkaz relace a odešle mimo-pásmový (out-of-band) rámec Cancel(). C worker okamžitě vyvolá sqlite3_interrupt(db), čímž odblokuje sqlite3_step, zatímco ovladač Go mapuje výsledný stav SQLITE_INTERRUPT na context.Canceled nebo context.DeadlineExceeded a instruuje database/sql k nahrazení připojení ve fondu.
  • Sandboxování adresářů (sqlsrv_pathrules.c): C worker vynucuje pravidla cest --allow-dir při každém volání open_v2, přičemž řeší symbolické odkazy a odmítá relativní průchod pomocí .. mimo povolený adresář. sqlite3_load_extension a SQLITE_LIMIT_ATTACHED jsou ve výchozím nastavení zakázány, takže injektáž SQL nemůže uniknout z adresáře databáze ani načíst libovolné sdílené objekty.

Uspořádání úložiště

Úložiště je strukturováno tak, že balíček nejvyšší úrovně poskytuje ovladač database/sql typu drop-in, zatímco launcher/ a sqlite3/ zůstávají přístupné pro pokročilá nasazení vlastních serverů:

 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)

Začínáme

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

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