分散SQLiteの5つの誤解を12年の歴史で検証
Raft・Litestream・Turso・D1など12年の分散SQLiteを振り返り、整合性やレプリケーション、Postgresを選ぶべき時を検証します。

分散SQLiteは、ある個人の週末プロジェクトから、ベンチャー資金の入る製品カテゴリーへと成長しましたが、その間の10年をほとんどのエンジニアは意識しないまま過ぎました。2014年9月、Philip O'Toole氏はSQLiteの前にRaftログを置き、クラスタ全体で全書き込みをレプリケートして、そのコードを「rqlite」としてGitHubに公開しました。それから12年、dqlite、Litestream、LiteFS、cr-sqlite、libSQL/Turso、Cloudflare D1と、エコシステム全体が出来上がっています。そして、この分野について人々が信じていることの多くは、興味深い形で間違っています。
私はクラスタ化されたデータベースを本番環境で運用し、その障害モードの後始末もしてきました。最初に言っておくと、分散SQLiteをめぐる議論はほとんどSQLiteの話ではありません。問われているのは「マシンが死んだらどうするか」「マシンが死んで、復旧を待てなかったらどうするか」「そもそもマシンが存在しない場合はどうするか」という、どの問いを立てているのかという点です。ここでは、12年分の歴史に照らして5つの誤解を検証します。
誤解1:分散SQLiteは最近のアイデアだ
レプリケートされたSQLiteに関する最近の記事の波は2021年頃に始まりました。Litestreamや「SQLiteだけで十分」という動きが注目を集め、TursoやCloudflare D1も資金調達やローンチ記事とともに登場しました。そのため、賢い誰かが最近になってSQLiteを複製できると気づいたのだ、という思い込みが広がりました。実際には、Raftを使ったSQLiteの最初のシステムは2014年に公開されています。しかも、SQLiteのレプリケーションが初めて行われたわけでもありません。Expensifyは何年も前からPaxos系のプロトコルで社内向けにこれを実現しており、2016年にBedrockとして公開しました。ActorDBも同じ時期に、Erlangのアクターごとに独自のRaftでレプリケートされたSQLiteデータベースを持たせていました。
功績の話だけではありません。12年間、独立した再発明に耐えてきた設計は、それ自体が何かを物語っています。少なくとも8つの異なるチームが、異なる要件とプロトコルを持ちながら、同じアーキテクチャにたどり着きました。ストレージエンジンとしてのSQLiteに、外側からレプリケーションを組み込むというものです。やめる理由を見つけた人はいませんでした。10年にわたってこうした収束した再実装が見られるとき、それは流行のサイクルではなく、本当に良いアイデアを見ているのです。現れては消えるカテゴリーと比べると、これは積み重なり続けてきました。
古い設計判断が今もなお重要な役割を担っている、という点も大事です。rqliteは2014年の時点でCPシステムを選びました。書き込みはリーダーを通り、過半数の確認が必要で、分断された少数派は書き込みを拒否します。そして一度もその判断を変えていません。この一つの選択が、HTTP API、読み取り整合性のレベル、スナップショットの挙動まで決めています。整合性を後で決めればよい設定項目として扱うチームは、たいてい後悔します。想定していた仕様と実際に実装した仕様のギャップこそ、分散システムのバグが生まれる温床だからです。
誤解2:SQLiteにRaftを載せれば、安価な分散Postgresが手に入る
この誤解は、リストの中でも最も高くつくものです。「世界で最も使われているデータベースにコンセンサスを足せば、クライアント・サーバー型システムの運用負荷を避けられる」という売り文句は魅力的に聞こえます。しかし実際に手に入るのは、すべての書き込みが過半数のノードへのネットワーク往復を伴う、シングルライターのデータベースです。しかもSQLiteのSQLの表面、つまりストアドプロシージャがなく、型システムも薄く、並行性のプリミティブも限られた状態は、そのまま残ります。
書き込みのパスこそが、正直なコストです。ローカルのSQLiteのコミットは、数回のfsyncで済みます。一方、Raftでレプリケートされたコミットは、リーダーがログに追記し、エントリをフォロワーに送り、過半数が永続化するのを待ち、SQLiteに適用してから応答する、という流れになります。健全なLAN上であれば、マイクロ秒単位ではなく1〜2ミリ秒程度になるでしょう。リージョンをまたぐと、光の速度とディスクの遅延が立ちはだかります。設定ストア、サービスディスカバリのメタデータ、小規模なコントロールプレーンのように、読み取りが多く書き込み頻度の低いワークロードであれば問題ありません。実はそれこそ、rqliteが想定していたニッチです(その起源は、どのノードに持たせるべきか適切な答えがないまま、少量のリレーショナルストレージを必要としていたクラスタ化されたシステムにあります)。ホットなOLTPワークロードには、明らかに不向きです。
そして、READMEには誰も書かない運用上の現実があります。リーダー選出は、あなたが自ら抱える障害モードになるのです。スプリットブレインは過半数投票で防げますが、それは3ノードのクラスタが2台を失うと、すべての書き込みを拒否することを意味します。「サーバーが死んだのでDBが落ちた」を「クォーラムが死んだのでDBが落ちた」に置き換えただけで、後者は深夜3時に当番エンジニアへ説明するのがずっと難しいものです。選んだシステムに対してJepsen式のテストを考えたことがないなら、ぜひ考えてください。あるHacker Newsのコメントでは、rqliteがJepsenでテストされたことがあるのかという鋭い質問が出ていました。テストされていないシステムには、たいてい何らかの驚きが潜んでいるものです。一方で、同じ議論のなかで経験豊富なエンジニアが、最近のツールのおかげで実際のPostgresを動かすのがずいぶん簡単になり、「とりあえずデータベースサーバーを使う」が再び正当なデフォルトになったと指摘していました。彼の指摘は的を射ています。

