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 внутри изолированного рабочего процесса через разделяемые кольцевые буферы 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)Встроенный бинарный файлИнструментарий сборки и происхождение
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 по закрепленной карте 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 / _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 построчно

В обычном драйвере 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
  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 КиБ, пока не будет достигнут предел пакета, слот не заполнится или не наступит 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