GoSuda

gosuda.org/sqlite3: Egy Drop-In database/sql Driver, amely natív C SQLite-ot futtat Shared Memory-n keresztül

By Lemon Mint
views ...

A gosuda.org/sqlite3 egy CGO_ENABLED=0 database/sql driver a Go-hoz, amely az upstream C SQLite 3.54.1 amalgamationt egy izolált munkafolyamaton belül futtatja HQQ osztott memóriás gyűrűkön keresztül. A szabványos "sqlite3" drivernév alatt regisztrál, támogatja a github.com/mattn/go-sqlite3 DSN lekérdezési paramétereit, hat operációs rendszerhez és architektúra-célponthoz ágyaz be SLSA-tanúsítvánnyal ellátott előre lefordított szerverbinárisokat, valamint kérés-pipeliningot használ 64 KiB-os kötegelt prefetchinggel, hogy a többsoros lekérdezések esetén felülmúlja az in-process cgo-t.

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

Miért egy újabb Go SQLite Driver?

Az SQLite a világ egyik legtöbbet deployed szoftverkomponense, a Go-ból történő használata azonban történelmileg kényelmetlen választás elé kényszerítette a mérnököket a buildelési egyszerűség és a motorhűség között.

Az egyik oldalon a github.com/mattn/go-sqlite3 áll, amely régóta a közösségi szabvány. Ez közvetlenül, a cgo-n keresztül csomagolja be a hivatalos C SQLite amalgamationt. Ezzel a valós C query planner, a teljes WAL konkurens szemantika és a kiszámítható C memóriakezelés érhető el. Ennek ára a CGO_ENABLED=1: a helyi buildek és a CI pipeline-ok lelassulnak, a minimális scratch vagy distroless konténerképek kompatibilis C futtatókönyvtárakat igényelnek, egy másik operációs rendszerre vagy CPU-architektúrára történő cross-compiling teljes C cross-toolchainet kíván meg, és a C-ben bekövetkező bármilyen memóriasérülés osztozik a Go-folyamat címtartományán.

A másik oldalon a tiszta Go-ban írt SQLite portok állnak, amelyek úgy jönnek létre, hogy a C amalgamationt mechanikusan Go forráskóddá fordítják át egy emulált C futtatókörnyezeten (libc) keresztül. Ezek visszaállítják a CGO_ENABLED=0 beállítást, ami megkönnyíti a buildeket. Tartós munkaterhelések alatt azonban a lefordított C halom (heap) és a soronkénti Go allokációk növelik a garbage-collection nyomást, valamint a zárolási, busy timeout vagy utasításmegszakítási finomabb élmezőkbeli esetek eltérhetnek a natív C buildektől.

Azért építettük meg a gosuda.org/sqlite3-at, hogy a Go alkalmazásoknak ne kelljen választaniuk a CGO_ENABLED=0 és avalódi C SQLite motor között. Azáltal, hogy a hivatalos SQLite 3.54.1 C amalgamationt egy dedikált osztott memóriás szerverbe (hqq-sqlite-server) fordítjuk, és a gosuda.org/sqlite3-on keresztül kommunikálunk, a Go bináris CGO_ENABLED=0 beállítással fordul le, miközben a változatlan, -O3-optimalizált C SQLite-ot hajtja végre egy folyamathatáron túl.

Nulla konfigurációs végrehajtás: A go get-től a sql.Open-ig

A github.com/mattn/go-sqlite3 lecserélése egy meglévő Go kódbázisban kizárólag a blank import útvonal megváltoztatását igényli. A driver "sqlite3" néven (és az explicit kiválasztáshoz "hqq-sqlite" néven) regisztrál, és elfogadja ugyanezen DSN lekérdezési karakterláncokat:

 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}

Beágyazott SLSA-tanúsítvánnyal ellátott munkafolyamat-binárisok

Természetes kérdés annak hallatán, hogy egy könyvtár egy C munkafolyamat-folyamatot használ: "Szükséges-e egy különálló C binárist telepítenem vagy szállítanom a Go programom mellett?"

Nem. A gosuda.org/sqlite3 közvetlenül a platformcímkével ellátott Go fájlokon belül (internal/embedded/embed_<goos>_<goarch>.go) ágyazza be az előre lefordított hqq-sqlite-server binárisokat mind a hat fő célponthoz. Mivel a Go buildkorlátozásai kizárólag a cél GOOS-nak és GOARCH-nak megfelelő fájlt fordítják le, a GOOS=linux GOARCH=arm64 go build paranccsal történő cross-compiling automatikusan csak az ~1,4 MiB méretű linux/arm64 szerverbinárist ágyazza be.

Cél OSCél Arch (GOARCH)Beágyazott BinárisBuild Toolchain & 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