誤解3:レプリケーションはイコール・コンセンサスである
12年分の歴史が本当に与えてくれる洞察が一つあるとすれば、それはこれです。「レプリケートされたSQLite」は一つの設計ではなく、設計空間における三つの角であり、それらは示唆に富む順序で登場しました。これらを混同するからこそ、バックアップの問題を解決するためにRaftクラスタを運用するという事態が起きるのです。
- コンセンサス(CP)。rqlite、dqlite。リーダーは一つ、強い整合性、本物のクラスタ。答えるのは「マシンが死んで、復旧を待てなかったらどうするか」。コストは書き込みのレイテンシとクォーラム運用。
- ストリーミング(シングルライター)。Litestream、のちにLiteFS。コンセンサスは一切使いません。SQLiteのWALをオブジェクトストレージにストリーミングし、単一ノードに対してほぼ継続的なディザスタリカバリを実現します。答えるのは「マシンが死んだらどうするか」。コストは、失われる可能性のある小さなデータの窓と、数秒から数分かかる復旧時間。
- マージ(AP)。cr-sqlite、そしてローカルファースト運動。SQLite拡張としてのCRDT。リーダーなしに複数のライターが分岐し、マージします。答えるのは「そもそも中央のマシンがない場合はどうするか」。コストは結果整合性と、設計時に考慮すべきマージのセマンティクス。
Ben Johnson氏のLitestreamの主張は、多くのアプリケーションについてはたしかに正しかったので、評価に値します。それらのアプリは一台のマシンで動いており、必要なのはクラスタではなく、マシンが死んだときに復旧する手段だからです。最初の問い「マシンが死んだらどうするか」は、二つ目の問い「復旧を待てなかったらどうするか」よりもはるかに一般的です。私は、RTO要件が「昼食前くらいまで」のデータベースを守るために、3ノードのRaftクラスタを展開するチームを何度も見てきました。それは信頼性エンジニアリングではなく、カーゴカルトです。そして面白いことに、LitestreamのWALストリーミングという手法は、v8のrqlite自身がWALを扱う方法にも影響を与えました。ストリーミングの角がコンセンサスの角へとフィードバックされたのです。
ですから、ツールを選ぶ前に、問いを書き出してください。文字通りに。「ノードXが死んだら、N秒以内に書き込みを再開する必要があり、直近M秒分のコミット済みデータを失うことは許容する」というように。Mが数秒でNが1分程度なら、Litestream-to-S3が答えになり、クォーラムのことで夜中に呼び出されることもありません。Mがゼロで、Nが数秒なら、あなたはコンセンサスの領域にいるのであり、すべての書き込みでその代償を払うことになります。これは、どんな脅威モデルにも通じる規律です。防御を買う前に障害に名前をつけ、どの破局に抗うのかを事前に決めておく。破局に耐える仕組みに取り組むときと同じです。
誤解4:マネージドな「エッジのSQLite」はトレードオフを消してくれる
この分野のお金は、プラットフォームの側に流れました。TursoはVC資金を集めた後、Supabaseに買収されました。CloudflareはD1をエッジネットワークの中に出荷しました。売り文句は、難しい分散の部分は誰かが代わりに引き受けてくれる、というものです。それが本当に当てはまることもあります。しかし、ときにはただのマーケティングで、そのうち不都合な実態が漏れ出てきます。あるCloudflareの開発者は、D1が本質的には、そのSQLiteデータベースを公開しているシングルトンのDurable Objectのラッパーにすぎず、多くの場合はDurable Objectsのストレージを直接使ったほうがよいと公に認めました。これはコンセンサスの意味での分散データベースではなく、プラットフォームで包まれた一つのSQLiteファイルにすぎません。本番でD1を使っているユーザーからも、それに見合った信頼性の報告が上がっています。
だからといって、マネージドなオプションが悪いわけではありません。トレードオフが消えたわけではなく、中身を確認できないAPIの向こう側に移動しただけです。rqliteを自分で運用すれば、CPが何を意味するかは正確にわかります。3ノード、リーダー一台、過半数のACK、分断された少数派は書き込みを拒否する。一方で「エッジのSQLite」を買う場合、ブラックボックスに対して同じ質問を投げる必要があります。書き込み先はどこか、分断が起きたらどうなるか、実際の耐久性の窓はどれくらいか。ベンダーの答えが曖昧なら、インシデントレビューも曖昧になるでしょう。
レプリケーションをサービスとして提供されても、整合性の問題は消えません。答えが、監査できない他人のインフラに移されるだけです。
歴史的なパターンは、しっかり理解しておく価値があります。オープンソースのシステム(rqlite、dqlite、Litestream)が、誰かがそれを売る前の8年間にアイデアを形にしてきました。そして資金は、そのアイデアを請求書の付くサービスに変えた者へと流れました。これはインフラにありがちな運命です。買うことが正しい判断であることも多いのですが、三つの角のうちどれを実際に手にしているのかを、目を開いて理解したうえで買ってください。

