GoSuda

gosuda.org/sqlite3: CGO_ENABLED=0 database/sql Driver чрез Shared-Memory IPC

By Lemon Mint views ...

gosuda.org/sqlite3 е драйвер за database/sql на Go с CGO_ENABLED=0, който изпълнява upstream C SQLite 3.54.1 amalgamation в изолиран работен процес (worker process) през споделени кръгови буфери в паметта на HQQ. Той се регистрира под стандартното име на драйвера "sqlite3", поддържа параметрите в DSN заявките на github.com/mattn/go-sqlite3, включва предварително компилирани сървърни двоични файлове за шест комбинации от операционна система и архитектура, атестирани по SLSA, и използва конвейеризиране на заявки (request pipelining) с предварително извличане на партиди от 64 KiB, за да надмине in-process cgo при заявки, връщащи множество редове.

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

Защо още един SQLite драйвер за Go?

SQLite е един от най-широко внедрените софтуерни компоненти в света, но използването му от Go исторически е принуждавало инженерите да избират между простота на изграждането и вярност към самия енджин.

От едната страна стои github.com/mattn/go-sqlite3, дългогодишният общностен стандарт. Той обвива официалното C SQLite amalgamation директно чрез cgo. Получавате истинския C планировчик на заявки, пълната семантика на WAL конкурентността и предвидимото управление на C паметта. Цената е CGO_ENABLED=1: локалните сглобки и CI конвейерите се забавят, минималните scratch или distroless контейнерни образи изискват съвпадащи C рънтайм библиотеки, кръстосаната компилация за друга операционна система или архитектура на процесора изисква пълна C кръстосана верига от инструменти, а всяко повреждане на паметта в C споделя адресното пространство на Go процеса.

От другата страна са чисто Go портовете на SQLite, генерирани чрез механично превеждане на C amalgamation в Go изходен код върху емулирана C рънтайм среда (libc). Те възстановяват CGO_ENABLED=0, което прави изграждането тривиално. При продължителни натоварвания обаче преведената C куп памет и алокациите в Go за всеки ред увеличават натиска върху събирането на боклук (garbage collection), а фини гранични случаи при заключването, времената за изчакване (busy timeouts) или прекъсването на оператори могат да се отклонят от нативните C сборки.

Създадохме gosuda.org/sqlite3, за да не се налага Go приложенията да избират между CGO_ENABLED=0 и истинския C енджин на SQLite. Чрез компилиране на официалния C amalgamation на SQLite 3.54.1 в специализиран сървър със споделена памет (hqq-sqlite-server) и комуникация чрез gosuda.org/hqq, вашият Go двоичен файл се компилира с CGO_ENABLED=0, докато изпълнява непроменен, оптимизиран с -O3 C SQLite през граница на процесите.

Изпълнение без конфигурация: от go get до sql.Open

Замяната на github.com/mattn/go-sqlite3 в съществуваща Go кодова база изисква промяна единствено на импорта с празно име (blank import). Драйверът се регистрира като "sqlite3" (а за изрична селекция и като "hqq-sqlite") и приема същите DSN низове за заявки:

 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}

Вградени двоични файлове на работния процес с атестация SLSA

Естествен въпрос, когато се чуе, че библиотеката използва работен процес на C, е: „Трябва ли да инсталирам или да доставя отделен C двоичен файл заедно с моята Go програма?"

Не. gosuda.org/sqlite3 включва предварително компилирани двоични файлове hqq-sqlite-server за всичките шест основни цели директно във файлове на Go с платформени етикети (internal/embedded/embed_<goos>_<goarch>.go). Тъй като ограниченията за компилация на Go компилират само файла, съответстващ на вашите GOOS и GOARCH, кръстосаното компилиране с GOOS=linux GOARCH=arm64 go build автоматично вгражда само двоичния файл на сървъра linux/arm64 с размер около 1,4 MiB.

Целева ОСЦелева архитектура (GOARCH)Вграден двоичен файлИнструментална верига и произход
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

