GoSuda

gosuda.org/sqlite3: driver database/sql con CGO_ENABLED=0 basato su IPC a memoria condivisa

By Lemon Mint views ...

gosuda.org/sqlite3 è un driver database/sql per Go con CGO_ENABLED=0 che esegue l'amalgama C di SQLite 3.54.1 upstream all'interno di un processo worker isolato, comunicando tramite gli anelli di memoria condivisa di HQQ. Si registra con il nome standard del driver "sqlite3", supporta i parametri di query del DSN di github.com/mattn/go-sqlite3, incorpora binari server precompilati con attestazione SLSA per sei target di sistema operativo e architettura, e utilizza il pipelining delle richieste con prefetch di batch da 64 KiB per superare cgo in-process nelle query a più righe.

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

Perché un Altro Driver SQLite per Go?

SQLite è una delle componenti software più diffuse al mondo, eppure il suo utilizzo da Go ha storicamente costretto gli ingegneri a una scelta scomoda tra semplicità di build e fedeltà del motore.

Da un lato si colloca github.com/mattn/go-sqlite3, il tradizionale standard della comunità. Esso integra direttamente l'amalgama C ufficiale di SQLite tramite cgo. Si ottengono il vero query planner C, la piena semantica di concorrenza WAL e una gestione della memoria C prevedibile. Il costo è CGO_ENABLED=1: le build locali e le pipeline CI rallentano, le immagini container minimali (scratch o distroless) richiedono librerie runtime C compatibili, la cross-compilazione verso un altro sistema operativo o un'altra architettura CPU richiede una toolchain C cross-compilante completa, e qualsiasi corruzione della memoria in C condivide lo spazio degli indirizzi del processo Go.

Dall'altro lato vi sono i port SQLite puri in Go, generati traducendo meccanicamente l'amalgama C in codice sorgente Go su un runtime C emulato (libc). Essi ripristinano CGO_ENABLED=0, rendendo le build banali. Sotto carichi di lavoro prolungati, tuttavia, l'heap C tradotto e le allocazioni Go per riga aumentano la pressione sul garbage collector, e sottili casi limite nel locking, nei busy timeout o nell'interruzione delle istruzioni possono divergere dalle build C native.

Abbiamo realizzato gosuda.org/sqlite3 affinché le applicazioni Go non debbano scegliere tra CGO_ENABLED=0 e il vero motore C di SQLite. Compilando l'amalgama C ufficiale di SQLite 3.54.1 in un server dedicato a memoria condivisa (hqq-sqlite-server) e comunicando tramite gosuda.org/hqq, il binario Go viene compilato con CGO_ENABLED=0 pur eseguendo SQLite C non modificato e ottimizzato con -O3 attraverso un confine di processo.

Esecuzione Senza Configurazione: da go get a sql.Open

Sostituire github.com/mattn/go-sqlite3 in una base di codice Go esistente richiede di modificare solo il percorso dell'import anonimo. Il driver si registra come "sqlite3" (e come "hqq-sqlite" per una selezione esplicita) e accetta le stesse stringhe di query 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}

Binari Worker Incorporati con Attestazione SLSA

Una domanda naturale, quando si apprende che una libreria utilizza un processo worker C, è: "Devo installare o distribuire un binario C separato insieme al mio programma Go?"

No. gosuda.org/sqlite3 incorpora binari hqq-sqlite-server precompilati per tutti e sei i target principali, direttamente all'interno di file Go con tag di piattaforma (internal/embedded/embed_<goos>_<goarch>.go). Poiché i vincoli di build di Go compilano solo il file corrispondente al target GOOS e GOARCH, la cross-compilazione con GOOS=linux GOARCH=arm64 go build incorpora automaticamente solo il binario server linux/arm64 di circa 1,4 MiB.

Sistema Operativo TargetArchitettura Target (GOARCH)Binario IncorporatoToolchain di Build e Provenienza
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

Quando sql.Open stabilisce la sua prima connessione, autolaunch.go materializza il binario incorporato nella directory della cache utente ($XDG_CACHE_HOME/hqq/bin/<sha256>/), verifica il suo digest crittografico SHA-256 rispetto alla mappa ReleaseHashes fissata in hashes.go tramite gosuda.org/hqq/secureexec, e avvia il worker con --parent-stdin, così che la sua durata sia strettamente vincolata al processo Go.

