GoSuda

gosuda.org/sqlite3: CGO_ENABLED=0 database/sql Driver Shared-Memory IPC:n välityksellä

By Lemon Mint views ...

gosuda.org/sqlite3 on Go-ohjelmointikielen database/sql -ajuri, joka on CGO_ENABLED=0-yhteensopiva ja ajaa alkuperäisen C-kielisen SQLite 3.54.1 -amalgaman eristetyssä työprosessissa HQQ:n jaetun muistin renkaiden kautta. Ajuri rekisteröityy vakionimellä "sqlite3", tukee github.com/mattn/go-sqlite3 -DSN-kyselyparametreja, sisältää upotetut SLSA-todennetut esikäännetyt palvelinbinäärit kuudelle käyttöjärjestelmän ja arkkitehtuurin kohteelle sekä käyttää kyselyiden putkitusta ja 64 KiB:n erien esihakua, minkä ansiosta se suoriutuu monirivisistä kyselyistä paremmin kuin prosessinsisäinen cgo.

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

Miksi Toinen Go-SQLite-ajuri?

SQLite on yksi maailman laajimmin käytetyistä ohjelmistoista, mutta sen käyttäminen Go-kielestä on historiallisesti pakottanut kehittäjät epämukavaan valintaan rakennuksen yksinkertaisuuden ja moottorin uskollisuuden välillä.

Toisaalta on github.com/mattn/go-sqlite3, pitkäaikainen yhteisön vakiovalinta. Se kääri viralliset C-kielisen SQLite-amalgaman suoraan cgo:n kautta. Saat todellisen C-kielisen kyselyoptimoijan, täydet WAL-samanaikaisuussemantiikat ja ennustettavan C-muistinhallinnan. Hinta on CGO_ENABLED=1: paikalliset käännökset ja CI-putkistot hidastuvat, minimaaliset scratch- tai distroless-kontti-imagot 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-koodin muistivaurio jakaa Go-prosessin osoiteavaruuden.

Toisaalta ovat puhtaan Go-kielen SQLite-portit, jotka on tuotettu mekaanisesti kääntämällä C-amalgama Go-lähdekoodiksi emuloidun C-ajonaikaisen ympäristön (libc) päälle. Ne palauttavat CGO_ENABLED=0:n, mikä tekee käännöksistä triviaaleja. Pitkäkestoisissa kuormissa käännetty C-keko ja rivikohtaiset Go-allokaatiot kuitenkin lisäävät roskienkeräyksen painetta, ja hienovaraiset reunatapaukset lukituksessa, varausaikakatkaisuissa tai lauseiden keskeytyksessä voivat poiketa natiiveista C-käännöksistä.

Rakensimme gosuda.org/sqlite3:n, jotta Go-sovelluksien ei tarvitse valita CGO_ENABLED=0:n ja todellisen C-kielisen SQLite-moottorin välillä. Kääntämällä virallisen SQLite 3.54.1 C-amalgaman omistettuun jaetun muistin palvelimeen (hqq-sqlite-server) ja kommunikoimalla gosuda.org/hqq:n kautta, Go-binääri käännetään CGO_ENABLED=0-tilassa ja suorittaa muokkaamatonta, -O3-optimoitua C-kielistä SQLite-koodia prosessirajan yli.

Konfiguroinniton Suoritus: go get:stä sql.Open:iin

