
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に貢献したい場合やバグを見つけた場合は、プルリクエストを作成するか、イシューを開いてください。
| 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 |
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):
| Cache level | What's cached | What's fetched from S3 | When this happens |
|---|---|---|---|
| none | なし | すべて | フレッシュスタート、空のキャッシュ |
| interior | 内部Bツリーページ | インデックス + データページ | 接続開始後の最初のクエリ |
| index | 内部 + インデックスページ | データページのみ | 通常のturbolite操作 |
| data | すべて | なし | ローカルSQLiteと同等 |
interior は最も現実的なコールドベンチマークです: 内部ページは接続開始時に先読みされ、最初のクエリを実行する頃にはキャッシュされています。インデックスページは最初のアクセス時にバックグラウンドで積極的にプリフェッチされますが、まだ準備ができていない可能性があります。
100K rows, Fly.io performance-2x (dedicated vCPU, NVMe, IAD):
| 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 |
ポイントルックアップはページあたりのオーバーヘッドが最も高くなります(約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 の制約を前提に設計されています。すべての判断はこのモデルから導かれます。
| S3 の制約 | 影響 |
|---|---|
| ラウンドトリップは遅い | リクエスト数を最小限に抑える。書き込みはバッチ化、読み込みは積極的にプリフェッチ。 |
| 帯域幅がボトルネック | 帯域幅の利用率を最大化する。 |
| PUT と GET は操作ごとに課金 | 64KB の GET も 16MB の GET も同じコスト。バイト効率ではなくリクエスト数を最適化する。 |
| オブジェクトは不変 | 決してその場で更新しない。新しいバージョンを書き、ポインターを切り替える。部分書き込みによる破損はない。 |
| ストレージは安価 | 容量を最適化しない。余裕を持ってプロビジョニングし、古いバージョンを保持し、あとで GC に片付けさせる。 |
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 の両方で早期立ち上げよりも優れたパフォーマンスを発揮します。