Amikor az sql.Open létrehozza az első kapcsolatot, az autolaunch.go a beágyazott binárist a felhasználói gyorsítótár-könyvtárba ($XDG_CACHE_HOME/hqq/bin/<sha256>/) materializálja, ellenőrzi annak kriptográfiai SHA-256 kivonatát a hashes.go fájlban található rögzített ReleaseHashes térképpel a gosuda.org/hqq/secureexec használatával, és a munkafolyamatot a --parent-stdin paraméterrel indítja el, így annak élettartama szigorúan a Go folyamathoz kötődik.

Clean-Room mattn/go-sqlite3 DSN Kompatibilitás

Annak érdekében, hogy valódi cserélhető (drop-in) helyettesítőként működjön, a DSN elemző (dsn.go) a mattn/go-sqlite3 kódbázisokban használt kapcsolat- és pragma-beállítások tiszta szobás (clean-room) replikáját valósítja meg:

DSN Lekérdezési ParaméterElfogadott ÉrtékekSQLite Hatás
_busy_timeout / _timeoutEgész milliszekundumok (pl. 5000)Megnyitáskor meghívja a sqlite3_busy_timeout(db, ms) függvényt.
_journal_mode / _journalDELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFFVégrehajtja a PRAGMA journal_mode = <mode> parancsot.
_txlockimmediate, exclusive, deferredKiválasztja a BEGIN IMMEDIATE, BEGIN EXCLUSIVE vagy BEGIN DEFERRED parancsot a db.BeginTx számára.
_foreign_keys / _fkon, true, 1, off, false, 0Végrehajtja a PRAGMA foreign_keys = ON/OFF parancsot.
_synchronous / _sync0/OFF, 1/NORMAL, 2/FULL, 3/EXTRAVégrehajtja a PRAGMA synchronous = <level> parancsot.
_cache_sizeEgész oldalak vagy -KiB (pl. -20000)Végrehajtja a PRAGMA cache_size = <val> parancsot.
_locauto vagy IANA időzóna (pl. UTC, Asia/Seoul)A DATETIME/TIMESTAMP szövegoszlopokat a célhelyen lévő *time.Time típusra elemzi.
modero, rw, rwc, memoryBeállítja a SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE vagy SQLITE_OPEN_MEMORY értéket.
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuumLogikai vagy egész módú értékekAlkalmazza a megfelelő munkamenet PRAGMA-t a kapcsolat inicializálásakor.

A folyamatizoláció átviteli sebességgé alakítása

Első pillantásra az SQLite külön folyamatban történő futtatása lassabbnak tűnhet, mint a cgo-n keresztüli, folyamaton belüli sqlite3_step hívás. A gyakorlatban az ellenkezője történik, amint egy lekérdezés egynél több sort ad vissza.

A soronkénti cgo hívások rejtett költsége

Egy hagyományos cgo SQLite driverben a database/sql az iterációt egyetlen soron és oszloponként hajtja végre. 1000 sor olvasása 4 oszloppal a következőt váltja ki:

  • 1000 cgo hívás a sqlite3_step függvényre
  • 4000 cgo hívás a sqlite3_column_type és sqlite3_column_int64 / sqlite3_column_text függvényekre
  • Több ezer egyedi határon átlépés a Go ütemező és a C verem (stack) között

Kérés-pipelining és 64 KiB-os kötegelt prefetching

A gosuda.org/sqlite3 megszünteti a soron belüli határátlépéseket azáltal, hogy újratervezi az átviteli protokollt a stmt.go, rows.go és sqlite3/server/sqlite3_hooks.c állományokban található két mechanizmus köré:

 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. Egylövetű lekérdezés-pipelining (stmt.go): Amikor a QueryContext vagy a Stmt.QueryContext egy csak olvasható utasítást hajt végre, az s.query() aszinkron módon elküldi a reset, bind_values, stmt_info (az első végrehajtáskor) és fetch_rows utasításokat egyetlen osztott memóriás műveletsorban. Mind a négy művelet egymás után hajtódik végre a C munkafolyamaton, és egyetlen fordulóval tér vissza.
  2. Szoros C kötegelt ciklusok és aszinkron prefetch (rows.go): A fetch_rows (sqlite3_hooks.c) belsejében a C munkafolyamat egy natív C ciklusban futtatja a sqlite3_step(stmt) függvényt, legfeljebb FetchSize sort (alapértelmezés szerint 128) kódolva közvetlenül a 64 KiB-os osztott memóriás válaszhelybe, amíg a kötegkorlát el nem éri, a hely meg nem telik, vagy az SQLITE_DONE be nem következik. Amint a rows.ingest() dekódolja az 1. köteget, elindítja a FetchRowsAsync-ot a 2. köteghez, mielőtt visszaküldené az első sort a hívónak. Miközben a Go kód az 1. köteget olvassa be, a C munkafolyamat már a 2. köteget lépteti egy másik CPU-magon.