Compatibilità DSN Clean-Room con mattn/go-sqlite3

Per funzionare come un vero sostituto diretto, il parser DSN (dsn.go) implementa una replica clean-room delle opzioni di connessione e dei pragma utilizzati nelle basi di codice mattn/go-sqlite3:

Parametro di Query DSNValori AccettatiEffetto in SQLite
_busy_timeout / _timeoutIntero in millisecondi (es. 5000)Chiama sqlite3_busy_timeout(db, ms) all'apertura della sessione.
_journal_mode / _journalDELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFFEsegue PRAGMA journal_mode = <mode>.
_txlockimmediate, exclusive, deferredSeleziona BEGIN IMMEDIATE, BEGIN EXCLUSIVE o BEGIN DEFERRED per db.BeginTx.
_foreign_keys / _fkon, true, 1, off, false, 0Esegue PRAGMA foreign_keys = ON/OFF.
_synchronous / _sync0/OFF, 1/NORMAL, 2/FULL, 3/EXTRAEsegue PRAGMA synchronous = <level>.
_cache_sizeIntero di pagine o -KiB (es. -20000)Esegue PRAGMA cache_size = <val>.
_locauto o fuso orario IANA (es. UTC, Asia/Seoul)Analizza le colonne di testo DATETIME/TIMESTAMP in *time.Time nella località di destinazione.
modero, rw, rwc, memoryImposta SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE o SQLITE_OPEN_MEMORY.
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuumValori booleani o interi di modalitàApplica il corrispondente PRAGMA di sessione all'inizializzazione della connessione.

Trasformare l'Isolamento di Processo in un Vantaggio di Throughput

A prima vista, eseguire SQLite in un processo separato sembra dover essere più lento che invocare sqlite3_step in-process tramite cgo. In pratica accade l'opposto non appena una query restituisce più di una singola riga.

Il Costo Nascosto delle Chiamate cgo Riga per Riga

In un tipico driver SQLite basato su cgo, database/sql guida l'iterazione una riga e una colonna alla volta. La lettura di 1.000 righe con 4 colonne comporta:

  • 1.000 chiamate cgo a sqlite3_step
  • 4.000 chiamate cgo a sqlite3_column_type e sqlite3_column_int64 / sqlite3_column_text
  • migliaia di singoli attraversamenti di confine tra lo scheduler Go e lo stack C

Pipelining delle Richieste e Prefetch dei Batch da 64 KiB

gosuda.org/sqlite3 elimina gli attraversamenti di confine per ogni riga riprogettando il protocollo di comunicazione attorno a due meccanismi in stmt.go, rows.go e 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 delle Query a Singolo Volo (stmt.go): Quando QueryContext o Stmt.QueryContext esegue un'istruzione di sola lettura, s.query() invia in modo asincrono reset, bind_values, stmt_info (alla prima esecuzione) e fetch_rows in un unico volo nella memoria condivisa. Tutte e quattro le operazioni vengono eseguite in sequenza sul worker C e restituiscono il risultato in un solo viaggio di andata e ritorno.
  2. Cicli Batch C Stretti e Prefetch Asincrono (rows.go): All'interno di fetch_rows (sqlite3_hooks.c), il worker C esegue sqlite3_step(stmt) in un ciclo C nativo, codificando fino a FetchSize righe (predefinito 128) direttamente nello slot di risposta in memoria condivisa da 64 KiB, fino al raggiungimento del limite del batch, al riempimento dello slot o al verificarsi di SQLITE_DONE. Non appena rows.ingest() decodifica il Batch 1, invia FetchRowsAsync per il Batch 2 prima di restituire la prima riga al chiamante. Mentre il codice Go scandisce il Batch 1, il worker C sta già eseguendo i passi per il Batch 2 su un altro core della CPU.

Su Apple M4 (darwin/arm64), confrontando gosuda.org/sqlite3 (CGO_ENABLED=0) con github.com/mattn/go-sqlite3 (CGO_ENABLED=1) sullo stesso database SQLite, emerge l'effetto del batching e del pipelining:

