gosuda.org/sqlite3: CGO_ENABLED=0 database/sql Driver поверх Shared-Memory IPC

gosuda.org/sqlite3 — это драйвер database/sql для Go с CGO_ENABLED=0, который запускает upstream C-амальгаму SQLite 3.54.1 внутри изолированного рабочего процесса через разделяемые кольцевые буферы HQQ. Он регистрируется под стандартным именем драйвера "sqlite3", поддерживает параметры строки запроса DSN из github.com/mattn/go-sqlite3, встраивает предварительно собранные серверные бинарные файлы с аттестацией SLSA для шести целевых комбинаций ОС и архитектуры и использует конвейеризацию запросов с предварительной выборкой пакетов по 64 КиБ, что позволяет превзойти встроенный cgo при выборке нескольких строк.
1go get gosuda.org/sqlite3@v0.3.3540100
Зачем еще один драйвер SQLite для Go?
SQLite является одним из самых широко используемых программных продуктов в мире, однако работа с ним из Go исторически ставила инженеров перед неудобным выбором между простотой сборки и точностью воспроизведения движка.
С одной стороны находится github.com/mattn/go-sqlite3, давний стандарт сообщества. Он напрямую оборачивает официальную C-амальгаму SQLite через cgo. Вы получаете настоящий планировщик запросов C, полную семантику конкурентности WAL и предсказуемое управление памятью C. Ценой этого является CGO_ENABLED=1: локальные сборки и CI-конвейеры замедляются, минимальные образы scratch или distroless требуют соответствующих библиотек времени выполнения C, кросс-компиляция для другой ОС или архитектуры процессора требует полного кросс-инструментария C, а любое повреждение памяти в C разделяет адресное пространство процесса Go.
С другой стороны находятся чисто Go-порты SQLite, созданные механической трансляцией C-амальгамы в исходный код Go поверх эмулируемой среды выполнения C (libc). Они восстанавливают CGO_ENABLED=0, что делает сборку тривиальной. Однако при длительных нагрузках транслированная куча C и выделения памяти для каждой строки в Go увеличивают нагрузку на сборщик мусора, а тонкие граничные случаи в блокировках, таймаутах ожидания занятости или прерывании операторов могут расходиться с нативными сборками C.
Мы создали gosuda.org/sqlite3, чтобы приложения на Go не были вынуждены выбирать между CGO_ENABLED=0 и настоящим движком C SQLite. Компилируя официальную C-амальгаму 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 требует изменения только пути пустого импорта. Драйвер регистрируется как "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 МиБ.
| Целевая ОС | Целевая архитектура (GOARCH) | Встроенный бинарный файл | Инструментарий сборки и происхождение |
|---|---|---|---|
| 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 |
Когда sql.Open устанавливает первое соединение, autolaunch.go материализует встроенный бинарный файл в пользовательский каталог кэша ($XDG_CACHE_HOME/hqq/bin/<sha256>/), проверяет его криптографическую контрольную сумму SHA-256 по закрепленной карте ReleaseHashes в hashes.go через gosuda.org/hqq/secureexec и запускает рабочий процесс с флагом --parent-stdin, чтобы его время жизни строго было связано с вашим процессом Go.
Совместимость DSN с mattn/go-sqlite3 (чистая реализация)
Чтобы работать как полноценная замена без изменений, анализатор DSN (dsn.go) реализует чистую (clean-room) копию параметров соединения и прагм, используемых в кодовых базах mattn/go-sqlite3:
| Параметр строки запроса DSN | Допустимые значения | Эффект в SQLite |
|---|---|---|
_busy_timeout / _timeout | Целое число миллисекунд (например, 5000) | Вызывает sqlite3_busy_timeout(db, ms) при открытии сеанса. |
_journal_mode / _journal | DELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFF | Выполняет PRAGMA journal_mode = <mode>. |
_txlock | immediate, exclusive, deferred | Выбирает BEGIN IMMEDIATE, BEGIN EXCLUSIVE или BEGIN DEFERRED для db.BeginTx. |
_foreign_keys / _fk | on, true, 1, off, false, 0 | Выполняет PRAGMA foreign_keys = ON/OFF. |
_synchronous / _sync | 0/OFF, 1/NORMAL, 2/FULL, 3/EXTRA | Выполняет PRAGMA synchronous = <level>. |
_cache_size | Целое число страниц или -KiB (например, -20000) | Выполняет PRAGMA cache_size = <val>. |
_loc | auto или часовой пояс IANA (например, UTC, Asia/Seoul) | Разбирает текстовые столбцы DATETIME/TIMESTAMP в *time.Time в целевой локации. |
mode | ro, 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 построчно
В обычном драйвере SQLite на основе cgo database/sql проходит по результатам строка за строкой и столбец за столбцом. Чтение 1 000 строк с 4 столбцами вызывает:
- 1 000 вызовов
cgoдляsqlite3_step - 4 000 вызовов
cgoдляsqlite3_column_typeиsqlite3_column_int64/sqlite3_column_text - Тысячи отдельных переходов границ между планировщиком Go и стеком C
Конвейеризация запросов и предварительная выборка пакетов по 64 КиБ
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
- Конвейеризация запроса в один полет (
stmt.go): КогдаQueryContextилиStmt.QueryContextвыполняет оператор только для чтения,s.query()асинхронно отправляетreset,bind_values,stmt_info(при первом выполнении) иfetch_rowsв одном полете через разделяемую память. Все четыре операции выполняются последовательно на рабочем процессе C и возвращаются за одно обращение. - Плотные пакетные циклы C и асинхронная предварительная выборка (
rows.go): Внутриfetch_rows(sqlite3_hooks.c) рабочий процесс C выполняетsqlite3_step(stmt)в нативном цикле C, кодируя доFetchSizeстрок (по умолчанию 128) непосредственно в слот ответа разделяемой памяти размером 64 КиБ, пока не будет достигнут предел пакета, слот не заполнится или не наступит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 (паритет, в 11 раз меньше выделений) |
Сканирование 100 строк (rows.Next) | 20.41 µs/op (30.5 KB, 211 allocs) | 36.34 µs/op (21.8 KB, 309 allocs) | в 1.78 раза быстрее |
Сканирование 1 000 строк (rows.Next) | 125.92 µs/op (296 KB, 2,030 allocs) | 349.10 µs/op (216 KB, 3,009 allocs) | в 2.77 раза быстрее |
| Подготовленная вставка (автокоммит) | 5.33 µs/op (0 B, 0 allocs) | 5.88 µs/op (120 B, 5 allocs) | в 1.10 раза быстрее (0 allocs) |
| Пакетная транзакция из 100 строк | 189.99 µs/op (16 B, 1 alloc) | 191.50 µs/op (12.1 KB, 503 allocs) | 1.01x (в 500 раз меньше выделений) |
Отмена, изоляция путей и восстановление пула соединений
Выполнение SQLite в изолированном рабочем процессе также усиливает то, как драйвер обрабатывает тайм-ауты, недоверенный SQL и границы файловой системы:
- Детерминированная отмена контекста (
conn.goиerrors.go): Каждый вызов драйвера активирует легковесный защитный механизмcontext.AfterFunc(cn.guard). Еслиcontext.Contextвызывающего кода отменяется или истекает его срок, пока на сервере выполняется медленный рекурсивный запрос или ожидание блокировки, защитный механизм помечает соединение как неисправное, разрывает локальное сеансовое соединение и отправляет внеполосный кадр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 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)
Начало работы
Установите модуль с помощью go get и откройте базу данных через sql.Open("sqlite3", dsn):
1go get gosuda.org/sqlite3@v0.3.3540100
- Репозиторий GitHub: https://github.com/gosuda/hqq-sqlite3
- Справочник по DSN и прагмам: docs/DSN.md
- Архитектура и протокол обмена: docs/ARCHITECTURE.md