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

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 încorporat | Lanț de compilare și proveniență |
|---|---|---|---|
| Linux | amd64, arm64 | hqq-sqlite-server_linux_<arch> | GCC -O3, SLSA Build L3 Provenance |
| macOS | arm64, amd64 | hqq-sqlite-server_darwin_<arch> | AppleClang -O3, SLSA Build L3 Provenance |
| Windows | amd64 | hqq-sqlite-server_windows_amd64.exe | MSVC -O2, SLSA Build L3 Provenance |
| Windows | arm64 | hqq-sqlite-server_windows_arm64.exe | zig 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 DSN | Valori acceptate | Efect SQLite |
|---|---|---|
_busy_timeout / _timeout | Număr întreg de milisecunde (ex. 5000) | Apelează sqlite3_busy_timeout(db, ms) la deschiderea sesiunii. |
_journal_mode / _journal | DELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFF | Execută PRAGMA journal_mode = <mode>. |
_txlock | immediate, exclusive, deferred | Selectează BEGIN IMMEDIATE, BEGIN EXCLUSIVE sau BEGIN DEFERRED pentru db.BeginTx. |
_foreign_keys / _fk | on, true, 1, off, false, 0 | Execută PRAGMA foreign_keys = ON/OFF. |
_synchronous / _sync | 0/OFF, 1/NORMAL, 2/FULL, 3/EXTRA | Execută PRAGMA synchronous = <level>. |
_cache_size | Număr întreg de pagini sau -KiB (ex. -20000) | Execută PRAGMA cache_size = <val>. |
_loc | auto sau fus orar IANA (ex. UTC, Asia/Seoul) | Analizează coloanele text DATETIME/TIMESTAMP și le convertește în *time.Time în locația țintă. |
mode | ro, rw, rwc, memory | Setează SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE sau SQLITE_OPEN_MEMORY. |
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuum | Valori booleene sau numere întregi de mod | Aplică 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
cgocătresqlite3_step - 4.000 de apeluri
cgocătresqlite3_column_typeșisqlite3_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
- Pipelining al interogărilor pe un singur zbor (
stmt.go): CândQueryContextsauStmt.QueryContextrulează o instrucțiune doar pentru citire,s.query()trimite asincronreset,bind_values,stmt_info(la prima execuție) șifetch_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. - Bucle compacte de lot în C și prefetch asincron (
rows.go): În interiorulfetch_rows(sqlite3_hooks.c), worker-ul C executăsqlite3_step(stmt)într-o buclă C nativă, codificând până laFetchSizerâ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țiaSQLITE_DONE. Imediat cerows.ingest()decodifică Lotul 1, trimiteFetchRowsAsyncpentru 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 lucru | gosuda.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ânduri | 189.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șierrors.go): Fiecare apel al driverului armează o gardă ușoarăcontext.AfterFunc(cn.guard). Dacăcontext.Contextal 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 cadruCancel()out-of-band. Worker-ul C invocă imediatsqlite3_interrupt(db), deblocândsqlite3_step, în timp ce driverul Go mapează statusul rezultatSQLITE_INTERRUPTlacontext.Canceledsaucontext.DeadlineExceededși îi ceredatabase/sqlsă înlocuiască conexiunea în pool. - Sandboxing al directoarelor (
sqlsrv_pathrules.c): Worker-ul C aplică regulile de cale--allow-dirla fiecare apelopen_v2, rezolvând legăturile simbolice și respingând traversarea relativă..în afara directorului permis.sqlite3_load_extensionșiSQLITE_LIMIT_ATTACHEDsunt 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
- Depozit GitHub: https://github.com/gosuda/hqq-sqlite3
- Referință DSN și pragma: docs/DSN.md
- Arhitectură și protocol de transport: docs/ARCHITECTURE.md