GoSuda

gosuda.org/sqlite3: Pudotettava database/sql-ajuri, joka suorittaa natiivia C SQLiteä jaetussa muistissa

By Lemon Mint
views ...

gosuda.org/sqlite3 on Go-kielelle tarkoitettu CGO_ENABLED=0 database/sql -ajuri, joka suorittaa alkuperäisen C-kielisen SQLite 3.54.1 -yhdistelmän (amalgamation) eristetyssä taustaprosessissa HQQ-jaetun muistin renkaiden kautta. Se rekisteröityy vakiomuotoisella "sqlite3"-ajurin nimellä, tukee github.com/mattn/go-sqlite3 -DSN-kyselyparametreja, upottaa SLSA-varmennetut esikäännetyt palvelimen binääritiedostot kuudelle käyttöjärjestelmä- ja arkkitehtuurikohteelle sekä käyttää pyyntöjen putkitusta (request pipelining) ja 64 KiB:n eräesihakua (batch prefetching) saavuttaakseen prosessinsisäistä cgo-ajuria paremman suorituskyvyn usean rivin kyselyissä.

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

Miksi toinen Go SQLite -ajuri?

SQLite on yksi maailman käytetyimmistä ohjelmistoista, mutta sen käyttäminen Go-kielestä on historiallisesti pakottanut insinöörit valitsemaan hankalasti rakennuksen yksinkertaisuuden ja moottorin tarkkuuden väliltä.

Toisella puolella on github.com/mattn/go-sqlite3, joka on pitkäaikainen yhteisön standardi. Se kääri virallisen C-kielisen SQLite-yhdistelmän suoraan cgo-rajapinnan kautta. Siinä saadaan käyttöönsä todellinen C-pohjainen kyselyjen optimoija (query planner), täydellinen WAL-samanaikaisuussemantiikka ja ennustettavissa oleva C-muistinhallinta. Hinta on CGO_ENABLED=1: paikalliset käännökset ja CI-putket hidastuvat, minimaaliset tyhjät tai jakeluvapaat (distroless) säiliövedokset (container images) vaativat vastaavat C-ajonaikaiset kirjastot, ristiinkääntäminen toiselle käyttöjärjestelmälle tai suoritinarkkitehtuurille vaatii täyden C-ristiinkääntötyökaluketjun, ja mikä tahansa C-kielinen muistin vioittuminen jakaa Go-prosessin osoiteavaruuden.

Toisella puolella ovat puhtaalla Go-kielellä toteutetut SQLite-portit, jotka on generoitu kääntämällä mekaanisesti C-yhdistemä Go-lähdekoodiksi emuloidun C-ajonuaikaisen ympäristön (libc) päällä. Ne palauttavat asetuksen CGO_ENABLED=0, mikä tekee käännöksistä yksinkertaisia. Jatkuvan kuormituksen alaisena käännetty C-keko (heap) ja rivikohtaiset Go-muistinvaraukset kuitenkin lisäävät roskienkeruun painetta, ja lukituksen, varattujen aikakatkaisujen tai lausekkeiden keskeytysten hienovaraiset reunatapaukset voivat erota alkuperäisistä C-käännöksistä.

Rakensimme kirjaston gosuda.org/sqlite3, jotta Go-sovellusten ei tarvitsisi valita CGO_ENABLED=0:n ja todellisen C-kielisen SQLite-moottorin välillä. Kääntämällä virallisen SQLite 3.54.1 -C-yhdistelmän erilliseksi jaetun muistin palvelimeksi (hqq-sqlite-server) ja kommunikoimalla gosuda.org/hqq-kirjaston kautta Go-binääritiedostosi kääntyy asetuksella CGO_ENABLED=0, samalla kun se suorittaa muuttumatonta, -O3-optimoitua C-kielistä SQLiteä prosessirajojen yli.

Nollakonfiguraation suoritus: Kohteesta go get kohteeseen sql.Open

Kirjaston github.com/mattn/go-sqlite3 korvaaminen olemassa olevassa Go-koodikannassa vaatii vain tyhjän tuontipolun (blank import path) vaihtamisen. Ajuri rekisteröityy nimellä "sqlite3" (ja "hqq-sqlite" nimenomaista valintaa varten) ja hyväksyy samat DSN-kyselymerkkijonot:

 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}

Upotetut SLSA-varmennetut työntekijäbinäärit (Worker Binaries)

Luonnollinen kysymys kuultaessa, että kirjasto käyttää C-kielistä taustaprosessia, on: "Pitääkö minun asentaa tai toimittaa erillinen C-binääri Go-ohjelmani ohessa?"

