すべてのデータの同期をやめる
(sqlsync.dev)- Graft は、すべてのクライアントに完全な変更ログを送る代わりに、物理レプリケーションの単純さと論理レプリケーションの効率を組み合わせようとするオープンソースの トランザクションストレージエンジン
- 固定サイズの Page で構成された Volume を Snapshot 単位で扱い、サーバーは実データではなく、変更されたページインデックスの圧縮ビットセットである
graftを返す - クライアントは
graftを見て必要なページだけを取得し、Leap ベースのプリフェッチ、ドメイン別プリフェッチ、全変更分を取得する先行フェッチを選べる - オブジェクトストレージとエッジサーバーを活用し、ブラウザ、モバイルアプリ、サーバーレス関数、組み込み環境のような 制約のある環境 でも部分レプリケーションを目指す
- 一貫性モデルは Serializable Snapshot Isolation であり、古い Snapshot に基づくコミットは拒否し、クライアントが reset/replay、merge、Volume fork のいずれかで対処する
Graft が解決しようとするレプリケーションの問題
- 部分レプリケーションは必要なデータだけ同期すればよいので簡単に見えるが、実際の設計ではレプリケーション方式ごとに明確な代償がある
- 論理レプリケーション はすべての変更を精密に追跡するが、強い一貫性を複雑にする
- 物理レプリケーション はその複雑さを避けられる一方で、後で捨てる変更まで含めてすべて同期しなければならない
- Graft は、遅延同期、部分レプリケーション、強い一貫性、水平スケーラビリティ、オブジェクトストレージの耐久性を目標に作られたオープンソースのトランザクションストレージエンジン
- 出発点は SQLSync での経験
- SQLSync は SQLite 上に構築されたフロントエンド最適化データベーススタックで、Git と分散システムの考え方を同期エンジンに取り入れている
- SQLSync は完全な変更ログをすべてのクライアントに複製する構造のため、サーバーでは問題なくてもエッジやブラウザ環境には向いていない
- Graft の目標は、クライアントが望むペースで同期し、必要なものだけ取得し、エッジやオフライン機器でも任意のデータを強い一貫性でレプリケートできるようにすること
完全レプリケーションとスキーマ認識 diff の間にある設計
- 既存の解決策は大きく 2 つに分かれる
- 完全レプリケーション: データセット全体を各クライアントに同期するため、サーバーレス関数や Web アプリのような制約環境では実用的でない
- スキーマ認識 diff: CDC や CRDT のように行またはフィールド単位の論理変更を追跡するが、アプリケーションとの深い統合が必要で、任意データへ一般化しにくい
- Graft は完全レプリケーションのようにスキーマ非依存
- 保存されるデータの種類を知る必要も気にする必要もなく、バイトを含むページをレプリケートする
- 同時に、論理レプリケーションのように、最後の同期以降に何が変わったかを圧縮した説明をクライアントへ渡す
- 中核となる抽象化は Volume
- Volume は固定サイズ Page の疎で整列されたコレクション
- クライアントは特定の Snapshot に対してトランザクション API で Volume を読み書きする
- 内部的には Graft が必要なものだけを保存・複製し、耐久性と拡張性を備えたバックエンドとしてオブジェクトストレージを使う
遅延同期: クライアントが望むタイミングで追いつく
- Graft は、エッジクライアントがたまにしか起動せず、ネットワークが不安定で、実行時間も短い環境を前提に設計されている
- 継続的なレプリケーションに依存せず、クライアントが いつ同期するか を自分で選ぶ
- 同期は「最後の Snapshot 以降に何が変わったのか」という問いから始まる
- サーバーは実データを送るのではなく、変更されたページインデックスの圧縮ビットセットである
graftを返すgraftは既存 Snapshot に新しい変更を継ぎ足すためのガイドとして機能する- クライアントはどのページを再利用でき、どのページを必要時に取得すべきかを把握できる
graftはデータそのものではなく変更メタデータなので、何をいつ取得するかの主導権はクライアントに残る
部分レプリケーションとプリフェッチ
- ブラウザタブ、モバイルアプリ、サーバーレス関数では、一部のクエリを処理するためにデータセット全体をダウンロードするのは難しい
- クライアントは
graftを受け取った後、どのページがまだ有効で、どのページを取得すべきかを判断する - 必要なページだけを選択的に取得するため、実際に使うデータだけをレプリケートできる
- Graft はページアクセス遅延を減らすため、複数のプリフェッチ方式をサポートする
- 汎用プリフェッチ: Leap アルゴリズムベースの組み込みプリフェッチャーがアクセスパターンを識別し、今後のページアクセスを予測する
- ドメイン別プリフェッチ: アプリケーションがユーザープロフィールのようによく参照されるデータについての知識を活用し、関連ページを先回りして取得できる
- 先行フェッチ: 必要であればすべての変更分を取得して、事実上完全レプリケーションに戻すこともでき、特にサーバー側 Graft ワークロードで有用
- ページはオブジェクトストレージ上に直接ホストされるため、耐久性と拡張性を備えたレプリケーション基盤として機能する
エッジ配置と組み込みクライアント
- Graft のエッジレプリケーションは、どのデータを同期するかだけでなく、必要な場所にデータを置くことまで目標にしている
- ページはオブジェクトストレージからグローバルなエッジサーバーフリートを通じて配信される
- 頻繁にアクセスされる hot page はクライアントの近くにキャッシュできる
- 世界中のどのユーザー位置でも低遅延と高応答性を目指す
- Graft クライアントは軽量で組み込みやすいよう設計されている
- 依存関係が少なく、ランタイムも小さい
- ブラウザ、デバイス、モバイルアプリ、サーバーレス関数のような環境に統合できる
- エッジキャッシュは一貫性や競合処理の問題を生むため、Graft は強い一貫性モデルも提供する
一貫性モデルと競合処理
- Graft は Serializable Snapshot Isolation を一貫性モデルとして採用する
- クライアントは特定の Snapshot において分離され一貫したデータビューを得られ、読み取りは互いに干渉せず同時に進められる
- 書き込みは厳密に直列化され、すべてのトランザクションに対してグローバルに一貫した順序が生まれる
- オフラインファーストと遅延レプリケーションの特性上、クライアントが古い Snapshot を基にコミットを試みることがある
- そのようなコミットを無条件に受け入れると strict serializability が壊れる
- Graft はそのコミットを安全に拒否し、クライアントに対処方法を選ばせる
- クライアントの一般的な選択肢は 3 つある
- Reset and replay: 最新 Snapshot を取得し、ローカルトランザクションを再適用したうえで再試行する
- グローバルデータは strict serializable な状態を維持する
- ローカルでは Optimistic Snapshot Isolation を体験し、読み取りは内部的に一貫した Snapshot を見るが、コミット拒否時にはその Snapshot が破棄されうる
- Merge: ローカル状態をサーバーの最新 Snapshot とマージする
- この場合、グローバル一貫性モデルは snapshot isolation まで下がる可能性がある
- Volume fork: 恒久的に新しい Volume を作成して分岐する
- グローバル serializability を維持する
- Reset and replay: 最新 Snapshot を取得し、ローカルトランザクションを再適用したうえで再試行する
作れるアプリケーション
- オフラインファーストアプリ: ノート、タスク管理、CRUD アプリのように部分的にオフラインで動作するアプリで、Graft が同期を担える
- 競合ハンドラーと組み合わせれば、任意データ上でマルチプレイヤー機能も可能
- クロスプラットフォームデータ: モバイルプラットフォーム、デバイス、Web でデータを共有し、ベンダーロックインを減らせる
- ステートレスな読み取りレプリカ: ローカル状態なしでデータベースレプリカを立ち上げ、最新 Snapshot メタデータを取得してすぐにクエリを実行できる
- 全データをダウンロードしたりログを再生したりする必要がない
- 任意データのレプリケーション: Graft はページレプリケーションに集中しているため、ページ内部のデータ形式には関与しない
- SQLite データベース、AI モデル、Parquet、Lance ファイル、Geospatial tilesets のようなデータを対象にできる
SQLite 拡張 libgraft
- 現在 Graft を使う最も簡単な方法は、ネイティブ SQLite 拡張の
libgraft libgraftは SQLite が動作する場所ならどこでも使え、クライアントが実際に利用するデータベースの一部だけをレプリケートする- SQLite の VFS を実装し、データベースの読み書きを横取りする
- SQLite が WAL mode で提供するものと同じトランザクションおよび並行性セマンティクスを提供する
- 提供機能は次のとおり
- オブジェクトストレージとの 非同期レプリケーション
- エッジやデバイスでの遅延部分レプリケーション
- Serializable Snapshot Isolation
- 特定時点への復元
- ドキュメントは GitHub の SQLite ドキュメント で見られる
参加とマネージドサービス計画
- Graft は GitHub で公開開発されている
- Issue、Discussion、Pull Request を受け付けており、contribution guide も用意されている
- 連絡チャネルとして Discord とメールが提供されている
- Graft Managed Service の提供も計画されており、ウェイトリスト登録リンクも用意されている
ロードマップ
- Graft は 1 年にわたる研究、複数回の反復、大きな方向転換を 1 度経てきたが、まだ多くの作業が残っている
- 計画されている項目は次のとおり
- WebAssembly 対応: ブラウザで Graft を使えるようにし、SQLite 公式 Wasm ビルド、wa-sqlite、sql.js のサポートを目指す
- Graft と SQLSync の統合: Wasm 対応後、SQLSync の mutation、rebase、query subscription レイヤーを分離し、Graft が複製するデータベース上に載せる計画
- クライアントライブラリの拡充: Python、JavaScript、Go、Java 向けのネイティブ Graft クライアントラッパーを用意したい
- 低遅延書き込み: 現在 push 処理はオブジェクトストレージに完全にコミットされるまでブロックされる
- S3 express zone の実験
- オブジェクトストレージの前段に低遅延・耐久性のある合意グループを置く方式
- ガベージコレクション、チェックポイント、コンパクション: クエリ性能の最大化、無駄領域の最小化、完全削除のために必要
- 認証と認可: マネージドサービスのアカウントから Volume の細かな読み書き権限までを含む広範な作業
- Volume forking: サービス側では Segment 参照を新しい Volume にコピーして zero-copy fork を行えるが、ローカル fork では現在すべてのページをコピーしなければならない
- 競合処理: 組み込みの競合解決戦略と拡張ポイントを提供する予定で、初期戦略は重ならないトランザクションを自動マージすること
SQLite レプリケーション解決策との比較
- 比較情報はドキュメントとブログ記事から集めたもので、完全に正確ではない可能性があるという断り書きがある
-
mvSQLite
- mvSQLite は SQLite ページを FoundationDB に直接保存するカスタム VFS 層を実装している
- Graft と mvSQLite は、ページレベルのバージョン管理によって遅延フェッチと部分データベースビューを可能にする点が似ている
- 違いは保存先とページ変更追跡の方式
- mvSQLite は FoundationDB に依存し、すべてのノードがクラスタへ直接アクセスする必要がある
- Graft の Splinter ベースの changeset は自己完結型で配布しやすく、変更ページのバージョンを知るために FoundationDB へ直接問い合わせる必要がない
-
Litestream
- Litestream は SQLite WAL frame をオブジェクトストレージへ継続的に複製するストリーミングバックアップソリューション
- Graft はカスタム VFS により SQLite のコミット過程へ直接統合され、遅延部分レプリケーションと分散書き込みを可能にする
- 両者ともページをオブジェクトストレージに複製し、特定時点への復元をサポートする
-
cr-sqlite
- cr-sqlite はテーブルを CRDT に変換し、論理的な行レベルレプリケーションを可能にする SQLite 拡張
- 自動競合解決を提供するが、スキーマ認識とアプリケーションレベルの統合が必要
- Graft はスキーマ非依存で、任意の SQLite 拡張やカスタムデータ構造と互換性がある一方、グローバル serializability のためにアプリケーションが競合解決を明示的に扱う必要がある
-
Cloudflare Durable Objects with SQLite Storage
- Durable Objects と SQLite を組み合わせると、ビジネスロジックで包んだ強い一貫性と高い耐久性を持つデータベースを Cloudflare エッジネットワークに置ける
- 内部的に SQLite WAL をオブジェクトストレージへ複製し、定期的にチェックポイントする点で Litestream に似ている
- Graft はレプリケーションを第一級機能として公開し、エッジとの効率的なレプリケーションを目指している
-
Cloudflare D1
- Cloudflare D1 は HTTP API 経由でアクセスするマネージド SQLite データベース
- Graft はデータをクライアントアプリケーションに埋め込み、エッジへ直接レプリケートする分散モデル
-
Turso & libSQL
-
rqlite & dqlite
-
Verneuil
- Verneuil はオブジェクトストレージを通じて SQLite Snapshot を読み取りレプリカへ非同期に複製し、信頼性を優先する
- レプリケーション遅延や鮮度を最小化する仕組みは明示的に避けている
- Graft はマルチライター分散データベースにより近く、選択的なリアルタイム部分レプリケーションを強調する
まだコメントはありません。