Carico di Lavorogosuda.org/sqlite3 (CGO_ENABLED=0)mattn/go-sqlite3 (CGO_ENABLED=1)Velocità Relativa
Selezione Puntuale (QueryRow per PK)3.52 µs/op (9 B, 1 alloc)3.56 µs/op (232 B, 11 allocs)1,01x (parità, 11x meno allocazioni)
Scansione di 100 Righe (rows.Next)20.41 µs/op (30.5 KB, 211 allocs)36.34 µs/op (21.8 KB, 309 allocs)1,78x più veloce
Scansione di 1.000 Righe (rows.Next)125.92 µs/op (296 KB, 2.030 allocs)349.10 µs/op (216 KB, 3.009 allocs)2,77x più veloce
Inserimento Preparato (Autocommit)5.33 µs/op (0 B, 0 allocs)5.88 µs/op (120 B, 5 allocs)1,10x più veloce (0 allocazioni)
Batch di Transazione da 100 Righe189.99 µs/op (16 B, 1 alloc)191.50 µs/op (12.1 KB, 503 allocs)1,01x (500x meno allocazioni)

Cancellazione, Sandboxing dei Percorsi e Ripristino del Pool di Connessioni

Eseguire SQLite in un worker isolato rafforza anche il modo in cui il driver gestisce timeout, SQL non attendibile e confini del filesystem:

  • Cancellazione Deterministica del Contesto (conn.go e errors.go): Ogni chiamata del driver attiva una leggera guardia context.AfterFunc (cn.guard). Se il context.Context del chiamante viene annullato o la sua scadenza si esaurisce mentre una query ricorsiva lenta o un'attesa di lock è in esecuzione sul server, la guardia contrassegna la connessione come non valida, interrompe il collegamento di sessione locale e invia un frame Cancel() fuori banda. Il worker C invoca immediatamente sqlite3_interrupt(db), sbloccando sqlite3_step, mentre il driver Go associa lo stato SQLITE_INTERRUPT risultante a context.Canceled o context.DeadlineExceeded e istruisce database/sql a sostituire la connessione nel pool.
  • Sandboxing delle Directory (sqlsrv_pathrules.c): Il worker C applica le regole di percorso --allow-dir a ogni chiamata open_v2, risolvendo i link simbolici e rifiutando l'attraversamento relativo .. al di fuori della directory consentita. sqlite3_load_extension e SQLITE_LIMIT_ATTACHED sono disabilitati per impostazione predefinita, così che un'iniezione SQL non possa uscire dalla directory del database né caricare oggetti condivisi arbitrari.

Struttura del Repository

Il repository è strutturato in modo che il pacchetto di primo livello fornisca il driver database/sql pronto all'uso, mentre launcher/ e sqlite3/ restano accessibili per distribuzioni avanzate con server personalizzati:

 1gosuda.org/sqlite3
 2├── driver.go / api.go         # Registrazione del driver database/sql ("sqlite3") e API Connector
 3├── dsn.go                     # Parser DSN clean-room di mattn/go-sqlite3 e configurazione dei pragma
 4├── conn.go / tx.go            # Ciclo di vita della sessione, guardia del contesto e macchina a stati delle transazioni
 5├── stmt.go / rows.go          # Pipelining delle richieste a singolo volo e prefetch dei batch da 64 KiB
 6├── errors.go                  # Codici di errore primari/estesi tipizzati di SQLite e corrispondenza sentinella
 7├── autolaunch.go / hashes.go  # Materializzazione del server incorporato senza configurazione e fissaggio SHA-256
 8├── internal/embedded/         # Binari //go:embed con tag di piattaforma (linux, darwin, windows)
 9├── launcher/                  # Supervisore di processo configurabile per distribuzioni con server personalizzati
10└── sqlite3/                   # Client Go hqq-bindgen generato e sorgente del server SQLite C11
11    └── server/                # Implementazione del server C11 (sqlite3_hooks.c, sqlsrv_policy.c)

Per Iniziare

Installa il modulo con go get e apri il database con sql.Open("sqlite3", dsn):

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