gosuda.org/sqlite3: Pudotettava database/sql-ajuri, joka suorittaa natiivia C SQLiteä jaetussa muistissa
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ääri | Kääntötyökaluketju ja alkuperätiedot (Provenance) |
|---|---|---|---|
| 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 |
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-kyselyparametri | Hyväksytyt arvot | Vaikutus SQLiteen |
|---|---|---|
_busy_timeout / _timeout | Kokonaisluku milliseunteina (esim. 5000) | Kutsuu funktiota sqlite3_busy_timeout(db, ms) istunnon avauksessa. |
_journal_mode / _journal | DELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFF | Suorittaa komennon PRAGMA journal_mode = <mode>. |
_txlock | immediate, exclusive, deferred | Valitsee arvon BEGIN IMMEDIATE, BEGIN EXCLUSIVE tai BEGIN DEFERRED funktiolle db.BeginTx. |
_foreign_keys / _fk | on, true, 1, off, false, 0 | Suorittaa komennon PRAGMA foreign_keys = ON/OFF. |
_synchronous / _sync | 0/OFF, 1/NORMAL, 2/FULL, 3/EXTRA | Suorittaa komennon PRAGMA synchronous = <level>. |
_cache_size | Kokonaislukusivut tai -KiB (esim. -20000) | Suorittaa komennon PRAGMA cache_size = <val>. |
_loc | auto tai IANA-aikavyöhyke (esim. UTC, Asia/Seoul) | Jäsentää DATETIME-/TIMESTAMP-tekstisarakkeet tyypille *time.Time kohdesijainnissa. |
mode | ro, rw, rwc, memory | Asettaa lipun SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE tai SQLITE_OPEN_MEMORY. |
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuum | Totuusarvot tai tilan kokonaisluvut | Soveltaa 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 funktiollesqlite3_step - 4 000
cgo-kutsua funktioillesqlite3_column_typejasqlite3_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
- Yhden matkan kyselyputkitus (
stmt.go): KunQueryContexttaiStmt.QueryContextsuorittaa vain lukuun tarkoitetun lausekkeen,s.query()lähettää toiminnotreset,bind_values,stmt_info(ensimmäisellä suorituskerralla) jafetch_rowsasynkronisesti yhdessä jaetun muistin siirtomatkassa. Kaikki neljä operaatiota suoritetaan peräkkäin C-taustaprosessissa ja ne palautuvat yhdellä edestakaisella matkalla. - Tiukat C-pohjaiset eräsilmukat ja asynkroninen esihaku (
rows.go): Funktiossafetch_rows(sqlite3_hooks.c) C-taustaprosessi suorittaa lausekettasqlite3_step(stmt)natiivissa C-silmukassa, koodaten enintäänFetchSizeriviä (oletuksena 128) suoraan 64 KiB:n jaetun muistin vastauslokeroon, kunnes eräraja saavutetaan, lokero on täynnä tai tapahtumaSQLITE_DONEesiintyy. Heti kunrows.ingest()purkaa erän 1 (Batch 1), se lähettää komennonFetchRowsAsyncerä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ökuorma | gosuda.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.gojaerrors.go): Jokainen ajurikutsu varustaa kevyelläcontext.AfterFunc-suojauksella (cn.guard). Jos kutsujancontext.Contextkeskeytetää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 ulkopuolisenCancel()-kehyksen. C-taustaprosessi kutsuu välittömästi funktiotasqlite3_interrupt(db), mikä vapauttaa funktionsqlite3_steplukituksen, kun taas Go-ajuri kohdistaa tuloksena olevanSQLITE_INTERRUPT-tilankoodin kontekstiincontext.Canceledtaicontext.DeadlineExceededja ohjaa rajapintaadatabase/sqlkorvaamaan yhteyden poolissa. - Hakemistojen hiekkalaatikointi (
sqlsrv_pathrules.c): C-taustaprosessi pakottaa--allow-dir-polkusäännöt jokaiseenopen_v2-kutsuun, ratkaisten symboliset linkit ja hyläten suhteellisen..-siirtymän sallitun hakemiston ulkopuolelle.sqlite3_load_extensionjaSQLITE_LIMIT_ATTACHEDon 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
- GitHub-tietovarasto: https://github.com/gosuda/hqq-sqlite3
- DSN- ja Pragma-viiteopas: docs/DSN.md
- Arkkitehtuuri ja siirtoprotokolla: docs/ARCHITECTURE.md