GoSuda

gosuda.org/sqlite3: CGO_ENABLED=0 database/sql illesztőprogram megosztott memórián alapuló IPC felett

By Lemon Mint views ...

gosuda.org/sqlite3 egy CGO_ENABLED=0 database/sql illesztőprogram Go-hoz, amely a hivatalos C SQLite 3.54.1 amalgamációt egy elszigetelt munkafolyamatban futtatja, HQQ megosztott memóriás gyűrűpuffereken keresztül. A "sqlite3" illesztőprogram-névvel regisztrálódik, támogatja a github.com/mattn/go-sqlite3 DSN lekérdezési paramétereit, hat operációs rendszer- és architektúra-célplatformhoz SLSA-attesztált, előre fordított szerverbináris fájlokat ágyaz be, és kérésenkénti csővezetékelést (request pipelining) valamint 64 KiB-os kötegelt előtöltést (batch prefetching) alkalmaz, így többsoros lekérdezéseken felülmúlja a folyamaton belüli cgo megoldást.

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

Miért még egy Go SQLite illesztőprogram?

Az SQLite a világ egyik legszélesebb körben telepített szoftverkomponense, mégis Go-ból történő használata hagyományosan kényelmetlen választás elé állította a mérnököket: vagy az egyszerű build folyamat vagy a motor hűsége mellett kellett dönteniük.

Az egyik oldalon a github.com/mattn/go-sqlite3 áll, a régóta használt közösségi szabvány. Ez cgo-n keresztül közvetlenül becsomagolja a hivatalos C SQLite amalgamációt. Így a valódi C lekérdezésoptimalizálót, a teljes WAL-párhuzamossági szemantikát és a kiszámítható C memóriakezelést kapjuk. Az ár a CGO_ENABLED=1: a helyi buildek és a CI-folyamatok lelassulnak, a minimális scratch vagy distroless konténerképeknek megfelelő C futtatókönyvtárakra van szükségük, egy másik operációs rendszerre vagy CPU-architektúrára történő keresztfordításhoz teljes C keresztfordító-eszköztárra van szükség, és a C-ben bekövetkező bármely memóriasérülés ugyanazt a Go-folyamat címtartományát érinti.

A másik oldalon a tiszta Go SQLite portok állnak, amelyeket a C amalgamáció emulált C futtatókörnyezeten (libc) keresztül, mechanikus fordítással alakítottak Go forráskóddá. Ezek visszaállítják a CGO_ENABLED=0 lehetőségét, ami triviálissá teszi a buildelést. Tartós terhelés mellett azonban a lefordított C-heap és a soronkénti Go-allokációk növelik a szemétgyűjtési nyomást, a zárolás, a foglaltsági időkorlátok vagy az utasítások megszakítása során felmerülő finom szélsőséges esetek pedig eltérhetnek a natív C buildektől.

A gosuda.org/sqlite3 azért készült, hogy a Go alkalmazásoknak ne kelljen választaniuk a CGO_ENABLED=0 és a valódi C SQLite motor között. A hivatalos SQLite 3.54.1 C amalgamáció egy dedikált, megosztott memóriás szerverbe (hqq-sqlite-server) fordításával, valamint a gosuda.org/hqq csomagon keresztüli kommunikációval a Go bináris fájl CGO_ENABLED=0 módban fordul, miközben a módosítatlan, -O3 optimalizált C SQLite egy folyamathatáron átívelve fut.

Konfiguráció nélküli futtatás: a go get-től a sql.Open-ig

A github.com/mattn/go-sqlite3 cseréje egy meglévő Go kódbázisban csupán a vak importútvonal megváltoztatását igényli. Az illesztőprogram "sqlite3" néven (és explicit kiválasztáshoz "hqq-sqlite" néven) regisztrálódik, és ugyanazokat a DSN lekérdezési karakterláncokat fogadja el:

 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-attesztált munkafolyamat-binárisok

Egy természetes kérdés, amely akkor merül fel, amikor megtudjuk, hogy egy könyvtár C munkafolyamatot használ: „Külön C binárist kell telepítenem vagy szállítanom a Go programommal együtt?”

Nem. A gosuda.org/sqlite3 mind a hat fő célplatformhoz előre fordított hqq-sqlite-server binárisokat ágyaz be közvetlenül a platformcímkézett Go fájlokba (internal/embedded/embed_<goos>_<goarch>.go). Mivel a Go build-megszorítások csak a céljának megfelelő GOOS és GOARCH fájlt fordítják le, a GOOS=linux GOARCH=arm64 go build parancs kizárólag a körülbelül 1,4 MiB-os linux/arm64 szerverbinárist ágyazza be.

Céloperációs rendszerCélarchitektúra (GOARCH)Beágyazott binárisBuild-eszköztár és eredet
Linuxamd64, arm64hqq-sqlite-server_linux_<arch>GCC -O3, SLSA Build L3 eredetiség
macOSarm64, amd64hqq-sqlite-server_darwin_<arch>AppleClang -O3, SLSA Build L3 eredetiség
Windowsamd64hqq-sqlite-server_windows_amd64.exeMSVC -O2, SLSA Build L3 eredetiség
Windowsarm64hqq-sqlite-server_windows_arm64.exezig cc -target aarch64-windows-gnu -O3, SLSA L3

