gosuda.org/sqlite3: Драйвер database/sql для прямой замен, выполняющий нативный C SQLite через общую память
gosuda.org/sqlite3 представляет собой драйвер database/sql с CGO_ENABLED=0 для языка Go, который запускает вышестоящую C-реализацию SQLite amalgamation версии 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 amalgamation с помощью cgo. Вы получаете настоящий C-планировщик запросов, полную семантику параллелизма WAL и предсказуемое управление памятью в C. Цена за это — CGO_ENABLED=1: локальные сборки и конвейеры CI замедляются, минимальные образы контейнеров или образы без дистрибутива требуют наличия соответствующих библиотек среды выполнения C, кросс-компиляция под другую ОС или архитектуру ЦП требует полноценной цепочки инструментов кросс-компиляции C, а любое повреждение памяти в C затрагивает адресное пространство процесса Go.
С другой стороны располагаются чисто Go-порты SQLite, созданные путем механического перевода C-реализации amalgamation в исходный код Go поверх эмулируемой среды выполнения C (libc). Они восстанавливают CGO_ENABLED=0, что делает сборку тривиальной. Однако при постоянных рабочих нагрузках переведенная C-куча и выделения памяти в Go для каждой строки увеличивают нагрузку на сборщик мусора, а тонкие граничные случаи блокировок, тайм-аутов занятости или прерывания инструкций могут отличаться от нативных C-сборок.
Мы создали gosuda.org/sqlite3, чтобы приложениям на Go не приходилось выбирать между CGO_ENABLED=0 и настоящим C-движком SQLite. Путем компиляции официальной C-реализации SQLite 3.54.1 amalgamation в выделенный сервер совместной памяти (hqq-sqlite-server) и взаимодействия через gosuda.org/sqlite3, ваш двоичный файл Go компилируется с CGO_ENABLED=0, выполняя при этом неизмененный C-код SQLite, оптимизированный с флагом -O3, через границу процессов.
Выполнение без настройки: от 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 |
| macOS | arm64, amd64 | hqq-sqlite-server_darwin_<arch> | AppleClang -O3, происхождение SLSA Build L3 |
| Windows | amd64 | hqq-sqlite-server_windows_amd64.exe | MSVC -O2, происхождение SLSA Build L3 |
| 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) реализует реплику параметров подключения и прагм, используемых в кодовых базах 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
В обычном драйвере cgo для SQLite компонент database/sql управляет итерацией по одной строке и одному столбцу за раз. Чтение 1000 строк с 4 столбцами инициирует:
- 1000 вызовов
cgoкsqlite3_step - 4000 вызовов
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-воркере и возвращаются за один круговой обход (round-trip). - Плотные пакетные циклы 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 по первичному ключу) | 3.52 µs/op (9 байт, 1 выделение) | 3.56 µs/op (232 байта, 11 выделений) | 1.01x (паритет, в 11 раз меньше выделений) |
Сканирование 100 строк (rows.Next) | 20.41 µs/op (30.5 КБ, 211 выделений) | 36.34 µs/op (21.8 КБ, 309 выделений) | в 1.78 раза быстрее |
Сканирование 1000 строк (rows.Next) | 125.92 µs/op (296 КБ, 2030 выделений) | 349.10 µs/op (216 КБ, 3009 выделений) | в 2.77 раза быстрее |
| Подготовленная вставка (Автокоммит) | 5.33 µs/op (0 байт, 0 выделений) | 5.88 µs/op (120 байт, 5 выделений) | в 1.10 раза быстрее (0 выделений) |
| Пакет транзакции из 100 строк | 189.99 µs/op (16 байт, 1 выделение) | 191.50 µs/op (12.1 КБ, 503 выделения) | 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 типа «drop-in», в то время как папки launcher/ и sqlite3/ остаются доступными для расширенных развертываний пользовательских серверов:
1gosuda.org/sqlite3
2├── driver.go / api.go # регистрация драйвера database/sql ("sqlite3") и API соединителя
3├── dsn.go # парсер DSN и конфигурация прагм в чистой комнате для mattn/go-sqlite3
4├── conn.go / tx.go # жизненный цикл сеанса, защита контекста и конечный автомат транзакций
5├── stmt.go / rows.go # однорейсовая конвейеризация запросов и пакетная предвыборка по 64 КиБ
6├── errors.go # типизированные первичные/расширенные коды ошибок SQLite и сопоставление меток
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
- Репозиторий на GitHub: https://github.com/gosuda/hqq-sqlite3
- Справочник по DSN и прагмам: docs/DSN.md
- Архитектура и сетевой протокол: docs/ARCHITECTURE.md