Apple M4 (darwin/arm64) architektúrán a gosuda.org/sqlite3 (CGO_ENABLED=0) és a github.com/mattn/go-sqlite3 (CGO_ENABLED=1) összehasonlítása ugyanazon az SQLite adatbázison mutatja be a kötegelés és a pipelining hatását:

Munkaterhelésgosuda.org/sqlite3 (CGO_ENABLED=0)mattn/go-sqlite3 (CGO_ENABLED=1)Relatív Sebesség
Point Select (QueryRow PK alapján)3.52 µs/op (9 B, 1 allokáció)3.56 µs/op (232 B, 11 allokáció)1,01x (pari, 11x kevesebb allokáció)
100 soros beolvasás (rows.Next)20.41 µs/op (30.5 KB, 211 allokáció)36.34 µs/op (21.8 KB, 309 allokáció)1,78x gyorsabb
1000 soros beolvasás (rows.Next)125.92 µs/op (296 KB, 2,030 allokáció)349.10 µs/op (216 KB, 3,009 allokáció)2,77x gyorsabb
Előkészített beszúrás (Autocommit)5.33 µs/op (0 B, 0 allokáció)5.88 µs/op (120 B, 5 allokáció)1,10x gyorsabb (0 allokáció)
100 soros tranzakciós köteg189.99 µs/op (16 B, 1 allokáció)191.50 µs/op (12.1 KB, 503 allokáció)1,01x (500x kevesebb allokáció)

Megszakítás, útvonal-homokozó (path sandboxing) és kapcsolati medence helyreállítása

Az SQLite izolált munkafolyamatban történő futtatása szintén megerősíti, hogy a driver hogyan kezeli az időtúllépéseket, a nem megbízható SQL-t és a fájlrendszer-határokat:

  • Determinisztikus kontextus-megszakítás (conn.go és errors.go): Minden driverhívás élesít egy könnyűsúlyú context.AfterFunc védelmet (cn.guard). Ha a hívó context.Context példánya megszakad, vagy a határideje lejár, miközben egy lassú rekurzív lekérdezés vagy zárolási várakozás fut a szerveren, a védelem hibásként jelöli meg a kapcsolatot, leállítja a helyi munkamenet-hivatkozást, és elküld egy sávon kívüli (out-of-band) Cancel() keretet. A C munkafolyamat azonnal meghívja a sqlite3_interrupt(db) függvényt, feloldva a sqlite3_step blokkolását, míg a Go driver az eredményül kapott SQLITE_INTERRUPT állapotot context.Canceled vagy context.DeadlineExceeded értékre képezi le, és utasítja a database/sql-t a kapcsolat medencében történő cseréjére.
  • Könyvtári homokozó (sqlsrv_pathrules.c): A C munkafolyamat betartatja az --allow-dir útvonalszabályokat minden open_v2 híváskor, feloldva a szimbolikus linkeket és elutasítva a megengedett könyvtáron kívüli relatív .. bejárásokat. A sqlite3_load_extension és az SQLITE_LIMIT_ATTACHED alapértelmezés szerint le vannak tiltva, így az SQL-injekció nem tud kijutni az adatbázis-könyvtárból, és nem tud tetszőleges megosztott objektumokat betölteni.

Repository felépítése

A repository úgy van felépítve, hogy a legfelső szintű csomag biztosítja a beilleszthető database/sql drivert, míg a launcher/ és sqlite3/ elérhető marad a haladó, egyedi szervertelepítésekhez:

 1gosuda.org/sqlite3
 2├── driver.go / api.go         # database/sql driver regisztráció ("sqlite3") és Connector API
 3├── dsn.go                     # Clean-room mattn/go-sqlite3 DSN elemző és pragma konfiguráció
 4├── conn.go / tx.go            # Munkamenet-életciklus, kontextus-védelem és tranzakciós állapottérkép
 5├── stmt.go / rows.go          # Egylövetű kérés-pipelining és 64 KiB-os kötegelt prefetching
 6├── errors.go                  # Típusos SQLite elsődleges/kiterjesztett hibakódok és sentinel illesztés
 7├── autolaunch.go / hashes.go  # Nulla konfigurációs beágyazott szerver materializálás és SHA-256 rögzítés
 8├── internal/embedded/         # Platformcímkével ellátott //go:embed binárisok (linux, darwin, windows)
 9├── launcher/                  # Konfigurálható folyamat-felügyelő egyedi szervertelepítésekhez
10└── sqlite3/                   # Generált hqq-bindgen Go kliens és C11 SQLite szerver forrás
11    └── server/                # C11 szerver implementáció (sqlite3_hooks.c, sqlsrv_policy.c)

Kezdő lépések

Telepítse a modult a go get paranccsal, és nyissa meg az adatbázist a sql.Open("sqlite3", dsn) használatával:

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