Amikor a 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árába ($XDG_CACHE_HOME/hqq/bin/<sha256>/) írja ki, ellenőrzi kriptográfiai SHA-256 üzenetkivonatát a hashes.go fájlban rögzített ReleaseHashes térképhez képest a gosuda.org/hqq/secureexec csomagon keresztül, majd a munkafolyamatot a --parent-stdin kapcsolóval indítja el, így az élettartama szigorúan a Go folyamatához kötött.

mattn/go-sqlite3 DSN-kompatibilitás tiszta lapról

Ahhoz, hogy valódi közvetlen csereké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 lapról írt másolatát valósítja meg:

DSN lekérdezési paraméterElfogadott értékekSQLite hatás
_busy_timeout / _timeoutEgész számú milliszekundum (pl. 5000)Munkamenet-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> utasítást.
_txlockimmediate, exclusive, deferredA db.BeginTx számára a BEGIN IMMEDIATE, BEGIN EXCLUSIVE vagy BEGIN DEFERRED utasítást választja ki.
_foreign_keys / _fkon, true, 1, off, false, 0Végrehajtja a PRAGMA foreign_keys = ON/OFF utasítást.
_synchronous / _sync0/OFF, 1/NORMAL, 2/FULL, 3/EXTRAVégrehajtja a PRAGMA synchronous = <level> utasítást.
_cache_sizeEgész számú lapok vagy -KiB (pl. -20000)Végrehajtja a PRAGMA cache_size = <val> utasítást.
_locauto vagy IANA-időzóna (pl. UTC, Asia/Seoul)A DATETIME/TIMESTAMP szöveges oszlopokat a céltartománybeli *time.Time típusba alakítja.
modero, rw, rwc, memoryBeállítja a SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE vagy SQLITE_OPEN_MEMORY értékét.
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuumLogikai értékek vagy módegészekA megfelelő munkamenet-PRAGMA alkalmazása a kapcsolat inicializálásakor.

A folyamatizoláció átviteli sebességi előnnyé alakítása

Első pillantásra úgy tűnhet, hogy az SQLite külön folyamatban történő futtatása lassabb kell hogy legyen, mint a sqlite3_step folyamaton belüli hívása cgo-n keresztül. A gyakorlatban éppen az ellenkezője történik, amint a 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 illesztőprogramban a database/sql egyszerre egy sort és egy oszlopot bejárva iterál. 4 oszlopot tartalmazó 1000 sor olvasása a következőket váltja ki:

  • 1000 cgo hívás a sqlite3_step függvényhez
  • 4000 cgo hívás a sqlite3_column_type és a sqlite3_column_int64 / sqlite3_column_text függvényekhez
  • Több ezer egyedi határátlépés a Go ütemező és a C verem között

Kérés-csővezetékelés és 64 KiB-os kötegelt előtöltés

A gosuda.org/sqlite3 a soronkénti határátlépéseket úgy szünteti meg, hogy a vezetéki protokollt két mechanizmus köré tervezi át, amelyek a stmt.go, rows.go és sqlite3/server/sqlite3_hooks.c fájlokban találhatók:

 1Go kliens (stmt.go / rows.go)                       C11 munkafolyamat (sqlite3_hooks.c)
 2-----------------------------                       ----------------------------
 31. Csővezeték egy repülésben:
 4   ResetAsync + BindValuesAsync
 5   + StmtInfoAsync + FetchRowsAsync(128)  =======>  reset -> bind -> stmt_info ->
 6                                                    ciklus sqlite3_step() legfeljebb 128 sorig
 7                                          <=======  1. köteg (1..128. sor 64 KiB-os résben)
 82. Azonnal indítsa az előtöltést:
 9   FetchRowsAsync(128) (2. köteg)         =======>  ciklus sqlite3_step() a 129..256. sorokra
10   Miközben a C a 2. köteget lépteti,
11   a Go hívó az 1. köteg 1..128. sorait
12   olvassa be!                            <=======  2. köteg kész a megosztott memóriában
  1. Egyfordulós lekérdezés-csővezetékelés (stmt.go): Amikor a QueryContext vagy a Stmt.QueryContext egy csak olvasható utasítást futtat, az s.query() a reset, a bind_values, a stmt_info (első végrehajtáskor) és a fetch_rows műveleteket aszinkron módon, egyetlen megosztott memóriás fordulóban küldi el. Mind a négy művelet egymás után fut le a C munkafolyamaton, és egyetlen körutazással tér vissza.
  2. Szoros C kötegelési ciklusok és aszinkron előtöltés (rows.go): A fetch_rows (sqlite3_hooks.c) belsejében a C munkafolyamat natív C ciklusban lépteti a sqlite3_step(stmt) függvényt, és legfeljebb FetchSize (alapértelmezés szerint 128) sort kódol közvetlenül a 64 KiB-os megosztott memóriás válaszrésbe, amíg el nem éri a kötegkorlátot, a rés meg nem telik, vagy be nem következik a SQLITE_DONE. Amint a rows.ingest() dekódolja az 1. köteget, még mielőtt az első sort visszaadná a hívónak, elindítja a FetchRowsAsync hívást a 2. kötegre. Miközben a Go kódja az 1. köteget olvassa, a C munkafolyamat már egy másik CPU-magon lépteti a 2. köteget.