Когато sql.Open установи първата си връзка, autolaunch.go материализира вградения двоичен файл в потребителската кеш директория ($XDG_CACHE_HOME/hqq/bin/<sha256>/), проверява криптографския му SHA-256 дайджест спрямо фиксирания map ReleaseHashes в hashes.go чрез gosuda.org/hqq/secureexec и стартира работния процес с --parent-stdin, така че животът му да бъде строго свързан с вашия Go процес.

Съвместимост с DSN на mattn/go-sqlite3 по метода "clean-room"

За да работи като истински драйверен заместител, DSN парсерът (dsn.go) реализира клийн-рум реплика на параметрите за връзка и PRAGMA, използвани в кодовите бази на mattn/go-sqlite3:

Параметър в DSN заявкатаПриемани стойностиЕфект в SQLite
_busy_timeout / _timeoutЦяло число в милисекунди (напр. 5000)Извиква sqlite3_busy_timeout(db, ms) при отваряне на сесията.
_journal_mode / _journalDELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFFИзпълнява PRAGMA journal_mode = <mode>.
_txlockimmediate, exclusive, deferredИзбира BEGIN IMMEDIATE, BEGIN EXCLUSIVE или BEGIN DEFERRED за db.BeginTx.
_foreign_keys / _fkon, true, 1, off, false, 0Изпълнява PRAGMA foreign_keys = ON/OFF.
_synchronous / _sync0/OFF, 1/NORMAL, 2/FULL, 3/EXTRAИзпълнява PRAGMA synchronous = <level>.
_cache_sizeЦяло число страници или -KiB (напр. -20000)Изпълнява PRAGMA cache_size = <val>.
_locauto или IANA часова зона (напр. UTC, Asia/Seoul)Парсира DATETIME/TIMESTAMP текстови колони в *time.Time в целевата локация.
modero, rw, rwc, memoryЗадава SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE или SQLITE_OPEN_MEMORY.
_case_sensitive_like, _query_only, _recursive_triggers, _secure_delete, _AutoVacuumБулеви стойности или целочислени режимиПрилага съответния PRAGMA за сесията при инициализиране на връзката.

Превръщане на изолацията на процесите в предимство по производителност

На пръв поглед изпълнението на SQLite в отделен процес изглежда задължително по-бавно от извикването на sqlite3_step в процеса чрез cgo. На практика се случва обратното, веднага щом една заявка върне повече от един ред.

Скритата цена на извикванията на cgo ред по ред

В конвенционален cgo драйвер за SQLite database/sql управлява итерацията по един ред и една колона наведнъж. Четенето на 1000 реда с 4 колони предизвиква:

  • 1000 cgo извиквания към sqlite3_step
  • 4000 cgo извиквания към sqlite3_column_type и sqlite3_column_int64 / sqlite3_column_text
  • Хиляди отделни преминавания на границата между планировчика на Go и C стека

Конвейеризиране на заявки и предварително извличане на партиди от 64 KiB

gosuda.org/sqlite3 премахва преминаванията на граница за всеки ред, като преосмисля протокола за обмен около два механизма в stmt.go, rows.go и 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. Конвейеризиране на заявки в един полет (stmt.go): Когато QueryContext или Stmt.QueryContext изпълни оператор само за четене, s.query() изпраща асинхронно reset, bind_values, stmt_info (при първото изпълнение) и fetch_rows в един споделен полет в паметта. И четирите операции се изпълняват последователно на C работния процес и се връщат при едно обръщение.
  2. Плътни C цикли за партиди и асинхронно предварително извличане (rows.go): Вътре в fetch_rows (sqlite3_hooks.c) C работният процес изпълнява sqlite3_step(stmt) в нативен C цикъл, кодирайки до FetchSize реда (по подразбиране 128) директно в 64 KiB слота за отговор в споделената памет, докато не бъде достигнат лимитът на партидата, слотът не се запълни или не настъпи SQLITE_DONE. Веднага щом rows.ingest() декодира Партида 1, той изпраща FetchRowsAsync за Партида 2, преди да върне първия ред на извикващия. Докато вашият Go код сканира Партида 1, C работният процес вече изпълнява стъпки за Партида 2 на друго ядро на процесора.

На Apple M4 (darwin/arm64) сравнението на gosuda.org/sqlite3 (CGO_ENABLED=0) с github.com/mattn/go-sqlite3 (CGO_ENABLED=1) върху една и съща SQLite база данни показва ефекта от партидната обработка и конвейеризирането:

Натоварванеgosuda.org/sqlite3 (CGO_ENABLED=0)mattn/go-sqlite3 (CGO_ENABLED=1)Относително ускорение
Точкова заявка (QueryRow по PK)3.52 µs/op (9 B, 1 alloc)3.56 µs/op (232 B, 11 allocs)1.01x (паритет, 11x по-малко алокации)
Сканиране на 100 реда (rows.Next)20.41 µs/op (30.5 KB, 211 allocs)36.34 µs/op (21.8 KB, 309 allocs)1.78x по-бързо
Сканиране на 1000 реда (rows.Next)125.92 µs/op (296 KB, 2,030 allocs)349.10 µs/op (216 KB, 3,009 allocs)2.77x по-бързо
Подготвено INSERT (Autocommit)5.33 µs/op (0 B, 0 allocs)5.88 µs/op (120 B, 5 allocs)1.10x по-бързо (0 алокации)
Партидна транзакция от 100 реда189.99 µs/op (16 B, 1 alloc)191.50 µs/op (12.1 KB, 503 allocs)1.01x (500x по-малко алокации)

Отмяна, изолация на пътищата и възстановяване на пула от връзки

Изпълнението на SQLite в изолиран работен процес също укрепва начина, по който драйверът обработва времевите ограничения, недоверения SQL и границите на файловата система:

  • Детерминирана отмяна на контекст (conn.go и errors.go): Всяко извикване на драйвера активира лек страж context.AfterFunc (cn.guard). Ако context.Context на извикващия бъде отменен или срокът му изтече, докато на сървъра се изпълнява бавна рекурсивна заявка или чакане на заключване, стражът маркира връзката като невалидна, прекъсва локалната сесийна връзка и изпраща извънлентов (out-of-band) фрейм Cancel(). C работният процес незабавно извиква sqlite3_interrupt(db), което освобождава sqlite3_step, а Go драйверът преобразува получения статус SQLITE_INTERRUPT в context.Canceled или context.DeadlineExceeded и указва на database/sql да замени връзката в пула.
  • Изолиране в директория (sqlsrv_pathrules.c): C работният процес прилага правилата за пътища --allow-dir при всяко извикване на open_v2, разрешавайки символните връзки и отхвърляйки относителни пътища .., които излизат извън разрешената директория. sqlite3_load_extension и SQLITE_LIMIT_ATTACHED са деактивирани по подразбиране, така че SQL инжекция не може да излезе извън директорията на базата данни или да зареди произволни споделени обекти.

Структура на хранилището

Хранилището е структурирано така, че основният пакет предоставя драйвера за database/sql като заместител, докато launcher/ и sqlite3/ остават достъпни за разширени внедрявания с персонализиран сървър:

 1gosuda.org/sqlite3
 2├── driver.go / api.go         # Регистрация на драйвера за database/sql ("sqlite3") и API на Connector
 3├── dsn.go                     # Clean-room DSN парсер за mattn/go-sqlite3 и конфигурация на PRAGMA
 4├── conn.go / tx.go            # Жизнен цикъл на сесията, контекстен страж и машина на състоянията за транзакции
 5├── stmt.go / rows.go          # Конвейеризиране на заявки в един полет и предварително извличане на партиди от 64 KiB
 6├── errors.go                  # Типизирани първични/разширени кодове за грешки на SQLite и съпоставяне на sentinel грешки
 7├── autolaunch.go / hashes.go  # Материализиране на вграден сървър без конфигурация и фиксиране на SHA-256
 8├── internal/embedded/         # Двоични файлове с платформени етикети //go:embed (linux, darwin, windows)
 9├── launcher/                  # Конфигурируем супервайзър на процеси за внедрявания с персонализиран сървър
10└── sqlite3/                   # Генериран Go клиент hqq-bindgen и C11 изходен код на SQLite сървъра
11    └── server/                # Реализация на C11 сървъра (sqlite3_hooks.c, sqlsrv_policy.c)

Начало на работа

Инсталирайте модула с go get и отворете базата си данни с sql.Open("sqlite3", dsn):

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