Olemassa olevan Go-koodikannan github.com/mattn/go-sqlite3 -kirjaston korvaaminen vaatii ainoastaan tyhjän tuontipolun muuttamisen. Ajuri rekisteröityy nimellä "sqlite3" (ja "hqq-sqlite" eksplisiittistä 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-todennetut työbinäärit

Luonnollinen kysymys, kun kuulee kirjaston käyttävän C-työprosessia, on: "Pitääkö minun asentaa tai toimittaa erillinen C-binääri Go-ohjelmani rinnalle?"

Ei. gosuda.org/sqlite3 upottaa esikäännetyt hqq-sqlite-server-binäärit kaikille kuudelle pääkohteelle suoraan alustakohtaisiin Go-tiedostoihin (internal/embedded/embed_<goos>_<goarch>.go). Koska Go:n käännösrajoitteet kääntävät vain kohdetta GOOS:ia ja GOARCH:ia vastaavan tiedoston, GOOS=linux GOARCH=arm64 go build -ristiinkäännös upottaa automaattisesti vain noin 1,4 MiB:n kokoisen linux/arm64-palvelinbinäärin.

Kohde-käyttöjärjestelmäKohdearkkitehtuuri (GOARCH)Upotettu binääriKäännöstyökalut ja alkuperä
Linuxamd64, arm64hqq-sqlite-server_linux_<arch>GCC -O3, SLSA Build L3 -alkuperä
macOSarm64, amd64hqq-sqlite-server_darwin_<arch>AppleClang -O3, SLSA Build L3 -alkuperä
Windowsamd64hqq-sqlite-server_windows_amd64.exeMSVC -O2, SLSA Build L3 -alkuperä
Windowsarm64hqq-sqlite-server_windows_arm64.exezig cc -target aarch64-windows-gnu -O3, SLSA L3

Kun sql.Open muodostaa ensimmäisen yhteyden, autolaunch.go purkaa upotetun binäärin käyttäjän välimuistihakemistoon ($XDG_CACHE_HOME/hqq/bin/<sha256>/), varmistaa sen kryptografisen SHA-256-tiivisteen vasten hashes.go-tiedoston kiinnitettyä ReleaseHashes-karttaa gosuda.org/hqq/secureexec-paketin avulla ja käynnistää työprosessin --parent-stdin-lipulla, jotta sen elinikä on tiukasti sidottu Go-prosessiisi.

mattn/go-sqlite3 DSN -yhteensopivuus puhtaasta lähteestä

Jotta ajuri toimisi todellisena suorana korvaajana, DSN-jäsennin (dsn.go) toteuttaa puhtaasta lähteestä rakennetun kopion yhteys- ja pragma-asetuksista, joita käytetään mattn/go-sqlite3-koodikannoissa:

DSN-kyselyparametriHyväksytyt arvotSQLite-vaikutus
_busy_timeout / _timeoutKokonaisluku millisekunteina (esim. 5000)Kutsuu sqlite3_busy_timeout(db, ms) istunnon avautuessa.
_journal_mode / _journalDELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFFSuorittaa PRAGMA journal_mode = <mode>.
_txlockimmediate, exclusive, deferredValitsee db.BeginTx:lle joko BEGIN IMMEDIATE, BEGIN EXCLUSIVE tai BEGIN DEFERRED.
_foreign_keys / _fkon, true, 1, off, false, 0Suorittaa PRAGMA foreign_keys = ON/OFF.
_synchronous / _sync0/OFF, 1/NORMAL, 2/FULL, 3/EXTRASuorittaa PRAGMA synchronous = <level>.
_cache_sizeKokonaisluku sivuina tai -KiB (esim. -20000)Suorittaa PRAGMA cache_size = <val>.
_locauto tai IANA-aikavyöhyke (esim. UTC, Asia/Seoul)Jäsentää DATETIME/TIMESTAMP tekstisarakkeet arvoiksi *time.Time kohdesijainnissa.
modero, rw, rwc, memoryAsettaa SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE tai SQLITE_OPEN_MEMORY.
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuumTotuusarvo tai tilakokonaislukuSoveltaa vastaavan istunnon PRAGMA-asetuksen yhteyden alustuksessa.

Prosessieristyksen Muuttaminen Läpäisykyvyn Eduksi

Ensi silmäyksellä SQLiten ajaminen erillisessä prosessissa vaikuttaa väistämättä hitaammalta kuin sqlite3_step-kutsujen tekeminen prosessinsisäisesti cgo:n kautta. Käytännössä päinvastoin tapahtuu heti, kun kysely palauttaa enemmän kuin yhden rivin.

Rivikohtaisten cgo-kutsujen piilokustannus

Tavanomaisessa cgo-SQLite-ajurissa database/sql ohjaa iterointia yksi rivi ja yksi sarake kerrallaan. 1 000 rivin lukeminen 4 sarakkeella aiheuttaa:

  • 1 000 cgo-kutsua sqlite3_step:lle
  • 4 000 cgo-kutsua sqlite3_column_type:lle sekä sqlite3_column_int64:lle / sqlite3_column_text:lle
  • Tuhansia yksittäisiä rajanylityksiä Go-ajastimen ja C-pinon välillä

Kyselyiden putkitus ja 64 KiB:n erien esihaku

gosuda.org/sqlite3 poistaa rivikohtaiset rajanylitykset uudistamalla siirtoprotokollan kahden mekanismin ympärille, jotka sijaitsevat tiedostoissa stmt.go, rows.go ja sqlite3/server/sqlite3_hooks.c:

 1Go-asiakas (stmt.go / rows.go)                      C11-työprosessi (sqlite3_hooks.c)
 2------------------------------                      ---------------------------------
 31. Putkita yhdessä lennossa:
 4   ResetAsync + BindValuesAsync
 5   + StmtInfoAsync + FetchRowsAsync(128)  =======>  reset -> bind -> stmt_info ->
 6                                                    silmukka sqlite3_step() enintään 128 riville
 7                                          <=======  Erä 1 (rivit 1..128 pakattuna 64 KiB:n slotiin)
 82. Laukaise välittömästi esihaku:
 9   FetchRowsAsync(128) (Erä 2)            =======>  silmukka sqlite3_step() riveille 129..256
10   Kun C askeltaa Erää 2, Go-kutsuja
11   lukee rivit 1..128 Erästä 1!           <=======  Erä 2 valmis jaetussa muistissa
  1. Yksittäislennon kyselyputkitus (stmt.go): Kun QueryContext tai Stmt.QueryContext suorittaa vain luku -lauseen, s.query() lähettää reset-, bind_values-, stmt_info- (ensimmäisellä suorituskerralla) ja fetch_rows-operaatiot asynkronisesti yhdessä jaetun muistin lennossa. Kaikki neljä operaatiota suoritetaan peräkkäin C-työprosessissa ja palautetaan yhdellä kierroksella.
  2. Tiiviit C-erisilmukat ja asynkroninen esihaku (rows.go): fetch_rows-funktion sisällä (sqlite3_hooks.c) C-työprosessi askeltaa sqlite3_step(stmt):ia natiivissa C-silmukassa ja koodaa enintään FetchSize-riviä (oletus 128) suoraan 64 KiB:n jaetun muistin vastausslottiin, kunnes erän raja, slotti täyttyy tai SQLITE_DONE tapahtuu. Heti kun rows.ingest() on purkanut Erän 1, se laukaisee FetchRowsAsync:n Erälle 2 ennen ensimmäisen rivin palauttamista kutsujalle. Kun Go-koodisi lukee Erää 1, C-työprosessi askeltaa jo Erää 2 toisella suoritinytimellä.

Apple M4:llä (darwin/arm64) vertailu gosuda.org/sqlite3:n (CGO_ENABLED=0) ja github.com/mattn/go-sqlite3:n (CGO_ENABLED=1) välillä samalla SQLite-tietokannalla osoittaa erien käsittelyn ja putkituksen vaikutuksen:

Kuormagosuda.org/sqlite3 (CGO_ENABLED=0)mattn/go-sqlite3 (CGO_ENABLED=1)Suhteellinen nopeus
Pistehaku (QueryRow PK:lla)3.52 µs/op (9 B, 1 allokaatio)3.56 µs/op (232 B, 11 allokaatiota)1.01x (tasavertainen, 11x vähemmän allokaatioita)
100 rivin skannaus (rows.Next)20.41 µs/op (30.5 KB, 211 allokaatiota)36.34 µs/op (21.8 KB, 309 allokaatiota)1.78x nopeampi
1 000 rivin skannaus (rows.Next)125.92 µs/op (296 KB, 2 030 allokaatiota)349.10 µs/op (216 KB, 3 009 allokaatiota)2.77x nopeampi
Valmisteltu lisäys (autocommit)5.33 µs/op (0 B, 0 allokaatiota)5.88 µs/op (120 B, 5 allokaatiota)1.10x nopeampi (0 allokaatiota)
100 rivin transaktioerä189.99 µs/op (16 B, 1 allokaatio)191.50 µs/op (12.1 KB, 503 allokaatiota)1.01x (500x vähemmän allokaatioita)

Peruutus, polkuhiekkalaatikointi ja yhteysaltaan palautuminen

SQLiten ajaminen eristetyssä työprosessissa vahvistaa myös tapaa, jolla ajuri käsittelee aikakatkaisuja, luotettavia SQL-lauseita ja tiedostojärjestelmän rajoja:

  • Deterministinen kontekstin peruutus (conn.go ja errors.go): Jokainen ajurikutsu asettaa kevyen context.AfterFunc-vartijan (cn.guard). Jos kutsujan context.Context peruutetaan tai sen määräaika umpeutuu, kun hidas rekursiivinen kysely tai lukitusodotus on käynnissä palvelimella, vartija merkitsee yhteyden virheelliseksi, katkaisee paikallisen istuntoyhteyden ja lähettää kaistan ulkopuolisen Cancel()-kehyksen. C-työprosessi kutsuu välittömästi sqlite3_interrupt(db):tä, mikä vapauttaa sqlite3_step:n, kun taas Go-ajuri kuvaa tuloksena olevan SQLITE_INTERRUPT-tilan arvoksi context.Canceled tai context.DeadlineExceeded ja ohjaa database/sql:n korvaamaan yhteyden altaassa.
  • Hakemistohiekkalaatikointi (sqlsrv_pathrules.c): C-työprosessi valvoo --allow-dir-polkusääntöjä jokaisella open_v2-kutsulla, selvittää symbolisia linkkejä ja hylkää sallitun hakemiston ulkopuolelle johtavat relatiiviset ..-siirtymät. sqlite3_load_extension ja SQLITE_LIMIT_ATTACHED ovat oletuksena poissa käytöstä, jotta SQL-injektio ei voi poistua tietokantahakemistosta tai ladata mielivaltaisia jaettuja objekteja.

Repositorion rakenne

Repositorio on jäsennelty siten, että ylätason paketti tarjoaa suoran database/sql-ajurin, kun taas launcher/ ja sqlite3/ ovat saatavilla edistyneitä mukautettuja palvelinkäyttöönottoja varten:

 1gosuda.org/sqlite3
 2├── driver.go / api.go         # database/sql -ajurin rekisteröinti ("sqlite3") ja Connector-API
 3├── dsn.go                     # Puhtaasta lähteestä rakennettu mattn/go-sqlite3 DSN-jäsennin ja pragma-asetukset
 4├── conn.go / tx.go            # Istunnon elinkaari, kontekstivartija ja transaktiotilakone
 5├── stmt.go / rows.go          # Yksittäislennon pyyntöputkitus ja 64 KiB:n erien esihaku
 6├── errors.go                  # Tyypitetyt SQLiten ensisijaiset/laajennetut virhekoodit ja vartijavastaavuus
 7├── autolaunch.go / hashes.go  # Konfiguroinniton upotetun palvelimen purkaminen ja SHA-256-kiinnitys
 8├── internal/embedded/         # Alustakohtaiset //go:embed-binäärit (linux, darwin, windows)
 9├── launcher/                  # Konfiguroitava prosessivalvoja mukautettuja palvelinkäyttöönottoja varten
10└── sqlite3/                   # Generoitu hqq-bindgen Go-asiakas ja C11 SQLite-palvelinlähdekoodi
11    └── server/                # C11-palvelintoteutus (sqlite3_hooks.c, sqlsrv_policy.c)

Aloittaminen

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

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