誤解5:強い整合性はバックアップに対する無料のアップグレードである
人々は、選択肢を、バックアップ(弱いが安価)対コンセンサス(強いが高価だが厳密に上位)として捉えがちです。しかしそれは正しくありません。Raftによる強い整合性は、同じ製品のより良いバージョンではなく、別の製品です。しかも、バックアップでは決して課されない継続的な税を課してきます。
一つ目の税はスループットです。両方を使った一人の実務者は、rqliteは「複数箇所に保存される」保証を与えてくれるが、単一ノードのデータベースよりずっと遅いと指摘しました。それがコンセンサスの往復であり、それは決してなくなりません。二つ目の税は、微妙なセマンティクスです。SQL文を送信してレプリケートすると、決定性の問題を抱えることになります。random()やdatetime('now')のような関数は、レイヤーが書き換えない限りノード間で食い違う可能性があります。そのため、一部の実装ではSQLiteのSQLのサブセットしかサポートしていないのです。一方、WALレベルでレプリケートすればその問題は避けられますが、SQLiteのディスク上のフォーマットやページレイアウトに結合することになります。どの層に置いても、妥協なしにはなりません。三つ目の税は、分散トランザクションが本当に難しいということです。あるエンジニアは、トランザクション内の各文を個別のRaftコマンドとして扱う設計(正しくはあるが遅い)から、開始ノードがトランザクション全体をローカルで実行し、ロールバックし、バッチ全体を一つのコミットコマンドとして送る設計へと移行した経緯を説明していました。これは、実際にこうしたシステムを運用してようやく学べる、苦労して得た教訓です。
rqliteのHTTP APIを使って、書き込みパスの違いを示す最小限の例を挙げます。
# Local SQLite: one process, one fsync, microseconds.
sqlite3 app.db "INSERT INTO kv VALUES ('flag', 'on')"
# rqlite: this HTTP call hides a Raft round trip —
# leader log append, replication to a majority, then apply.
curl -XPOST 'localhost:4001/db/execute?pretty' \
-H 'Content-Type: application/json' \
-d '["INSERT INTO kv VALUES (?, ?)", "flag", "on"]'
# The guarantee changed: the write now survives the loss
# of any single node. So did the latency budget.
そして、得られなかったものにも注目してください。読み取りのスケールです。Raftはログをレプリケートするので、フォロワーがリーダーの役割を引き継げますが、デフォルトでフォロワーが強い整合性を持つ読み取りを返すようにはなりません。だからこそrqliteは読み取り整合性のレベルを提供しているのです。これもまたトレードオフだからです。「分散」に読み取り容量の増加を期待して来たのであれば、必要だったのはコンセンサスではなく読み取りレプリカでした。
SQLite上のRaftの12年から実際に学べること
このカテゴリーの歴史は、インフラのアイデアがどう成熟するかについて、珍しいほど明快な証拠を示しています。まず、具体的な必要性のために、原則に則ったものを誰かが作ります(rqliteは、ある職場で実際に直面したクラスタリングの問題から生まれました)。次に、他の人々がパッケージングの制約に合わせて作り直します。CanonicalのdqliteはCanonicalが自社製品の内部にデータベースを必要としたため組み込むライブラリであり、rqliteはどんな製品の横でも動かすサーバーです。その後、一般的なケースは原則的な解決策が想定するより小さいのだと主張する人が現れ(Litestream)、その正しさが証明され、分野全体が再調整されます。最後に、プラットフォームがそれを包んで販売します。インフラの仕事を長く続けていれば、この映画をまた何度も見ることになるでしょう。今まさに、LLMエージェントも同じように分散システムの教訓を学び直しているところです。
すべてを踏まえた、飾り気のない私の推奨です。まずは、単一ノードのSQLiteとWALのオブジェクトストレージへのストリーミングから始めてください。それだけでどこまでいけるか、驚くはずです。そして、あなたの障害対応は復旧作業になり、コンセンサスのポストモーテムにはなりません。ストリーミングでは満たせない可用性の要件を一文で言えるようになったときに初めて、Raftベースのシステムへ移行してください。そのときには、書き込みレイテンシとクォーラム運用のコストも受け入れてください。Postgresの SQL の表面や、マルチライターのスループットが必要になった時点で、すぐにPostgresに手を伸ばしてください。どれも合わないのであれば、組み込みデータベースの世界全体にも選択肢があります。組み込みグラフデータベースから、ローカルファーストのCRDTの領域まで。
誤解を解いてまとめると、分散SQLiteは新しいものではなく、安価なPostgresでもなく、一つの設計でもなく、マネージドなラッパーによって安全になるものでもなく、バックアップに対する無料のアップグレードでもありません。それは12年の歴史を持つ、よく地図化された設計空間であり、三つの角があります。どの角も、異なる問いに対しては正しいのです。痛い目を見るエンジニアは、問いを書き出したことがない人たちです。


