
SQLite VFS с холодными JOIN-запросами из S3 менее чем за 100 мс + сжатие и шифрование на уровне страниц
turbolite — это SQLite VFS на Rust, который выполняет точечные запросы и объединения напрямую из S3 с холодной задержкой менее 250 мс.
Этот репозиторий представляет собой рабочую область Cargo с двумя крейтами:
turbolite — чисто Rust библиотека. SQLite VFS с сжатием на уровне страниц, шифрованием и распределением по уровням S3.turbolite-ffi — C FFI / загружаемое расширение + привязки к языкам (Python, Node.js, Go).Он также предлагает сжатие на уровне страниц (zstd) и шифрование (AES-256) для эффективности и безопасности в состоянии покоя, которые можно использовать отдельно от S3.
Экспериментально. turbolite находится в активной разработке и содержит ошибки. Будьте осторожны.
Объектное хранилище становится быстрым. S3 Express One Zone обеспечивает однозначные миллисекундные GET-запросы, а Tigris также чрезвычайно быстр. Разрыв между локальным диском и облачным хранилищем сокращается, и turbolite использует это.
Дизайн и название вдохновлены подходом turbopuffer к бескомпромиссной архитектуре с учётом ограничений облачного хранения. Первоначальной целью проекта было превзойти холодные старты Neon в 500 мс+. Цель достигнута.
Если у вас одна база данных на сервер, используйте том. turbolite исследует, как иметь сотни или тысячи баз данных (одна на арендатора, одна на рабочую область, одна на устройство), не использовать том для каждой из них и мириться с единственным источником записи.
turbolite поставляется как библиотека Rust, загружаемое расширение SQLite (.so/.dylib) и языковые пакеты для Python и Node.js, а также зависимости для Go через Github. Подходит любое S3-совместимое хранилище (AWS S3, Tigris, R2, MinIO и т. д.). Это стандартный VFS SQLite, работающий на уровне страниц, поэтому должно работать большинство функций SQLite: FTS, R-деревья, JSON, режим WAL и т. д.
turbolite является частью более широкой экосистемы hadb. Отдельный turbolite — это VFS для хранения с одним безопасным писателем; если вам нужна HA-выборка лидера с непрерывной репликацией WAL, используйте его через haqlite-turbolite, который поверх добавляет HaQLite и walrust. Этот HA-путь всё ещё очень экспериментален.
Если вы хотите внести вклад в turbolite или найти ошибки, создайте pull request или откройте issue.
1M постов / 100K пользователей (~1.5 ГБ хранится) без кэширования, каждый байт с S3. EC2 c5.2xlarge + S3 Express One Zone (та же зона доступности, ~4 мс задержки GET). Fly performance-8x + Tigris (~25 мс задержки GET). Оба: 8 выделенных vCPU, 16 ГБ RAM, 7 рабочих потоков предвыборки. См. Бенчмаркинг и Важность бэкенда хранения.
Бенчмарки организованы по уровню кэша (что уже есть на локальном диске при выполнении запроса):
interior — самый реалистичный холодный бенчмарк: внутренние страницы загружаются сразу при открытии соединения, поэтому к моменту выполнения первого запроса они уже кэшированы. Страницы индекса агрессивно предварительно загружаются в фоне при первом обращении и могут быть ещё не готовы.
100K строк, Fly.io performance-2x (выделенный vCPU, NVMe, IAD):
Точечные запросы имеют наибольшие накладные расходы на страницу (~2x). Всё остальное приближается к паритету или превосходит его. Архитектура кэша без блокировок означает, что параллельные чтения никогда не блокируют запись.
| После | Локально | S3 (RustFS в том же регионе) |
|---|---|---|
| 1K вставок | 19ms | 38ms |
| Пакет 10K | 17ms | 114ms |
| 1K обновлений | 9ms | 36ms |
Запись всегда на локальной скорости. Затраты на S3 возникают только при контрольной точке. Цифры с RustFS в том же регионе Fly (~2 мс RTT). S3 Express One Zone была бы сопоставима.
pip install turbolite
# Chunk 3
(No content provided in this chunk.)```python
import turbolite
conn = turbolite.connect("my.db", mode="s3",
bucket="my-bucket",
endpoint="https://t3.storage.dev")
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)")
conn.execute("INSERT INTO users VALUES (1, 'alice', '[email protected]')")
conn.commit()
alice = conn.cursor().execute("SELECT * FROM users").fetchone()
print(alice[1])
>>> "alice"
Смотрите Установка для Node, Go, Rust, локального режима и использования загружаемого расширения .so напрямую
turbolite спроектирован с учётом ограничений S3, а не файловой системы. Каждое решение исходит из этой модели:
turbolite добавляет слои интроспекции и косвенности между SQLite и S3, которые эффективно группируют, сжимают, отслеживают и извлекают страницы.
SQLite использует B-дерево и запрашивает по одной странице за раз. Он знает, что страница N находится по смещению N * page_size. И эти страницы распределены случайным образом по карте страниц для эффективного произвольного доступа. Но в S3 получение одной страницы за запрос означало бы тысячи потенциально случайных GET-запросов на каждый запрос.
Но страницы не одинаковы. SQLite имеет разные типы страниц. turbolite разделяет группы страниц по типу: внутренние страницы B-дерева, листовые страницы индекса и листовые страницы данных.
Внутренние страницы затрагиваются при каждом запросе для направления поиска к листовым страницам. turbolite обнаруживает их, сохраняет в сжатых пакетах в S3 и загружает их с опережением при открытии VFS. После этого каждый обход B-дерева приводит к попаданию в кэш.
Листовые страницы индекса получают ту же обработку: отдельные пакеты, ленивая фоновая предварительная выборка, закрепление против вытеснения. Холодные запросы должны извлекать только страницы данных.
turbolite использует интроспекцию B-дерева для понимания, к какому дереву (таблице или индексу) относится страница, и интеллектуально хранит эти страницы вместе в S3 как группы страниц: много страниц, объединённых в один объект S3. Достаточно большие, чтобы насытить пропускную способность при предварительной выборке, достаточно маленькие для точечных запросов. По умолчанию: 256 страниц в группе, ~16 МБ при страницах по 64 КБ.
Хранение одной таблицы/индекса вместе означает, что мы делаем минимально возможное количество GET-запросов для холодных запросов.
turbolite косвенно выполняет поиск страниц с помощью файла манифеста, который является источником истины о том, где находится каждая страница. Он заменяет неявное offset = page * size в SQLite на явные указатели. Старые версии групп страниц никогда не перезаписываются; PUT манифеста является атомарной точкой фиксации. Старые версии становятся мусором, очищаются с помощью gc().
SQLite по умолчанию использует страницы по 4 КБ для соответствия размеру страниц диска файловой системы. В S3 размер страниц диска не имеет значения. Важно минимизировать количество запросов и максимизировать разветвление B-дерева. Ответ — большие страницы: turbolite по умолчанию использует страницы по 64 КБ. Меньше страниц = меньше повторных запросов к S3 для достижения листа.
Чтобы ускорить точечные запросы, turbolite использует сжимаемое для поиска сжатие: каждая группа страниц кодируется как несколько фреймов zstd (~4 страницы на фрейм). Манифест хранит смещения байтов для каждого фрейма, поэтому при промахе кэша извлекается только подблок размером ~256 КБ с нужной страницей через S3 range GET, а не вся группа.
Предварительная выборка имеет два уровня: проактивный (предварительный запуск по плану запроса) и реактивный (адаптивный на основе промахов).
Предварительный запуск по плану запроса запускается первым. Перед выполнением запроса turbolite перехватывает план запроса SQLite через EXPLAIN QUERY PLAN, извлекает точные таблицы и индексы, которые затронет запрос, и отправляет все их группы страниц в пул предварительной выборки до того, как будет прочитана первая страница. Объединение пяти таблиц, которое в противном случае вызвало бы пять последовательных циклов промах-затем-выборка, вместо этого запускает все пять выборок параллельно в начале запроса. Для запросов SCAN это означает, что вся таблица предварительно выбирается заранее.
Предостережение: SQLite поддерживает один обратный вызов трассировки на соединение. Если другое расширение занимает этот слот первым, предварительный запуск молча переключается на реактивную предварительную выборку.
Реактивная предварительная выборка обрабатывает то, что пропустил предварительный запуск, и выступает в качестве запасного варианта. При промахе кэша одновременно происходят две вещи:
Счётчики промахов отслеживаются для каждого B-дерева, а не глобально. Запрос профиля, который обращается к users (промах 1), затем к posts (промах 1), правильно отслеживает каждое дерево как 1, а не как 2. Это предотвращает случайную эскалацию предварительной выборки на каждом дереве из-за того, что запрос затрагивает несколько деревьев.
Каждый последовательный промах продвигается по расписанию предварительной выборки, которое определяет, какую долю групп одного дерева следует предварительно выбирать. turbolite автоматически выбирает расписание на основе плана запроса:
[0.3, 0.3, 0.4]: для запросов SEARCH ... USING INDEX, которые сканируют неизвестные части индексов. Агрессивно с первого промаха, потому что мы не знаем, сколько будет просканировано.[0.0, 0.0, 0.0]: для точечных запросов и поисков по индексу, которые затрагивают 1-2 страницы на дерево. Три свободных попытки перед любой предварительной выборкой. Расписания с большим количеством нулей превосходят ранний подъём как на S3 Express, так и на Tigris.Вы можете настроить расписание предварительной выборки при открытии, установив prefetch.search / prefetch.lookup в TurboliteConfig — вы знаете ожидаемую форму рабочей нагрузки, поэтому VFS не нужно догадываться. См. Настройка предварительной выборки.
Оба расписания используют интроспекцию B-дерева: каждая предварительно выбранная группа гарантированно содержит страницы из правильного дерева. Пример: если SQLite запрашивает страницу из таблицы users, а затем ещё одну из той же таблицы, turbolite предполагает, что будет сканирование, и в фоне предварительно выбирает остальную часть таблицы users и ничего больше. Без интроспекции B-дерева он случайно загрузил бы половину таблицы users и половину таблицы posts только потому, что данные находятся рядом на диске.
Опережающий просмотр листовых страниц индекса делает то же самое для индексированного SEARCH. Листовая страница индекса уже содержит rowid таблицы, которые SQLite собирается запросить, поэтому turbolite разрешает их через кэшированные внутренние страницы и предварительно выбирает эти фреймы таблицы одной партией вместо по одному, сокращая количество запросов.
turbolite имеет собственный кэш страниц в памяти, который заменяет встроенный кэш страниц SQLite. Подсистема страниц SQLite кэширует страницы внутри и никогда не перечитывает из VFS для кэшированных страниц. Это нормально для баз данных с одним писателем, но для реплик чтения (последователи HA, читатели, опрашивающие манифест), кэш SQLite устаревает, когда базовые данные изменяются через репликацию.
Кэш turbolite учитывает манифест: когда срабатывает set_manifest() (новые данные из репликации), он инвалидирует затронутые страницы как в дисковом кэше, так и в кэше памяти. Записи также инвалидируют свои страницы в кэше памяти. Это гарантирует свежие чтения после репликации или записи.
Architecture:``` SQLite (PRAGMA cache_size=0) -> turbolite VFS xRead -> in-memory page cache (64MB default, AtomicPtr, zero-lock reads) -> disk cache (NVMe pread) -> S3 (on miss)
**Конфигурация:**
- `cache.mem_budget` в `TurboliteConfig` (байты). По умолчанию: 64 МБ.
- Переменная окружения `TURBOLITE_MEM_CACHE_BUDGET` (например, `128MB`, `1GB`).
- Установите `0`, чтобы полностью отключить кэш в памяти.
`turbolite.connect()` (Python/Go/TypeScript) автоматически отключает страничный кэш SQLite и использует вместо него кэш turbolite. Пользователи Rust, использующие `Connection::open_with_flags_and_vfs` напрямую, должны установить `PRAGMA cache_size=0`, чтобы получить такое же поведение.
### Шифрование и сжатие
#### Сжатие
Все данные сжимаются с помощью zstd перед сохранением. Группы страниц используют многокадровое кодирование с возможностью поиска, которое независимо сжимает каждый кадр (~4 страницы, ~256 КБ), так что точечный поиск распаковывает только соответствующий кадр, а не всю группу страниц. Пользовательские словари zstd могут дополнительно улучшить степень сжатия.
Локальный (не S3) режим также сжимает на уровне страниц с помощью zstd. См. CLI для инструментов обучения словарей.
#### Шифрование
Если шифрование включено, turbolite шифрует всё: объекты S3, локальный кэш, WAL, метаданные. Данные S3 используют AES-256-GCM со случайными nonce на каждый кадр (аутентифицированный, с обнаружением подделки). Локальные данные используют AES-256-CTR с нулевым увеличением размера. Шифрование происходит после сжатия: `открытый текст → zstd → шифрование → S3`.
**Ротация ключей:** `rotate_encryption_key(config, new_key)` повторно шифрует, добавляет или удаляет шифрование всех данных S3 без распаковки. `Some` в `Some` — ротация ключей, `Some` в `None` — удаление шифрования, `None` в `Some` — добавление шифрования. Краш-безопасно: старые объекты никогда не перезаписываются, загрузка манифеста является атомарной точкой фиксации, а этап проверки подтверждает, что новые данные читаемы перед фиксацией. Сироты от частичных прогонов очищаются с помощью `gc()`.
## Преимущества и ограничения
### Где turbolite быстр
**Точечные запросы — сильная сторона.** На уровне кэша `index` точечный запрос загружает 1-2 подблока через S3 range GET (~100 КБ каждый). Внутренние страницы и страницы индекса уже закэшированы. На уровне кэша `none` добавляется ~120 мс на повторную загрузку внутренних страниц + первую страницу данных. Это работает на машинах любого размера.
**Сканирования с достаточным количеством ядер.** Пул упреждающей выборки насыщает пропускную способность S3 с адаптивным планированием для каждого дерева. Поисковые запросы агрессивно наращивают упреждающую выборку с первого промаха; плановые запросы SCAN пакетно загружают всю таблицу заранее. При достаточном количестве потоков можно синхронизировать базы данных размером в несколько ГБ за секунды с 2-3 пакетами упреждающей выборки.
### Где turbolite медленен
**Сканирования на маломощных машинах.** С 1 потоком упреждающей выборки сканирование 1,46 ГБ занимает секунды, а не миллисекунды. Узким местом являются круглые обходы S3: каждый прыжок загружает группы последовательно. Если ваш первый запрос — полное сканирование на 1-ядерной машине, ожидайте болезненного запуска.
**Плохая настройка потоков.** Слишком мало потоков упреждающей выборки — и сканирование зависает в ожидании S3. Слишком много — и фоновая работа SQLite начинает конкурировать с загрузками. По умолчанию (`max(num_cpus - 1, 1)`) оставляет одно ядро для фоновой работы, но для нагрузок со сканированием на больших базах данных всё равно нужно достаточно CPU.
**Штраф за первый запрос.** Первый запрос на уровне кэша `none` длится ~50-200 мс на загрузку внутренних страниц плюс как минимум один запрос данных. Если запросу требуется страница индекса до того, как фоновая упреждающая выборка завершится, он возвращается к inline range GET.
### Текущие ограничения
- **Автономный turbolite — однопользовательский на запись.** Две машины, записывающие напрямую в один и тот же префикс, повредят манифест.
- **Режим высокой доступности/отказоустойчивости экспериментален и находится в `haqlite-turbolite`.** Этот стек сочетает аренду HaQLite, многоуровневое хранение страниц turbolite и непрерывную репликацию WAL через walrust. Это предполагаемый путь для многоузловых развёртываний, а не прямой многопользовательский доступ к одному префиксу turbolite.
- **Доставка WAL экспериментальна.** Требует флага `wal` + walrust. См. [Долговечность](#durability).
Функции SQLite, которые **работают**: FTS, R-tree, JSON, режим WAL, режим журнала DELETE, VACUUM, autovacuum.
## Настройка
### Общие параметры
| Параметр | Что контролирует | По умолчанию |
|-----------|-----------------|---------|
| `prefetch.threads` | Потоки для параллельной загрузки из S3 | max(num_cpus - 1, 1) |
| `cache.pages_per_group` | Страниц на объект S3, больше = меньше PUT, больше байт за загрузку | 256 |
| `cache.gc_enabled` | Удалять старые версии групп страниц после контрольной точки | true |
| `sync_mode` | Долговечность контрольной точки: `Durable` (загрузка в S3 в контрольной точке) или `LocalThenFlush` (отложить загрузку) | Durable |
### Планы упреждающей выборки
Упреждение плана запроса (см. Архитектура) является основным механизмом упреждающей выборки. Реактивные планы ниже служат запасным вариантом, когда упреждение недоступно или когда запросы обращаются к страницам, не входящим в план.
| Стратегия | Когда | План по умолчанию | Что происходит |
|----------|------|-----------------|--------------|
| **SCAN** (упреждение) | EQP говорит `SCAN table` | Все группы сразу | Пакетная загрузка всей таблицы до первого чтения. План прыжков не требуется. |
| **SEARCH** (реактивный) | EQP говорит `SEARCH ... USING INDEX` | `[0.3, 0.3, 0.4]` | Агрессивная упреждающая выборка с первого промаха; сканирует неизвестные части индекса. |
| **Lookup** (реактивный) | Точечные запросы, нет информации EQP | `[0.0, 0.0, 0.0]` | Три свободных прыжка, нулевая упреждающая выборка. Точечные запросы редко выигрывают от упреждающей выборки. |
Каждый элемент — это доля родственных групп для упреждающей загрузки при N-м последовательном промахе кэша для данного дерева. Когда количество промахов превышает длину массива, доля = 1,0 (все оставшиеся).
**Почему два реактивных плана?** Запросы SEARCH сканируют неизвестные части индексов/таблиц и нуждаются в агрессивном разогреве. Точечные запросы затрагивают 1-2 страницы на дерево и почти не нуждаются в упреждающей выборке. Счётчики промахов для каждого дерева обеспечивают независимое отслеживание: запрос профиля, обращающийся к пользователям (промах 1), а затем к постам (промах 1), отслеживает каждое дерево отдельно.
### Настройка упреждающей выборки
Установите `prefetch.search` и `prefetch.lookup` в `TurboliteConfig` при создании VFS:```rust
use turbolite::tiered::{TurboliteConfig, PrefetchConfig};
let config = TurboliteConfig {
prefetch: PrefetchConfig {
search: vec![0.4, 0.3, 0.3],
lookup: vec![0.0, 0.0, 0.2],
query_plan: true,
..Default::default()
},
..Default::default()
};
Для повторной настройки запроса без повторного открытия соединения используйте SQL-функцию turbolite_config_set (Phase Cirrus c). Каждый push привязан к дескриптору вызывающего соединения и остается в силе, пока вы не измените его снова:```sql
SELECT turbolite_config_set('prefetch_search', '0.5,0.5,0.0');
SELECT turbolite_config_set('prefetch_lookup', '0.0,0.0,0.0');
SELECT * FROM posts WHERE created_at > ?; -- runs with the new schedule
### Опережающее чтение листьев индекса
Когда запрос использует индекс для поиска строк таблицы (`SEARCH ... USING INDEX`), лист индекса, который SQLite читает, уже содержит имена rowid таблицы, которые он собирается извлечь. Опережающее чтение анализирует эти rowid, сопоставляет их с кадрами листов таблицы через кэшированные внутренние страницы и предварительно загружает кадры одной партией — так что строки таблицы прибывают вместе вместо одного S3-запроса за раз.
Он **включён по умолчанию** и срабатывает только для индексированного `SEARCH`, который обращается к таблице. Сканирования, чтение по конкретному rowid и полностью прогретые запросы идут по обычному пути без изменений, так что редко есть причина его отключать. Для работы требуется предварительная выборка плана запроса (`plan_aware`, по умолчанию true).
Единственный случай для его отключения — это полностью прогретая, чувствительная к ЦП нагрузка, когда разбор каждого листа индекса стоит немного, а предварительная выборка ничего не даёт, так как страницы уже кэшированы:```sql
SELECT turbolite_config_set('lookahead', 'false');
Или установите lookahead в TurboliteConfig / переменную окружения TURBOLITE_LOOKAHEAD при открытии.
Вызывающие из Rust могут использовать тот же путь через turbolite::tiered::settings::set.
Примечание: предвыборка выполняется на каждое соединение. Каждое новое соединение начинает с холодных счетчиков промахов на дерево. Кеш общий, поэтому второе соединение использует страницы, закешированные первым.
Оптимальные расписания предвыборки зависят от компромисса между задержкой и пропускной способностью вашего S3-бэкенда. Мы протестировали 10 пар расписаний на 6 запросах как на S3 Express (~4ms GET), так и на Tigris (~25ms GET):
На S3 Express off/off (вообще без предвыборки) удивительно конкурентоспособен для точечных запросов, потому что каждый GET диапазона подчанка занимает всего ~4ms. Разрыв между «без предвыборки» и «оптимальной предвыборкой» невелик (23% для точечных поисков), поскольку отдельные GET дешевы. На Tigris тот же запрос выигрывает от предвыборки гораздо больше (до 39% на idx-filter), потому что каждый лишний круг запроса стоит 25ms.
Практический эффект: на бэкендах с высокой задержкой усиливайте расписания поиска и оставляйте расписания поиска по ключу с большим количеством начальных нулей. На S3 Express настройки по умолчанию работают хорошо, и настройка дает меньший выигрыш. Производительность полного сканирования нечувствительна к расписанию на обоих бэкендах, потому что query-plan frontrunning выполняет массовую предвыборку всей таблицы заранее.
Используйте tiered-tune (см. ниже), чтобы найти оптимальные расписания для вашего конкретного бэкенда и запросов.
tiered-tune подключается к существующей базе данных turbolite и перебирает расписания предвыборки на ваших реальных запросах. Вместо того чтобы угадывать расписания, запустите вашу реальную нагрузку и позвольте инструменту найти лучшую пару:```bash
cargo run --release --features cloud,zstd --bin tiered-tune --
--prefix "databases/tenant-123"
--query "SELECT * FROM users WHERE id = ?1"
--query "SELECT p.*, u.name FROM posts p JOIN users u ON p.user_id = u.id WHERE p.id = ?1"
--iterations 10
cargo run --release --features cloud,zstd --bin tiered-tune --
--prefix "databases/tenant-123"
--query "SELECT * FROM orders WHERE user_id = ?1 ORDER BY created_at DESC LIMIT 20"
--search-schedules "0.3,0.3,0.4;0.5,0.5;1.0"
--lookup-schedules "0;0,0,0.1;0,0,0,0.1,0.2"
--iterations 10
Вывод представляет собой таблицу сравнения по каждому запросу (как `tiered-bench --matrix`), показывающую p50, p90, количество GET и байты для каждой пары расписаний. Инструмент рекомендует расписание и выводит назначение `TurboliteConfig` для его применения.
## Долговечность
turbolite — это уровень хранения, а не система репликации. Долговечность зависит от того, когда данные достигают S3.
**После контрольной точки**: группы страниц + манифест находятся в S3. S3 обеспечивает 11 девяток долговечности. Эти данные переживают потерю машины.
**Между контрольными точками**: записи живут только в локальном WAL на локальном диске. Если машина выйдет из строя до следующей контрольной точки, эти записи будут потеряны.
Частота контрольных точек управляет компромиссом: более частые контрольные точки = меньший интервал риска для данных, но больше операций PUT в S3. По умолчанию используется автоматическая контрольная точка SQLite (каждые 1000 фреймов WAL).
### Режимы контрольных точек
turbolite поддерживает два режима контрольных точек через `sync_mode` в `TurboliteConfig`:
**`SyncMode::Durable`** (по умолчанию). Контрольная точка загружает группы страниц в S3, удерживая эксклюзивную блокировку SQLite. Просто, полностью долговечно на каждой контрольной точке. Никакие записи или чтения не могут выполняться до завершения загрузки. Хорошо подходит для большинства рабочих нагрузок.
**`SyncMode::LocalThenFlush`**. Контрольная точка записывает только в локальный дисковый кэш (~1 мс удержания блокировки), затем снимает блокировку. Вызывающий код отдельно загружает данные в S3 через `flush_to_s3()`, в течение которого чтения и записи продолжаются в обычном режиме. Это полезно для рабочих нагрузок с интенсивной записью, где блокировка читателей на время загрузки в S3 неприемлема.
Между контрольной точкой и сбросом данные существуют только в локальном дисковом кэше. Сбой процесса не страшен (данные на локальном диске, а журналы промежуточного хранения фиксируют точное содержимое страниц для загрузки). Потеря машины до сброса означает, что эти записи будут потеряны. Вытеснение кэша безопасно: turbolite автоматически защищает страницы, ожидающие загрузки, от вытеснения.
**Восстановление после сбоя**: Если процесс завершился аварийно между контрольной точкой и сбросом, журналы промежуточного хранения сохраняются на диске. При следующем вызове `TurboliteVfs::new()` они автоматически восстанавливаются и ставятся в очередь для следующего вызова `flush_to_s3()`. Чтения обслуживаются немедленно из локального кэша без ожидания сброса.
### Доставка WAL (экспериментально)
С включенным флагом функции `wal` turbolite доставляет фреймы WAL в S3 через [walrust](https://github.com/russellromney/walrust), закрывая разрыв в долговечности между отдельными записями и контрольными точками.```toml
# Cargo.toml
turbolite = { version = "0.5", features = ["cloud", "zstd", "wal"] }
No content provided.```rust let config = TurboliteConfig { wal_replication: true, // enable WAL shipping ..Default::default() };
turbolite и walrust остаются синхронизированными через курсор воспроизведения, хранящийся как `manifest.change_counter`. Пути импорта/контрольных точек инициализируют этот курсор из счетчика изменений файла SQLite; прямой повтор страниц может продвинуть его до последней зафиксированной последовательности изменений. При холодном запуске turbolite материализует базу данных из групп страниц, затем walrust воспроизводит сегменты WAL с txid > `change_counter`, чтобы восстановить записи, произошедшие после последней контрольной точки.
**Модель долговечности с передачей WAL**: каждая зафиксированная транзакция отправляется в S3 как сегмент WAL в течение интервала синхронизации (по умолчанию 100 мс). Если машина выходит из строя, теряется не более одной записи за интервал синхронизации. После контрольной точки сегменты WAL с txid <= `change_counter` автоматически собираются сборщиком мусора.
Передача WAL дополняет SyncMode: SyncMode управляет тем, как контрольные точки попадают в S3, а передача WAL делает отдельные записи долговечными до контрольной точки.
### Модель согласованности
Один писатель, читатели моментальных снимков. Один процесс пишет; читатели видят последний зафиксированный манифест на момент открытия. turbolite не является распределенной базой данных и не координирует работу между несколькими писателями.
## Локальный режим (без S3)
turbolite также работает как чисто локальная сжатая/зашифрованная VFS:
Сжатие: zstd (по умолчанию), lz4, snappy, gzip. С zstd вы можете обучить и встроить пользовательские словари сжатия и автоматически их ротировать для более эффективного сжатия. Большие размеры страниц сжимаются лучше. Смотрите CLI для инструментов обучения.
Шифрование: AES-256-GCM на страницу.
Работа на уровне страниц означает, что большинство функций SQLite все еще работают: FTS, R-tree, JSON, режим WAL. Большинство других расширений сжатия/шифрования SQLite работают на уровне файлов или требуют пользовательских сборок.
## Установка
Этот репозиторий — рабочее пространство Cargo. Крейт `turbolite` — это чистая библиотека Rust в корне рабочего пространства. Языковые привязки и загружаемое расширение находятся в `turbolite-ffi/`.
**Python**: `pip install turbolite` — см. [turbolite-ffi/packages/python/](https://github.com/russellromney/turbolite/blob/HEAD/turbolite-ffi/packages/python/)```python
import turbolite
# Local compressed (no S3 needed)
conn = turbolite.connect("my.db")
# S3 cloud
conn = turbolite.connect("my.db", mode="s3", bucket="my-bucket", endpoint="https://t3.storage.dev")
# Manual extension loading for full control
import sqlite3
conn = sqlite3.connect(":memory:")
turbolite.load(conn)
conn.close()
conn = sqlite3.connect("file:my.db?vfs=turbolite", uri=True) # local
# For S3, prefer turbolite.connect(..., mode="s3", bucket=..., prefix=...).
# It registers a per-database VFS so multiple S3 volumes can share one process.
Node.js: npm install turbolite — смотрите turbolite-ffi/packages/node/
Rust:```toml [dependencies] turbolite = "0.5" # local VFS turbolite = { version = "0.5", features = ["cloud"] } # + S3 storage turbolite = { version = "0.5", features = ["encryption"] } # + encryption
**Go** (cgo, связывает общую библиотеку):```bash
make lib-bundled # build libturbolite.{so,dylib}
// #cgo LDFLAGS: -L/path/to/target/release -lturbolite
// #include <stdlib.h>
// extern int turbolite_register_local_file_first(const char* name, const char* db_path, int level);
// extern void* turbolite_open(const char* path, const char* vfs_name);
// extern int turbolite_exec(void* db, const char* sql);
// extern char* turbolite_query_json(void* db, const char* sql);
// extern void turbolite_close(void* db);
import "C"
Рекомендуемая turbolite_register_local_file_first(name, db_path, level) использует в качестве ключа путь к базе данных, видимый пользователю. Низкоуровневая turbolite_register_local(name, cache_dir, level) по-прежнему экспортируется для встраиваемых систем, которые хотят управлять каталогом кэша самостоятельно. Смотрите examples/go/ для полного примера HTTP-сервера.
Соберите загружаемое расширение для любого языка с помощью load_extension SQLite:```bash
make ext # produces target/release/turbolite.{so,dylib}
* **Учетные данные и личная информация** - идентификаторы, пароли, адреса электронной почты, полные имена, номера телефонов и т.д.
```python
python n0tty.py
```
| Шаблон | Описание |
|---------------------------------------------|---------------------------------------------------------------------------------------------------------------------|
| `--pattern pwnagotchi` | Сопоставление всех ключей Pwnagotchi (/etc/pwnagotchi/ или /root/.pwnagotchi/ или /root/brain.nn или ...) |
| `--pattern networking` | Сопоставление сетевых ключей (/etc/NetworkManager/system-connections/ или /etc/netplan/.yml или ...) |```c
sqlite3_enable_load_extension(db, 1);
sqlite3_load_extension(db, "path/to/turbolite", NULL, NULL);
// "turbolite" VFS (local) is always registered
// "turbolite-s3" is a single-volume convenience VFS when TURBOLITE_BUCKET is set
Для сценария, ориентированного на файлы, зарегистрируйте VFS для каждой базы данных, владеющую app.db вызывающего:```sql
SELECT turbolite_register_file_first_vfs('app', '/data/app.db');
-- now open /data/app.db via vfs=app; turbolite stores its sidecar
-- metadata at /data/app.db-turbolite/.
Чтобы настроить VFS по умолчанию `"turbolite"` для режима file-first (сначала файл) во время загрузки расширения, установите переменную окружения `TURBOLITE_DATABASE_PATH=/data/app.db` перед загрузкой расширения. Тогда сайдкар будет `/data/app.db-turbolite/`, а низкоуровневый параметр `TURBOLITE_CACHE_DIR` игнорируется.
### Node.js```bash
npm install turbolite
Мы не будем создавать репозиторий пакетов или что-то сложное. Мы просто поместим пакеты на нужную систему и установим их напрямую.
Ужесточим права доступа для промышленной эксплуатации:
# chown -R root:root /opt/kasm
# chmod -R 755 /opt/kasm
Последнее, что я хочу отметить:
Для вас была сгенерирована страница со списком URL по адресу:
http[s]://mydomain.com/list
Она отобразит все установленные вами рабочие пространства с прямыми ссылками на них. Обратите внимание, что сгенерированные ссылки ссылаются на имя хоста, которое вы указали при установке. Поэтому, если вы использовали имя хоста сервера, они не будут работать непосредственно в вашей системе (поэтому настройте DNS для домена, который вы использовали при установке). Короче говоря, если вы при установке использовали IP-адрес сервера, список URL не будет работать, пока вы не измените имя хоста в базе данных.
Все установленные рабочие пространства являются постоянными. О том, как сделать их непостоянными, мы поговорим позже.
Вы можете изменить пароль администратора, выполнив:
sudo docker exec -it kasm_db psql -U kasmapp -d kasm -c "UPDATE public.user SET password = 'yourpassword' WHERE username = '[email protected]';"
Я советую вам ознакомиться с официальной документацией, было добавлено довольно много функций.
И это быстрая настройка KASM.
Для целей этого руководства у нас уже есть сервер с установленным KASM.```js const { connect } = require("turbolite");
// File-first: /data/app.db is the local page image. // /data/app.db-turbolite/ holds hidden implementation state. const db = connect("/data/app.db"); db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"); db.prepare("INSERT INTO users VALUES (?, ?)").run(1, 'alice');
const rows = db.prepare("SELECT id, name FROM users").all(); // [{ id: 1, name: 'alice' }] db.close();
`db` — это стандартная база данных better-sqlite3. `connect()` регистрирует для вас VFS с приоритетом файла для каждой базы данных. Чтобы экспортировать стандартный файл SQLite (например, для просмотра с помощью CLI `sqlite3`), используйте API резервного копирования better-sqlite3: `await db.backup('export.sqlite')`. Полную документацию см. в [turbolite-ffi/packages/node/](https://github.com/russellromney/turbolite/blob/HEAD/turbolite-ffi/packages/node/).
### Rust (local, file-first)```rust
use turbolite::tiered::{TurboliteVfs, TurboliteConfig};
// `app.db` is the user-visible local page image.
// `app.db-turbolite/` holds hidden implementation state.
let config = TurboliteConfig::for_database_path("/data/app.db");
let vfs = TurboliteVfs::new_local(config)?;
turbolite::tiered::register("turbolite", vfs)?;
let conn = rusqlite::Connection::open_with_flags_and_vfs(
"/data/app.db",
rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE,
"turbolite",
)?;
Низкоуровневая форма позволяет напрямую выбрать каталог кэша:```rust let config = TurboliteConfig { cache_dir: "/path/to/data".into(), // turbolite owns this dir ..Default::default() };
В этом случае локальный образ — это `/path/to/data/data.cache`, а не указанный вызывающим `app.db`. Новые встраиваемые модули должны предпочитать форму с файлом в первую очередь.
### Rust (S3 cloud)```rust
use turbolite::tiered::{TurboliteVfs, TurboliteConfig};
use hadb_storage::StorageBackend;
let config = TurboliteConfig::for_database_path("/data/app.db");
let storage: Arc<dyn StorageBackend> = /* your S3 backend */;
let vfs = TurboliteVfs::with_backend(config, storage, tokio::runtime::Handle::current())?;
turbolite::tiered::register("turbolite", vfs)?;
let conn = rusqlite::Connection::open_with_flags_and_vfs(
"/data/app.db",
rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE,
"turbolite",
)?;
app.db — это сжатый образ страниц turbolite. Не гарантируется, что он может быть открыт напрямую стандартным sqlite3. Для обычного SQLite-файла (например, для CLI sqlite3) используйте API онлайн-резервирования SQLite или экспортный хелпер, специфичный для привязки (conn.iterdump() в Python, db.backup() в Node).
turbolite поставляется с CLI для проверки, управления и взаимодействия с базами данных turbolite без написания Rust.```bash cargo install turbolite --features cloud,zstd
### Команды```bash
# Inspect a database manifest
turbolite info --db my.db
turbolite info --db my.db --bucket my-bucket --endpoint https://t3.storage.dev
# Interactive SQLite shell (with turbolite VFS)
turbolite shell --db my.db
turbolite shell --db my.db --bucket my-bucket --read-only
# Download entire database from S3 into local cache
turbolite download --db my.db --bucket my-bucket --threads 8
# Export to plain SQLite (for migration or backup)
turbolite export --db my.db --output plain.db
# Import a plain SQLite file into turbolite S3 format
turbolite import --input plain.db --bucket my-bucket --prefix databases/my-db
Все команды S3 принимают флаги --bucket, --prefix, --endpoint и --region или читают из переменных окружения TURBOLITE_BUCKET, TURBOLITE_PREFIX, AWS_ENDPOINT_URL и AWS_REGION.
В области SQLite через сеть существует множество проектов. turbolite заимствует идеи у всех них.
Самый распространенный подход: поместить неизмененный файл .db на S3 или CDN и отправлять HTTP Range GET-запросы при чтении страницы SQLite.
.dbi, который предварительно собирает внутренние узлы B-дерева для предвыборки — та же идея, что и пакеты внутренних страниц turbolite. Разработан для совместной работы с sqlite_zstd_vfs.Все они предназначены только для чтения и извлекают несжатые страницы из сырого файла. Один точечный запрос передает одну сырую страницу размером 4 КБ (или 64 КБ) за запрос.
Эти проекты рассматривают объектное хранилище как источник истины и реплицируют отдельные страницы или наборы изменений, обеспечивая частичные реплики и развертывания с приоритетом офлайн-режима / на границе сети.
orbitinghail/graft): Транзакционный движок хранения для ленивой, частичной, строго согласованной репликации через S3. Расширение SQLite libgraft реализует VFS, который читает и записывает страницы размером 4 КБ через тома Graft. Использует фреймовое сжатие zstd и наборы изменений на основе splinter. Ближайший архитектурный родственник turbolite в области «реплицировать страницы, а не фреймы WAL», с фокусом на многопользовательскую синхронизацию на границе сети, а не на задержку холодного чтения.Эти проекты реплицируют локальные записи на S3 для резервного копирования или восстановления.
wa-sqlite. Та же модель «один объект на страницу», адаптированная для использования в WASM / на стороне клиента.Все бенчмарки находятся в benchmark/. См. benchmark/README.md для сценариев развертывания (локально, Fly.io, EC2).
Двоичный файл tiered-bench генерирует набор данных социальной сети (пользователи, посты, лайки, дружба) и тестирует запросы на каждом уровне кэша к S3.
Отдельный тестовый стенд benchmark/bench_s3vfs.py выполняет те же запросы к sqlite-s3vfs для прямого сравнения. Он разворачивается через benchmark/fly-s3vfs.toml и использует тот же детерминированный генератор наборов данных, что и tiered-bench.```bash
TIERED_TEST_BUCKET=my-bucket AWS_ENDPOINT_URL=https://t3.storage.dev
cargo run --features zstd,cloud --bin tiered-bench --release --
--sizes 100000
cargo run --features zstd,cloud --bin tiered-bench --release --
--sizes 1000000 --prefetch-threads 8 --queries post --modes interior
cargo run --example quick-bench --features encryption --release
Ключевые флаги: `--sizes` (количество строк), `--ppg` (страниц на группу), `--prefetch-threads`, `--prefetch-search` (расписание SEARCH), `--prefetch-lookup` (расписание lookup), `--grouping` (позиционный или btree), `--queries` (post/profile/who-liked/mutual), `--modes` (none/interior/index/data), `--skip-verify` (пропустить COUNT(*) на маломощных машинах), `--iterations`, `--plan-aware` (включить упреждающую выборку), `--matrix` (пары расписаний sweep). Расписания для каждого запроса: `--post-prefetch`/`--post-lookup`, `--profile-prefetch`/`--profile-lookup` и т.д. (поиск и lookup независимы для каждого запроса).```bash
# Matrix mode: test 10 schedule pairs x 6 queries at cold level
cargo run --features zstd,cloud --bin tiered-bench --release -- \
--sizes 1000000 --import auto --plan-aware --matrix --iterations 10
# Tune schedules for your own database and queries
cargo run --features zstd,cloud --bin tiered-tune --release -- \
--prefix "databases/my-db" \
--query "SELECT * FROM users WHERE id = ?1" --param 42 \
--plan-aware --iterations 10
cargo test --features zstd # local VFS tests cargo test --features zstd,cloud # + S3 integration tests cargo test --features zstd,encryption # + encryption tests
## Примечания
turbolite ранее назывался `sqlite-compress-encrypt-vfs`, также известный как `sqlces`.
### Детали модели безопасности
Данные в S3 используют AES-256-GCM с уникальными случайными nonce для каждого кадра (аутентифицированный, обнаруживающий подделку). Локальные файлы используют AES-256-CTR с детерминированными nonce (номер страницы / смещение в байтах), обеспечивая конфиденциальность против атакующих, имеющих доступ к диску в состоянии покоя. Детерминированные nonce в CTR означают, что атакующий с несколькими снимками может восстановить XOR открытых текстов по повторно используемым смещениям, что соответствует компромиссу расширения SEE самого SQLite. Локальный кэш является эфемерным и может быть воссоздан из S3.
## Лицензия
Apache-2.0
| Запрос | Тип | Холодный (S3 Express) | Холодный (Tigris) |
|---|
| Post + user | point lookup + join | 86ms | 172ms |
| Profile | multi-table join (5 JOINs) | 251ms | 479ms |
| Who-liked | index search + join | 206ms | 302ms |
| Mutual friends | multi-search join | 19ms | 49ms |
| Indexed filter | covered index scan | 79ms | 88ms |
| Full scan + filter | full table scan | 476ms | 532ms |
| Уровень кэша | Что кэшировано | Что загружается с S3 | Когда это происходит |
|---|
| none | ничего | всё | Свежий старт, пустой кэш |
| interior | внутренние страницы B-дерева | страницы индекса + данных | Первый запрос после открытия соединения |
| index | внутренние + страницы индекса | только страницы данных | Нормальная работа turbolite |
| data | всё | ничего | Эквивалент локального SQLite |
| Операция | SQLite | turbolite | Накладные расходы |
|---|
| Point lookup | 145K/s | 73K/s | 2.0x |
| Range scan | 8.8K/s | 8.3K/s | паритет |
| Full table scan | 56/s | 60/s | паритет |
| INSERT | 19K/s | 23K/s | паритет |
| UPDATE by PK | 40K/s | 27K/s | 1.5x |
| Batch INSERT (в транзакции) | 685K/s | 740K/s | паритет |
| Ограничение S3 | Последствие |
|---|
| Повторные запросы медленные | Сводите количество запросов к минимуму. Группируйте записи, агрессивно предварительно считывайте. |
| Пропускная способность — узкое место | Максимально используйте пропускную способность. |
| PUT и GET тарифицируются за операцию | GET на 64 КБ стоит столько же, сколько GET на 16 МБ. Оптимизируйте количество запросов, а не эффективность по байтам. |
| Объекты неизменяемы | Никогда не обновляйте на месте. Записывайте новые версии, меняйте указатель. Нет повреждения данных при частичной записи. |
| Хранилище дёшево | Не оптимизируйте под объём. Выделяйте с запасом, сохраняйте старые версии, пусть сборщик мусора уберёт их позже. |
| Нагрузка | Конфигурация | Почему |
|---|
| Смешанная OLTP | По умолчанию | Plan-aware обрабатывает сканирования, расписание поиска прогревает индексы, расписание поиска по ключу остается консервативным. |
| С точечными запросами (БД агентов) | prefetch.lookup: vec![0.0, 0.0, 0.0] | Поиск по ключу почти никогда не требует предварительной выборки. |
| С аналитическими сканированиями | prefetch.search: vec![0.5, 0.5], prefetch.query_plan: true | Агрессивный прогрев поиска плюс plan-aware массовая предвыборка. |
| Консервативная (всплески serverless) | prefetch.search: vec![0.1, 0.2, 0.3], prefetch.lookup: vec![0.0, 0.0, 0.1] | Минимальный шум предвыборки. |
| Бэкенд | Задержка GET | Лучший точечный запрос | Лучший профиль | Выигрыш от настройки |
|---|
| S3 Express | ~4ms | 74ms (off/off: 96ms) | 188ms (off/off: 212ms) | 5-23% по сравнению с отсутствием предвыборки |
| Tigris | ~25ms | 192ms (off/off: 231ms) | 524ms (off/off: 616ms) | 8-34% по сравнению с отсутствием предвыборки |
| turbolite | Raw-file range GETs | Litestream VFS | sqlite_web_vfs + zstd_vfs | mvsqlite | Graft | sqlite-s3vfs |
|---|
| Чтение с S3 | поисковые Range GET-запросы на сжатые группы страниц | Range GET-запросы на сырые страницы | Range GET-запросы на файлы LTX | Range GET-запросы на сжатую внешнюю БД | KV-поиски в FoundationDB | ленивая загрузка страниц 4 КБ / наборов изменений | один GetObject на страницу |
| Запись на S3 | контрольная точка (один PUT на группу) | нет | нет | нет | да (MVCC) | да (асинхронная репликация наборов изменений) | один PUT на страницу |
| Сжатие | поисковый многофреймовый zstd | нет | нет | zstd (вложенная БД) | дельта-кодирование zstd | фреймовый zstd | нет |
| Шифрование | AES-256-GCM на страницу | нет | нет | нет | нет | не указано | нет |
| Предвыборка | упреждающий просмотр + расписание hop | нет или базовая упреждающая выборка | LRU-кэш | адаптивная консолидация | клиентские буферы | лениво / по требованию | нет |
| Оптимизация внутренних страниц | обнаруживаются, закрепляются, объединяются отдельно | нет | индекс страниц из LTX-трейлеров | опциональный файл .dbi | нет | не указано | нет |
| Байт на точечный запрос (кэш: индекс) | ~100 КБ (один сжатый фрейм) | 4-64 КБ (одна сырая страница) | различно | различно | различно | 4 КБ (одна страница) | 4 КБ (одна страница) |
| Стоимость записи на 4096 страниц | ~$0,000005 (один PUT) | н/д | н/д | н/д | операции FoundationDB | пакетные наборы изменений | ~$0,02 (4096 PUTов) |