Apple M4 (darwin/arm64) processzoron, ugyanazon SQLite adatbázison összehasonlítva a gosuda.org/sqlite3 (CGO_ENABLED=0) és a github.com/mattn/go-sqlite3 (CGO_ENABLED=1) illesztőprogramokat, a kötegelés és a csővezetékelés hatása látszik:

Terhelésgosuda.org/sqlite3 (CGO_ENABLED=0)mattn/go-sqlite3 (CGO_ENABLED=1)Relatív sebesség
Pontlekérdezés (QueryRow PK szerint)3,52 µs/op (9 B, 1 allokáció)3,56 µs/op (232 B, 11 allokáció)1,01x (paritás, 11x kevesebb allokáció)
100 soros bejárá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 bejárás (rows.Next)125,92 µs/op (296 KB, 2030 allokáció)349,10 µs/op (216 KB, 3009 allokáció)2,77x gyorsabb
Előkészített beszúrás (automatikus commit)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ózás és kapcsolatkészlet-helyreállítás

Az SQLite elszigetelt munkafolyamatban történő futtatása az illesztőprogram időtúllépés-kezelését, a nem megbízható SQL-kezelést és a fájlrendszer-határokat is erősíti:

  • Determinisztikus kontextus-megszakítás (conn.go és errors.go): Minden illesztőprogram-hívás egy könnyűsúlyú context.AfterFunc őrt (cn.guard) állít be. Ha a hívó context.Context megszakad, vagy a határideje lejár, miközben egy lassú rekurzív lekérdezés vagy zárvárakozás fut a szerveren, az őr rossznak jelöli a kapcsolatot, megszakítja a helyi munkamenet-kapcsolatot, és sávon kívüli Cancel() keretet küld. A C munkafolyamat azonnal meghívja a sqlite3_interrupt(db) függvényt, feloldva a sqlite3_step blokkolását, a Go illesztőprogram pedig a keletkező SQLITE_INTERRUPT állapotot context.Canceled vagy context.DeadlineExceeded értékre képezi le, és utasítja a database/sql csomagot, hogy cserélje le a kapcsolatot a készletben.
  • Könyvtár-homokozózás (sqlsrv_pathrules.c): A C munkafolyamat minden open_v2 híváskor érvényesíti a --allow-dir útvonalszabályokat, feloldja a szimbolikus linkeket, és elutasítja a megengedett könyvtáron kívüli relatív .. bejárást. A sqlite3_load_extension és a SQLITE_LIMIT_ATTACHED alapértelmezés szerint letiltott, így az SQL-injektálás nem léphet ki az adatbázis-könyvtárból, és nem tölthet be tetszőleges megosztott objektumokat.

Tárolóelrendezés

A tárolót úgy strukturálták, hogy a legfelső szintű csomag biztosítsa a közvetlen database/sql illesztőprogramot, miközben a launcher/ és a sqlite3/ könyvtárak továbbra is elérhetők maradnak a haladó, egyéni szerveres telepítésekhez:

 1gosuda.org/sqlite3
 2├── driver.go / api.go         # database/sql illesztőprogram-regisztráció ("sqlite3") és Connector API
 3├── dsn.go                     # Tiszta lapról írt mattn/go-sqlite3 DSN-elemző és pragma-konfiguráció
 4├── conn.go / tx.go            # Munkamenet-életciklus, kontextusőr és tranzakciós állapotgép
 5├── stmt.go / rows.go          # Egyfordulós kérés-csővezetékelés és 64 KiB-os kötegelt előtöltés
 6├── errors.go                  # Típusos SQLite elsődleges és kiterjesztett hibakódok, őrszem-illesztés
 7├── autolaunch.go / hashes.go  # Konfiguráció nélküli beágyazott szerver-kibontás és SHA-256 rögzítés
 8├── internal/embedded/         # Platformcímkézett //go:embed binárisok (linux, darwin, windows)
 9├── launcher/                  # Konfigurálható folyamatfelügyelő egyéni szerveres telepítésekhez
10└── sqlite3/                   # Generált hqq-bindgen Go kliens és C11 SQLite szerverforrás
11    └── server/                # C11 szerver-implementáció (sqlite3_hooks.c, sqlsrv_policy.c)

Első lépések

Telepítse a modult a go get paranccsal, majd nyissa meg adatbázisát a sql.Open("sqlite3", dsn) hívással:

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