
SQLite VFS with sub-100ms cold JOIN queries from S3 + page-level compression and encryption
turboliteは、Rustで書かれたSQLite VFSで、S3から直接ポイントルックアップと結合を提供し、コールドレイテンシが250ms未満です。
このリポジトリは、以下の2つのクレートからなるCargoワークスペースです。
turbolite — 純粋なRustライブラリ。ページレベルの圧縮、暗号化、S3階層化を備えたSQLite VFS。turbolite-ffi — C FFI / ロード可能な拡張 + 言語バインディング (Python, Node.js, Go)。また、ページレベルの圧縮(zstd)と暗号化(AES-256)を提供し、保存時の効率性とセキュリティを実現します。これらはS3とは別に使用することもできます。
実験段階. turboliteは活発に開発中であり、バグが含まれています。注意してください。
オブジェクトストレージは高速化しています。S3 Express One Zone は一桁ミリ秒のGETを提供し、Tigrisも非常に高速です。ローカルディスクとクラウドストレージのギャップは縮まりつつあり、turboliteはそれを活用しています。
設計と名前は、turbopuffer のクラウドストレージの制約を徹底的に考慮したアプローチに触発されています。このプロジェクトの初期目標は、Neonの500ms以上のコールドスタート を打ち負かすことでした。目標は達成されました。
サーバーごとに1つのデータベースがある場合は、ボリュームを使用してください。turboliteは、何百、何千ものデータベース(テナントごと、ワークスペースごと、デバイスごとに1つ)を持ち、それぞれにボリュームを持ちたくないが、単一の書き込みソースで問題ない場合の方法を探求しています。
turboliteは、Rustライブラリ、SQLiteロード可能拡張 (.so/.dylib)、Python および Node.js 向けの言語パッケージ、さらにGo用のGithub依存関係として提供されます。S3互換のストレージであれば何でも動作します(AWS S3、Tigris、R2、MinIOなど)。標準のSQLite VFSであり、ページレベルで動作するため、ほとんどのSQLite機能(FTS、Rツリー、JSON、WALモードなど)が動作するはずです。
turboliteは、より広範なhadb エコシステムの一部です。スタンドアロンのturboliteは、1つの安全な書き込み元を持つストレージVFSです。HAリーダー選出と継続的なWALレプリケーションが必要な場合は、haqlite-turbolite を介して使用してください。これはHaQLiteとwalrust を上層に追加します。そのHAパスはまだ非常に実験的です。
turboliteに貢献したい場合やバグを見つけた場合は、プルリクエストを作成するか、イシューを開いてください。
1M posts / 100K users (~1.5GB stored) with nothing cached, every byte from S3. EC2 c5.2xlarge + S3 Express One Zone (same AZ, ~4ms GET latency). Fly performance-8x + Tigris (~25ms GET latency). Both: 8 dedicated vCPU, 16GB RAM, 7 prefetch worker threads. See Benchmarking and Storage backend matters.
Benchmarks are organized by cache level (what's already on local disk when the query runs):
interior は最も現実的なコールドベンチマークです: 内部ページは接続開始時に先読みされ、最初のクエリを実行する頃にはキャッシュされています。インデックスページは最初のアクセス時にバックグラウンドで積極的にプリフェッチされますが、まだ準備ができていない可能性があります。
100K rows, Fly.io performance-2x (dedicated vCPU, NVMe, IAD):
ポイントルックアップはページあたりのオーバーヘッドが最も高くなります(約2倍)。それ以外はほぼ同等かそれ以上です。ロックフリーのキャッシュアーキテクチャにより、同時読み取りが書き込みをブロックすることはありません。
| After | Local | S3 (same-region RustFS) |
|---|---|---|
| 1K inserts | 19ms | 38ms |
| 10K batch | 17ms | 114ms |
| 1K updates | 9ms | 36ms |
書き込みは常にローカル速度です。S3のコストはチェックポイント時にのみ発生します。数値は同じFlyリージョン内のRustFSによるものです(~2ms RTT)。S3 Express One Zoneも同程度になるでしょう。
pip install turbolite
セキュリティ研究者が、プログラムのユーザー入力処理に問題を発見し、潜在的なコード実行の脆弱性につながることが判明しました。プロジェクトメンテナーは、最新リリースでこれらの懸念に対処しました。```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"
See インストール を参照してください(Node、Go、Rust、ローカルモード、.so ローダブルエクステンションの直接利用について)。
turbolite は、ファイルシステムの制約ではなく S3 の制約を前提に設計されています。すべての判断はこのモデルから導かれます。
turbolite は SQLite と S3 の間にイントロスペクションと間接層を追加し、ページを効率的にグループ化、圧縮、追跡、フェッチします。
SQLite は B-tree インデックスを使用し、一度に 1 ページずつ要求します。ページ N はバイトオフセット N * page_size にあることを認識しています。また、それらのページは効率的なランダムアクセスのためにページマップ全体にランダムに分散しています。しかし S3 では、1 リクエストあたり 1 ページを取得すると、クエリごとに数千ものランダムな GET が発生する可能性があります。
しかし、ページは均等に作られているわけではありません。SQLite には異なる種類のページがあります。turbolite は ページグループを種類ごとに分離 します。内部 B-tree、インデックスリーフ、データリーフの各ページです。
内部ページはすべてのクエリでアクセスされ、リーフページへのルックアップをルーティングします。turbolite はそれらを検出し、圧縮バンドルとして S3 に保存し、VFS オープン時に eager にロードします。その後は、すべての B-tree トラバーサルがキャッシュヒットになります。
インデックスリーフページも同様に扱います。個別のバンドル、遅延バックグラウンドプリフェッチ、退避防止のピン留め。コールドクエリではデータページのみをフェッチする必要があります。
turbolite は B-tree イントロスペクション を活用して、ページが どのツリー(テーブルまたはインデックス)の一部であるか を理解し、それらのページを S3 でインテリジェントに ページグループ としてまとめて保存します。多数のページを 1 つの S3 オブジェクトにチャンク化します。プリフェッチ時に帯域を飽和させるのに十分な大きさであり、ポイントクエリには十分に小さい。デフォルト: グループあたり 256 ページ、64KB ページで約 16MB。
同じテーブル/インデックスをまとめて保存することで、コールドクエリの GET 回数を最小限に抑えます。
turbolite は マニフェストファイルを使用してページルックアップを間接化 し、各ページの在り処の真実の情報源とします。SQLite の暗黙の offset = page * size を明示的なポインターに置き換えます。古いページグループのバージョンは上書きされません。マニフェストの PUT がアトミックなコミットポイントです。古いバージョンはガベージになり、gc() によってクリーンアップされます。
SQLite はデフォルトで 4KB ページを使用し、ファイルシステムのディスクページサイズに合わせます。S3 ではディスクページサイズは無関係です。重要なのはリクエスト数を最小限に抑え、B-tree のファンアウトを最大化することです。答えは 大きなページ です。turbolite はデフォルトで 64KB ページを使用します。ページ数が少ない = リーフに到達するまでの S3 ラウンドトリップが少ない。
ポイントクエリを高速にするために、turbolite は シーク可能な圧縮 を使用します。各ページグループは複数の zstd フレーム(フレームあたり約 4 ページ)としてエンコードされます。マニフェストはフレームごとのバイトオフセットを保存するため、キャッシュミス時にはグループ全体ではなく、必要なページを含む約 256KB のサブチャンクだけを S3 のレンジ GET でフェッチします。
プリフェッチには プロアクティブ(クエリプランの先読み)と リアクティブ(適応的ミスベース)の 2 層があります。
クエリプランの先読み が最初に実行されます。クエリが実行される前に、turbolite は EXPLAIN QUERY PLAN を介して SQLite のクエリプランを傍受し、クエリが使用する正確なテーブルとインデックスを抽出し、最初のページが読み込まれる前にそれらのすべてのページグループをプリフェッチプールに投入します。5 つのテーブルを結合するクエリは、通常は 5 回の連続したミス→フェッチサイクルを引き起こしますが、その代わりにクエリ開始時に 5 つのフェッチすべてを並行して実行します。SCAN クエリの場合、テーブル全体が最初にプリフェッチされます。
注意点: SQLite は接続ごとに 1 つのトレースコールバックをサポートしています。別の拡張機能が最初にスロットを占有した場合、先読みはサイレントにリアクティブプリフェッチにフォールバックします。
リアクティブプリフェッチ は先読みで見逃されたものを処理し、フォールバックとして機能します。キャッシュミスが発生すると、次の 2 つのことが同時に発生します。
ミスカウンターは B-tree ごとに追跡され、グローバルではありません。users(ミス 1)と posts(ミス 1)にヒットするプロファイルクエリは、各ツリーを正しく 1 と追跡し、2 とはなりません。これにより、マルチテーブル結合が複数のツリーに触れるという理由だけで、誤ってすべてのツリーでプリフェッチをエスカレーションすることを防ぎます。
連続するミスごとに プリフェッチスケジュール が進行し、同じツリーのグループをどの程度プリフェッチするかを制御します。turbolite はクエリプランに基づいて自動的にスケジュールを選択します。
[0.3, 0.3, 0.4]: インデックスの未知の部分をスキャンする SEARCH ... USING INDEX クエリ用。最初のミスから積極的にプリフェッチします。インデックスのどの程度がスキャンされるかわからないためです。[0.0, 0.0, 0.0]: ツリーあたり 1-2 ページにヒットするポイントクエリとインデックスルックアップ用。プリフェッチする前に 3 回のフリーミスがあります。ゼロを多く含むスケジュールは、S3 Express と Tigris の両方で早期立ち上げよりも優れたパフォーマンスを発揮します。プリフェッチスケジュールは、TurboliteConfig の prefetch.search / prefetch.lookup を設定することでオープン時に調整できます。想定されるワークロードの形状を知っているため、VFS が推測する必要はありません。プリフェッチの設定 を参照してください。
両方のスケジュールは B-tree イントロスペクションを活用しています。プリフェッチされたすべてのグループは、正しいツリーのページを含むことが保証されています。例: SQLite が users テーブルからページを要求し、続いて同じテーブルから別のページを要求した場合、turbolite はスキャンが来ると想定し、users テーブルの残りの部分をバックグラウンドでプリフェッチし、それ以外はプリフェッチしません。B-tree イントロスペクションがないと、データがディスク上で隣接しているという理由だけで、users テーブルの半分と posts テーブルの半分を誤ってフェッチしてしまうでしょう。
インデックスリーフの先読み は、インデックス付き SEARCH でも同様に行います。インデックスリーフには、SQLite が次に要求するテーブル行 ID がすでにリストされています。そのため、turbolite はキャッシュされた内部ページを介してそれらを解決し、それらのテーブルフレームを 1 つずつではなく 1 つのバッチでプリフェッチし、リクエスト数を削減します。
turbolite は独自のインメモリページキャッシュを持ち、SQLite の組み込みページキャッシュを置き換えます。SQLite のページャーはページを内部的にキャッシュし、キャッシュされたページについては VFS から再読み込みしません。これは単一ライターデータベースでは問題ありませんが、読み取りレプリカ(HA フォロワー、マニフェストポーリングリーダー)の場合、SQLite のキャッシュは、複製を通じて基になるデータが変更されると古くなります。
turbolite のキャッシュは マニフェスト認識型 です。set_manifest() が発火すると(複製による新しいデータ)、影響を受けるページをディスクキャッシュとインメモリキャッシュの両方で無効にします。書き込みもインメモリキャッシュ内のページを無効にします。これにより、複製後または書き込み後の新鮮な読み取りが保証されます。
アーキテクチャ:``` 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` on `TurboliteConfig` (バイト). デフォルト: 64MB.
- `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ページ、約256KB)を独立して圧縮するため、ポイントルックアップではページグループ全体ではなく関連フレームのみを解凍します。カスタムzstd辞書を使用すると圧縮率をさらに向上させることができます。
ローカル(非S3)モードでもページレベルでzstd圧縮を行います。辞書トレーニングツールについてはCLIを参照してください。
#### 暗号化
暗号化が有効な場合、turboliteはS3オブジェクト、ローカルキャッシュ、WAL、メタデータを含むすべてを暗号化します。S3データはフレームごとにランダムなノンスを使用したAES-256-GCM(認証付き、改ざん検出)を使用します。ローカルデータはゼロサイズオーバーヘッドのAES-256-CTRを使用します。暗号化は圧縮後に行われます: `平文 → zstd → 暗号化 → S3`。
**キーローテーション:** `rotate_encryption_key(config, new_key)` は、解凍せずにS3上のすべてのデータを再暗号化、追加、または削除します。`Some` から `Some` はキーのローテーション、`Some` から `None` は暗号化の削除、`None` から `Some` は暗号化の追加を行います。クラッシュセーフ: 古いオブジェクトは決して上書きされず、マニフェストのアップロードがアトミックなコミットポイントとなり、コミット前に新しいデータが読み取り可能であることを確認する検証ステップがあります。部分的な実行による孤立オブジェクトは `gc()` によってクリーンアップされます。
## 強みと制限
### turboliteが高速な場面
**ポイントルックアップが最も得意です。** キャッシュレベル `index` では、ポイントルックアップはS3の範囲GET (~100KBずつ) で1〜2個のサブチャンクを取得します。内部ページとインデックスページはすでにキャッシュされています。キャッシュレベル `none` では、内部ページの再取得と最初のデータページのために約120msが追加されます。これはどのマシンサイズでも機能します。
**十分なコアでのスキャン。** プリフェッチプールは、ツリーごとの適応型スケジューリングでS3の帯域幅を飽和させます。検索クエリは最初のミスから積極的にプリフェッチを増加させます。プラン認識型のSCANクエリは、テーブル全体を事前に一括プリフェッチします。十分なスレッドがあれば、数秒で数GBのデータベースを2〜3回のプリフェッチバッチで同期できます。
### turboliteが低速な場面
**小規模マシンでのスキャン。** プリフェッチスレッドが1つの場合、1.46GBのスキャンには数秒ではなく数分かかります。ボトルネックはS3のラウンドトリップです。各ホップがグループを直列に取得します。最初のクエリが1vCPUマシンでのフルスキャンである場合、起動が困難になることが予想されます。
**スレッドチューニングの不良。** プリフェッチスレッドが少なすぎるとスキャンがS3の待機で停止します。多すぎるとフォアグラウンドのSQLite処理がダウンロードと競合し始めます。デフォルト(`max(num_cpus - 1, 1)`)はフォアグラウンド処理のために1コアを残しますが、大規模データベースでのスキャン主体のワークロードには十分なCPUが必要です。
**最初のクエリのペナルティ。** キャッシュレベル `none` での最初のクエリは、内部ページの読み込みに約50〜200ms、さらに少なくとも1回のデータ取得が必要です。バックグラウンドのプリフェッチが完了する前にクエリがインデックスページを必要とする場合、インラインの範囲GETにフォールバックします。
### 現在の制限
- **スタンドアロンのturboliteはシングルライターです。** 2台のマシンが同じプレフィックスに直接書き込むと、マニフェストが破損します。
- **HA/フェイルオーバーモードは実験的であり、`haqlite-turbolite` にあります。** このスタックはHaQLiteリース、turboliteページ階層化、walrustによる継続的WALレプリケーションを組み合わせています。これは直接的な複数ライターによる1つのturboliteプレフィックスへのアクセスではなく、マルチノードデプロイメントのための意図されたパスです。
- **WALの出荷は実験的です。** `wal` 機能フラグ + walrust が必要です。[耐久性](#durability)を参照してください。
**動作する** SQLiteの機能: FTS、R-tree、JSON、WALモード、DELETEジャーナルモード、VACUUM、オートバキューム。
## チューニング
### 一般パラメータ
| パラメータ | 制御内容 | デフォルト |
|-----------|----------|----------|
| `prefetch.threads` | 並列S3取得のためのワーカースレッド | max(num_cpus - 1, 1) |
| `cache.pages_per_group` | S3オブジェクトあたりのページ数、大きいほどPUTが少なく、1回あたりのバイト数が増加 | 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]` | 3つのフリーホップ、プリフェッチはゼロ。ポイントクエリはプリフェッチの恩恵をほとんど受けません。 |
各要素は、ツリーごとの連続キャッシュミスのN回目における、兄弟グループのプリフェッチ割合です。ミスが配列の長さを超えると、割合 = 1.0 (残りすべて)。
**なぜ2つのリアクティブスケジュールなのか?** SEARCHクエリはインデックス/テーブルの未知の部分をスキャンするため、積極的なウォームアップが必要です。Lookupはツリーあたり1〜2ページにヒットし、プリフェッチはほとんど必要ありません。ツリーごとのミスカウンターにより独立した追跡が保証されます: プロファイルクエリがユーザー (ミス1) にヒットし、次に投稿 (ミス1) にヒットする場合、各ツリーは個別に追跡されます。
### プリフェッチの設定
VFS構築時に `TurboliteConfig` で `prefetch.search` と `prefetch.lookup` を設定します:```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()
};
接続を再開せずにクエリごとに再調整するには、
turbolite_config_set SQL関数を使用します(Phase Cirrus c)。各プッシュは呼び出し側の接続ハンドルにスコープされ、再度変更するまで有効です:```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
### Index-leaf lookahead
クエリがインデックスを使用してテーブル行を見つける場合(`SEARCH ... USING INDEX`)、SQLiteが読み取るインデックスリーフは、フェッチしようとしているテーブルのrowidを既に指定しています。先読みはそれらのrowidを解析し、キャッシュされた内部ページを介してテーブルリーフフレームに解決し、フレームを1つのバッチでプリフェッチします。これにより、テーブル行が一度に1回のS3ラウンドトリップではなく、まとめて到着します。
これは**デフォルトで有効**であり、テーブルに追跡するインデックス付きの`SEARCH`に対してのみ機能します。スキャン、rowid単位の読み取り、完全にウォームなクエリは通常のパスをそのまま使用するため、これをオフにする理由はほとんどありません。クエリプランのプリフェッチ(`plan_aware`、デフォルトtrue)が必要です。
これを無効にする唯一のケースは、完全にウォームでCPUに敏感なワークロードです。この場合、各インデックスリーフの解析にわずかなコストがかかり、ページがすでにキャッシュされているためプリフェッチは行われません。```sql
SELECT turbolite_config_set('lookahead', 'false');
または、オープン時に TurboliteConfig の lookahead または環境変数 TURBOLITE_LOOKAHEAD を設定します。
Rust の呼び出し元は、turbolite::tiered::settings::set を介して同じパスを呼び出すことができます。
注記: プリフェッチは接続ごとに行われます。新しい接続はそれぞれ、ツリーごとのコールドミスカウンタから開始します。キャッシュは共有されるため、2番目の接続は最初の接続によってキャッシュされたページの恩恵を受けます。
最適なプリフェッチスケジュールは、S3バックエンドのレイテンシと帯域幅のトレードオフに依存します。我々は、S3 Express(GETに約4ms)とTigris(GETに約25ms)の両方で、6つのクエリに対して10のスケジュールペアをテストしました:
S3 Expressでは、off/off(プリフェッチなし)がポイントクエリに対して驚くほど競争力があります。これは各サブチャンク範囲GETが約4msしかかからないためです。「プリフェッチなし」と「最適なプリフェッチ」の差は小さく(ポイントルックアップで23%)、個々のGETが安価だからです。Tigrisでは、同じクエリがプリフェッチからはるかに大きな恩恵を受けます(idx-filterで最大39%)。これは無駄なラウンドトリップごとに25msかかるためです。
実際の効果: 高レイテンシバックエンドでは、検索スケジュールをより積極的にし、ルックアップスケジュールは先頭のゼロを多く保ちます。S3 Expressでは、デフォルトでうまく機能し、チューニングによる効果は小さくなります。両方のバックエンドでフルスキャンのパフォーマンスはスケジュールに影響を受けません。これはクエリプランの先行実行がテーブル全体を事前に一括プリフェッチするためです。
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
出力は、各スケジュールペアのp50、p90、GET数、バイト数を示すクエリごとの比較表(`tiered-bench --matrix`のようなもの)です。ツールはスケジュールを推奨し、それを適用するための`TurboliteConfig`の割り当てを出力します。
## 耐久性
turboliteはストレージ層であり、レプリケーションシステムではありません。耐久性はデータがS3に到達するタイミングに依存します。
**チェックポイント後**: ページグループとマニフェストはS3にあります。S3は耐久性11ナインを提供します。このデータはマシンの損失に耐えます。
**チェックポイント間**: 書き込みはローカルディスク上のローカルWALにのみ存在します。次のチェックポイントの前にマシンがダウンすると、それらの書き込みは失われます。
チェックポイント頻度がトレードオフを制御します。チェックポイントを頻繁に行うと、データのリスクウィンドウは小さくなりますが、S3 PUTが増えます。デフォルトはSQLiteの自動チェックポイント(1000 WALフレームごと)です。
### チェックポイントモード
turboliteは`TurboliteConfig`の`sync_mode`を介して2つのチェックポイントモードをサポートしています。
**`SyncMode::Durable`**(デフォルト)。チェックポイントはSQLiteのEXCLUSIVEロックを保持しながらページグループをS3にアップロードします。シンプルで、すべてのチェックポイントで完全に耐久性があります。アップロードが完了するまで、書き込みも読み取りも進行できません。ほとんどのワークロードに適しています。
**`SyncMode::LocalThenFlush`**。チェックポイントはローカルディスクキャッシュにのみ書き込み(ロック保持時間約1ms)、その後ロックを解放します。呼び出し元は`flush_to_s3()`を介して個別にS3にアップロードし、その間も読み取りと書き込みは通常通り続行されます。これは、S3アップロード中にリーダーをブロックすることが許容できない書き込み負荷の高いワークロードに役立ちます。
チェックポイントとフラッシュの間、データはローカルディスクキャッシュにのみ存在します。プロセスがクラッシュしても問題ありません(データはローカルディスク上にあり、ステージングログがアップロード用の正確なページ内容をキャプチャします)。フラッシュ前のマシン損失は、それらの書き込みが失われることを意味します。キャッシュの退避は安全です。turboliteは保留中のページを自動的に退避から保護します。
**クラッシュリカバリ**: プロセスがチェックポイントとフラッシュの間にクラッシュした場合、ステージングログはディスク上に残ります。次の`TurboliteVfs::new()`で、それらは自動的にリカバリされ、次の`flush_to_s3()`呼び出しのためにキューに入れられます。読み取りはフラッシュを待たずにローカルキャッシュから即座に提供されます。
### WAL shipping(実験的)
`wal`機能フラグを有効にすると、turboliteは[walrust](https://github.com/russellromney/walrust)を介してWALフレームをS3に送信し、個々の書き込みとチェックポイントの間の耐久性のギャップを埋めます。```toml
# Cargo.toml
turbolite = { version = "0.5", features = ["cloud", "zstd", "wal"] }
turboliteとwalrustは、`manifest.change_counter`として保存されたリプレイカーソルを介して同期を維持します。インポート/チェックポイントパスは、SQLiteのファイル変更カウンターからそのカーソルをシードします。ダイレクトページリプレイは、それを最新のコミット済み変更セットシーケンスに進めることができます。コールドスタート時、turboliteはページグループからデータベースを具体化し、次にwalrustはtxid > `change_counter`のWALセグメントをリプレイして、最後のチェックポイント以降に発生した書き込みを復旧します。
**WAL出荷による耐久性モデル**: コミットされた各トランザクションは、同期間隔(デフォルト100ms)内にWALセグメントとしてS3に出荷されます。マシンがダウンした場合、最大でも1同期間隔分の書き込みが失われます。チェックポイント後、txid <= `change_counter`のWALセグメントは自動的にガベージコレクションされます。
WAL出荷はSyncModeを補完します。SyncModeはチェックポイントがS3に到達する方法を制御し、WAL出荷はチェックポイント前に個々の書き込みを永続化します。
### 一貫性モデル
単一ライター、スナップショットリーダー。1つのプロセスが書き込みを行い、リーダーは開いた時点で最後にコミットされたマニフェストを参照します。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/main/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) は、キャッシュディレクトリを自分で管理したい組み込みユーザーのためにエクスポートされています。完全なHTTPサーバーの例については、
examples/go/ を参照してください。
SQLiteの load_extension を使用して、任意の言語でロード可能な拡張機能をビルドします:```bash
make ext # produces target/release/turbolite.{so,dylib}
INPUT:```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
ファイルファーストのユーザーストーリーでは、呼び出し元のapp.dbを所有するデータベースごとのVFSを登録します:```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/.
To configure the default `"turbolite"` VFS for file-first mode at extension
load time, set `TURBOLITE_DATABASE_PATH=/data/app.db` in the environment
before loading the extension. The sidecar is then `/data/app.db-turbolite/`
and the lower-level `TURBOLITE_CACHE_DIR` knob is ignored.
### Node.js```bash
npm install 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 ファイルをエクスポートするには(例えば `sqlite3` CLI で検査するため)、better-sqlite3 のバックアップ API を使用します: `await db.backup('export.sqlite')`。詳細なドキュメントは [turbolite-ffi/packages/node/](https://github.com/russellromney/turbolite/blob/main/turbolite-ffi/packages/node) を参照してください。
### Rust (ローカル, ファイル優先)```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() };
その場合、ローカルイメージは呼び出し元が指定する `app.db` ではなく、`/path/to/data/data.cache` になります。新しいエンベッダーはファイル優先形式を推奨します。
### Rust (S3 クラウド)```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 ファイル(例:sqlite3 CLI 用)には、SQLite オンラインバックアップ API またはバインディング固有のエクスポートヘルパー(Python の conn.iterdump()、Node の db.backup())を使用してください。
turbolite には、Rust を書かずに turbolite データベースを検査、管理、操作するための CLI が付属しています。```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-over-network の領域には多くのプロジェクトがあります。turbolite はそれらすべてからアイデアを借用しています。
最も一般的なアプローチ:変更されていない .db ファイルを S3 または CDN に配置し、SQLite がページを読み取るときに HTTP Range GET を発行します。
.dbi インデックスファイルを備えています(turbolite の内部ページバンドルと同じアイデア)。sqlite_zstd_vfs と組み合わせて使用するように設計されています。これらはすべて読み取り専用で、生ファイルから非圧縮ページをフェッチします。ポイントルックアップはリクエストごとに生の 4KB(または 64KB)ページを転送します。
これらはオブジェクトストレージを信頼できる情報源として扱い、個々のページまたは変更セットをレプリケートすることで、部分レプリカとオフラインファースト/エッジ展開を可能にします。
orbitinghail/graft): S3 上での遅延・部分・強一貫性レプリケーションのためのトランザクションストレージエンジン。libgraft SQLite 拡張は、Graft ボリュームを介して 4KB ページを読み書きする VFS を実装しています。フレーム化 zstd 圧縮とスプライターベースの変更セットを使用します。turbolite の「WAL フレームではなくページをレプリケートする」分野において、コールドリードレイテンシよりもマルチライターエッジ同期に焦点を当てた最も近いアーキテクチャ上の従兄弟です。これらはローカルの書き込みを S3 にレプリケートして、バックアップやリストアに使用します。
wa-sqlite 用の sqlite-s3vfs の TypeScript / ブラウザ移植版。同じ 1 オブジェクト 1 ページモデルを WASM / クライアントサイド向けに適合させています。すべてのベンチマークは benchmark/ にあります。デプロイシナリオ(ローカル、Fly.io、EC2)については benchmark/README.md を参照してください。
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` (ルックアップスケジュール), `--grouping` (位置指定またはbtree), `--queries` (投稿/プロフィール/いいねした人/相互), `--modes` (なし/内部/インデックス/データ), `--skip-verify` (小規模マシンでのCOUNT(*)をスキップ), `--iterations`, `--plan-aware` (先読みプリフェッチを有効化), `--matrix` (スイープスケジュールペア)。クエリごとのスケジュール: `--post-prefetch`/`--post-lookup`, `--profile-prefetch`/`--profile-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 データは、フレームごとに一意のランダム nonce を使用した AES-256-GCM(認証付き、改ざん検出)を使用します。ローカルファイルは、決定論的 nonce(ページ番号/バイトオフセット)を使用した AES-256-CTR を使用し、ディスク上の攻撃者に対する機密性を提供します。CTR の決定論的 nonce は、マルチスナップショット攻撃者が再利用されたオフセットで平文の XOR を回復できる可能性があることを意味し、これは SQLite 独自の SEE 拡張のトレードオフと一致します。ローカルキャッシュは一時的であり、S3 から再作成可能です。
## ライセンス
Apache-2.0
| Query | Type | Cold (S3 Express) | Cold (Tigris) |
|---|
| Post + user | ポイントルックアップ + 結合 | 86ms | 172ms |
| Profile | マルチテーブル結合 (5 JOINs) | 251ms | 479ms |
| Who-liked | インデックス検索 + 結合 | 206ms | 302ms |
| Mutual friends | マルチ検索結合 | 19ms | 49ms |
| Indexed filter | カバードインデックススキャン | 79ms | 88ms |
| Full scan + filter | フルテーブルスキャン | 476ms | 532ms |
| Cache level | What's cached | What's fetched from S3 | When this happens |
|---|
| none | なし | すべて | フレッシュスタート、空のキャッシュ |
| interior | 内部Bツリーページ | インデックス + データページ | 接続開始後の最初のクエリ |
| index | 内部 + インデックスページ | データページのみ | 通常のturbolite操作 |
| data | すべて | なし | ローカルSQLiteと同等 |
| Operation | SQLite | turbolite | Overhead |
|---|
| Point lookup | 145K/s | 73K/s | 2.0x |
| Range scan | 8.8K/s | 8.3K/s | parity |
| Full table scan | 56/s | 60/s | parity |
| INSERT | 19K/s | 23K/s | parity |
| UPDATE by PK | 40K/s | 27K/s | 1.5x |
| Batch INSERT (in txn) | 685K/s | 740K/s | parity |
| S3 の制約 | 影響 |
|---|
| ラウンドトリップは遅い | リクエスト数を最小限に抑える。書き込みはバッチ化、読み込みは積極的にプリフェッチ。 |
| 帯域幅がボトルネック | 帯域幅の利用率を最大化する。 |
| PUT と GET は操作ごとに課金 | 64KB の GET も 16MB の GET も同じコスト。バイト効率ではなくリクエスト数を最適化する。 |
| オブジェクトは不変 | 決してその場で更新しない。新しいバージョンを書き、ポインターを切り替える。部分書き込みによる破損はない。 |
| ストレージは安価 | 容量を最適化しない。余裕を持ってプロビジョニングし、古いバージョンを保持し、あとで GC に片付けさせる。 |
| ワークロード | 設定 | 理由 |
|---|
| 混合OLTP | デフォルト | プラン対応によりスキャンを処理、検索スケジュールがインデックスをウォームアップ、ルックアップスケジュールは控えめに保つ。 |
| ポイント主体(エージェントDB) | prefetch.lookup: vec![0.0, 0.0, 0.0] | ルックアップはほとんどプリフェッチを必要としません。 |
| スキャン主体の分析 | prefetch.search: vec![0.5, 0.5], prefetch.query_plan: true | 積極的な検索ウォームアップとプラン対応の一括プリフェッチ。 |
| 保守的(バースト的なサーバーレス) | 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 | 生ファイルレンジGET | Litestream VFS | sqlite_web_vfs + zstd_vfs | mvsqlite | Graft | sqlite-s3vfs |
|---|
| S3からの読み取り | 圧縮ページグループに対するシーク可能なレンジGET | 生ページに対するレンジGET | LTXファイルに対するレンジGET | 圧縮された外部DBに対するレンジGET | FoundationDBへのKVルックアップ | 4KBページ/変更セットの遅延フェッチ | ページごとに1回のGetObject |
| S3への書き込み | チェックポイント(グループごとに1回のPUT) | なし | なし | なし | あり(MVCC) | あり(非同期変更セットレプリケーション) | ページごとに1回のPUT |
| 圧縮 | シーク可能なマルチフレームzstd | なし | なし | zstd(ネストDB) | zstdデルタエンコーディング | フレーム化zstd | なし |
| 暗号化 | ページごとにAES-256-GCM | なし | なし | なし | なし | 記載なし | なし |
| プリフェッチ | 先読み + ホップスケジュール | なしまたは基本的な先読み | LRUキャッシュ | 適応型統合 | クライアントバッファ | 遅延/オンデマンド | なし |
| 内部ページ最適化 | 検出、固定、別途バンドル | なし | LTXトレーラからのページインデックス | オプションの.dbiファイル | なし | 記載なし | なし |
| ポイントルックアップあたりのバイト数(キャッシュ: インデックス) | ~100KB(1つの圧縮フレーム) | 4-64KB(1つの生ページ) | 変動 | 変動 | 変動 | 4KB(1ページ) | 4KB(1ページ) |
| 4096ページあたりの書き込みコスト | ~$0.000005(1回のPUT) | n/a | n/a | n/a | FoundationDB操作 | バッチ変更セット | ~$0.02(4096回のPUT) |