Ei. gosuda.org/sqlite3 upottaa esikäännetyt hqq-sqlite-server-binääritiedostot kaikille kuudelle pääkohteelle suoraan alustatunnisteilla varustettuihin Go-tiedostoihin (internal/embedded/embed_<goos>_<goarch>.go). Koska Gon käännösrajoitteet kääntävät vain kohdemuuttujiasi GOOS ja GOARCH vastaavan tiedoston, komento GOOS=linux GOARCH=arm64 go build upottaa ristiinkäännettäessä automaattisesti vain noin 1,4 MiB kokoisen linux/arm64-palvelimen binääritiedoston.

KohdekäyttöjärjestelmäKohdearkkitehtuuri (GOARCH)Upotettu binääriKääntötyökaluketju ja alkuperätiedot (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

Kun sql.Open muodostaa ensimmäisen yhteytemäbä, autolaunch.go toteuttaa upotetun binäärin käyttäjän välimuistihakemistoon ($XDG_CACHE_HOME/hqq/bin/<sha256>/), tarkistaa sen kryptografisen SHA-256-tiivisteen hashes.go-tiedostossa olevaa kiinnitettyä ReleaseHashes-karttaa vasten gosuda.org/hqq/secureexec-kirjaston kautta ja käynnistää taustaprosessin lipulla --parent-stdin, jotta sen elinkaari on tiukasti sidottu Go-prosessiisi.

Puhdastilayhteensopivuus (Clean-Room) mattn/go-sqlite3 DSN:n kanssa

Toimiakseen aidosti suoraan korvaavana komponenttina DSN-jäsennin (dsn.go) toteuttaa puhtastilaperiaatteella replikan yhteys- ja pragma-asetuksista, joita käytetään mattn/go-sqlite3-koodikannoissa:

DSN-kyselyparametriHyväksytyt arvotVaikutus SQLiteen
_busy_timeout / _timeoutKokonaisluku milliseunteina (esim. 5000)Kutsuu funktiota sqlite3_busy_timeout(db, ms) istunnon avauksessa.
_journal_mode / _journalDELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFFSuorittaa komennon PRAGMA journal_mode = <mode>.
_txlockimmediate, exclusive, deferredValitsee arvon BEGIN IMMEDIATE, BEGIN EXCLUSIVE tai BEGIN DEFERRED funktiolle db.BeginTx.
_foreign_keys / _fkon, true, 1, off, false, 0Suorittaa komennon PRAGMA foreign_keys = ON/OFF.
_synchronous / _sync0/OFF, 1/NORMAL, 2/FULL, 3/EXTRASuorittaa komennon PRAGMA synchronous = <level>.
_cache_sizeKokonaislukusivut tai -KiB (esim. -20000)Suorittaa komennon PRAGMA cache_size = <val>.
_locauto tai IANA-aikavyöhyke (esim. UTC, Asia/Seoul)Jäsentää DATETIME-/TIMESTAMP-tekstisarakkeet tyypille *time.Time kohdesijainnissa.
modero, rw, rwc, memoryAsettaa lipun SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE tai SQLITE_OPEN_MEMORY.
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuumTotuusarvot tai tilan kokonaisluvutSoveltaa vastaavaa istunnon PRAGMA-asetusta yhteyden alustuksessa.

Prosessieristyksen muuntaminen suorituskykyeduksi (Throughput Advantage)

Ensisilmäyksellä SQLiten ajaminen erillisessä prosessissa saattaa kuulostaa hitaammalta kuin funktion sqlite3_step kutsuminen prosessinsisäisesti cgo-rajapinnan kautta. Käytännössä tapahtuu päinvastoin heti, kun kysely palauttaa useamman kuin yhden rivin.

Rivikohtaisten cgo-kutsujen piilossa oleva hinta

Perinteisessä cgo-pohjaisessa SQLite-ajurissa database/sql ohjaa iteraatiota yksi rivi ja yksi sarake kerrallaan. 1 000 rivin lukeminen 4 sarakkeella käynnistää:

  • 1 000 cgo-kutsua funktiolle sqlite3_step
  • 4 000 cgo-kutsua funktioille sqlite3_column_type ja sqlite3_column_int64 / sqlite3_column_text
  • Tuhansia yksittäisiä rajanylityksiä Go-ajastimen (scheduler) ja C-pakan (stack) välillä

Pyyntöjen putkitus ja 64 KiB:n eräesihaku

gosuda.org/sqlite3 poistaa rivikohtaiset rajanylitykset suunnittelemalla siirtoprotokollan (wire protocol) uudelleen kahden mekanismin ympärille tiedostoissa stmt.go, rows.go ja 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. Yhden matkan kyselyputkitus (stmt.go): Kun QueryContext tai Stmt.QueryContext suorittaa vain lukuun tarkoitetun lausekkeen, s.query() lähettää toiminnot reset, bind_values, stmt_info (ensimmäisellä suorituskerralla) ja fetch_rows asynkronisesti yhdessä jaetun muistin siirtomatkassa. Kaikki neljä operaatiota suoritetaan peräkkäin C-taustaprosessissa ja ne palautuvat yhdellä edestakaisella matkalla.
  2. Tiukat C-pohjaiset eräsilmukat ja asynkroninen esihaku (rows.go): Funktiossa fetch_rows (sqlite3_hooks.c) C-taustaprosessi suorittaa lauseketta sqlite3_step(stmt) natiivissa C-silmukassa, koodaten enintään FetchSize riviä (oletuksena 128) suoraan 64 KiB:n jaetun muistin vastauslokeroon, kunnes eräraja saavutetaan, lokero on täynnä tai tapahtuma SQLITE_DONE esiintyy. Heti kun rows.ingest() purkaa erän 1 (Batch 1), se lähettää komennon FetchRowsAsync erälle 2 ennen ensimmäisen rivin palauttamista kutsujalle. Samaan aikaan kun Go-koodisi lukee erää 1, C-taustaprosessi suorittaa jo erää 2 toisella suorittimen ytimellä.

Apple M4 -suorittimella (darwin/arm64) verrattaessa kirjastoa gosuda.org/sqlite3 (CGO_ENABLED=0) kirjastoon github.com/mattn/go-sqlite3 (CGO_ENABLED=1) samassa SQLite-tietokannassa näkyy erien käsittelyn ja putkituksen vaikutus:

Työkuormagosuda.org/sqlite3 (CGO_ENABLED=0)mattn/go-sqlite3 (CGO_ENABLED=1)Suhteellinen nopeus
Pistekysely (Point Select, QueryRow perusavaimella)3.52 µs/op (9 B, 1 varaus)3.56 µs/op (232 B, 11 varausta)1,01x (vastaava, 11x vähemmän varauksia)
100 rivin skannaus (rows.Next)20.41 µs/op (30.5 KB, 211 varausta)36.34 µs/op (21.8 KB, 309 varausta)1,78x nopeampi
1 000 rivin skannaus (rows.Next)125.92 µs/op (296 KB, 2 030 varausta)349.10 µs/op (216 KB, 3 009 varausta)2,77x nopeampi
Valmisteltu lisäys (Prepared Insert, automaattinen tallennus)5.33 µs/op (0 B, 0 varausta)5.88 µs/op (120 B, 5 varausta)1,10x nopeampi (0 varausta)
100 rivin tapahtumaerä (Transaction Batch)189.99 µs/op (16 B, 1 varaus)191.50 µs/op (12.1 KB, 503 varausta)1,01x (500x vähemmän varausta)

Keskeytys, polkujen hiekkalaatikointi (Sandboxing) ja yhteyspoolin palautus

SQLiten ajaminen eristetyssä taustaprosessissa vahvistaa myös sitä, miten ajuri käsittelee aikakatkaisuja, epäluotettavaa SQL-koodia ja tiedostojärjestelmän rajoja:

  • Deterministinen kontekstin keskeytys (conn.go ja errors.go): Jokainen ajurikutsu varustaa kevyellä context.AfterFunc-suojauksella (cn.guard). Jos kutsujan context.Context keskeytetään tai sen määräaika (deadline) umpeutuu hitaan rekursiivisen kyselyn tai lukitusodotuksen aikana palvelimella, suojaus merkitsee yhteyden virheelliseksi, katkaisee paikallisen istuntoyhteyden ja lähettää kaistan ulkopuolisen Cancel()-kehyksen. C-taustaprosessi kutsuu välittömästi funktiota sqlite3_interrupt(db), mikä vapauttaa funktion sqlite3_step lukituksen, kun taas Go-ajuri kohdistaa tuloksena olevan SQLITE_INTERRUPT-tilankoodin kontekstiin context.Canceled tai context.DeadlineExceeded ja ohjaa rajapintaa database/sql korvaamaan yhteyden poolissa.
  • Hakemistojen hiekkalaatikointi (sqlsrv_pathrules.c): C-taustaprosessi pakottaa --allow-dir-polkusäännöt jokaiseen open_v2-kutsuun, ratkaisten symboliset linkit ja hyläten suhteellisen ..-siirtymän sallitun hakemiston ulkopuolelle. sqlite3_load_extension ja SQLITE_LIMIT_ATTACHED on poistettu käytöstä oletusarvoisesti, jotta SQL-injektio ei voi paeta tietokonehakemistosta tai ladata mielivaltaisia jaettuja objekteja.

Tietovaraston rakenne (Repository Layout)

Tietovarasto on jäsennetty niin, että ylätason paketti tarjoaa suoraan korvaavan database/sql-ajurin, kun taas launcher/- ja sqlite3/-hakemistot säilyvät saavutettavina edistyneitä mukautettuja palvelinasennuksia varten:

 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)

Aloittaminen

Asenna moduuli komennolla go get ja avaa tietokanta komennolla sql.Open("sqlite3", dsn):

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