8月12日、Tailscaleが「How Tailscale helped find the SQLite WAL-Reset bug」と題した記事を公開した。この記事では、TailscaleのコントロールプレーンでSQLiteのWALリセットバグが引き起こした断続的な障害と、その原因究明・修正に至るまでの6ヶ月間の経緯が詳しく紹介されている。
「枯れた技術」の代名詞ともいえるSQLiteで、コミット済みのデータがエラーなしにサイレントに消える——そんなバグが実在した。しかも、世界規模のVPNサービスを支えるTailscaleのコントロールプレーンで6ヶ月にわたり19回の破損を引き起こすまで、誰も正体を掴めなかった。再現不能、ログにも痕跡なし。その調査過程は、デバッグの教科書として読める内容だ。
SQLite本番運用という選択と、突然の破損
Tailscaleはコントロールプレーンの内部設計としてSQLiteを採用している。「大規模サービスにSQLiteは使えない」というイメージに反して、シャード分割と厳格なシングルライター構成を組み合わせることで、シンプルかつ信頼性の高いアーキテクチャを実現するアプローチだ。近年エンジニアリングブログ界隈でも注目を集めており、Tailscaleはその代表的な実践例の一つとして知られる。
2024年8月、S3バックアップを読むデータパイプラインが1つのシャードのデータベースでエラーを検知した。PRAGMA integrity_checkを実行すると、バックアップが破損していることが判明した。SQLiteの破損は理論上起こりうるとはいえ、正常な運用では滅多に発生しない。修復して調査したが、原因は特定できなかった。
その後、同様の破損が何度も繰り返された。6ヶ月間で合計19回。破損のたびにシャード上のコントロールプレーンを停止して修復・復旧する必要があり、当初は1回あたり1時間以上のダウンタイムが生じた。特定のシャードや顧客、機能、時間帯、負荷レベルと紐づかず、10月から12月にかけては6週間まったく発生しない期間もあった。再現条件が不明なまま、本番環境でのフォレンジック調査に頼るしかない状況が続いた。
「消えた書き込み」を可視化したトランザクションログ
根本原因の調査と並行して、Tailscaleは復旧プロセスの改善も進めた。その一環として構築したトランザクションログパイプラインが、決定的な手がかりをもたらした。
データベースを変更するSQLステートメントをすべて別のログファイルにストリーミングし、既知の正常なバックアップに対してそのトランザクションを再生することで、データロスを最小化しつつ復旧できる仕組みだ。SQLiteはシングルライター・シリアライザブルトランザクションのデータベースであるため、このトランザクション履歴は完全に線形かつ決定論的になる(PostgreSQLやMySQLのようなマルチライターDBでは成立しない手法だ)。
ところが、2件のインシデントでこのトランザクションログの再生が失敗した。調査すると、あるトランザクションがコミットした書き込みが、後続のトランザクションからまったく見えなくなっていた。エラーも出さずにデータが消える——本来あり得ない挙動だ。これがバグの核心に迫る突破口となった。
WALとチェックポイント——バグが潜んでいた場所
ここでSQLiteの内部構造を押さえておく必要がある。
Tailscaleはパフォーマンスと並行性のため、SQLiteをWAL(Write-Ahead Logging)モードで運用している。WALモードでは、更新されたページは直接データベースファイルに書かれず、まず「WALファイル」に書き込まれる。一定のタイミングでWALファイルの内容をメインのデータベースファイルに書き戻す処理がチェックポイントだ(詳細はSQLite公式のWAL解説を参照)。
Tailscaleでは高速・一貫なバックアップを取るために、このチェックポイント処理を自前で制御していた。破損インシデント時のメトリクスからは「WALファイルに実際にあるページ数より多くのページをコピーしたと報告される」という異常も観測されており、この独自のチェックポイント制御が怪しいと目を付けた段階だった。
デバッグ用シムtmstmpvfsの投入
Tailscaleはこの問題の解析にあたり、SQLiteのプロフェッショナルサポート契約を活用してSQLite開発チームと直接技術的な議論を行った。深い内部挙動の解析には、コアチームとの密な連携が不可欠だったという。
SQLiteの開発チームはこの問題に対処するため、仮想ファイルシステム層をラップするデバッグ用シム「tmstmpvfs」を新たに開発した(ソースコードはSQLiteの公開リポジトリで確認できる)。このシムはデータベースへの変更に関するトレース情報を追加でログに書き出す仕組みで、通常のSQLite VFS(仮想ファイルシステム)の上に薄く重ねることで既存コードへの変更を最小限に抑えながら詳細な計装が可能になる。
TailscaleはこのシムをLIVE環境に組み込んで待機した。次のインシデントはすぐに発生した。
バグの正体——チェックポイントと書き込みの稀なデータレース
追加ログを解析した結果、SQLite開発チームはついにバグを特定した。チェックポイントと書き込みトランザクションの間に生じる稀なデータレースだ。
チェックポイントの特定タイミングで書き込みが発生すると、チェックポイント処理がWALファイルの状態を誤認し、まだデータベースに書き戻されていないページを「書き戻し済み」と判断してWALをリセットしてしまう。これにより、コミット済みのデータがサイレントに失われ、その後の読み取りから見えなくなる——これが「WALリセットバグ」の正体だ。発生頻度が極めて低く、かつエラーを返さずに完了するため、6ヶ月間誰も正体を掴めなかったのは必然とも言える。
バグはSQLiteの開発チームによって修正され、Tailscaleはその修正を取り込んだ。現在はこのバグに起因する破損は発生していないと報告されている。
3つの教訓
Tailscaleはこの経験から、以下の3点を教訓として挙げている。
第一に、再現できないバグには本番環境での段階的な計装が有効だ。トランザクションログパイプラインはもともと復旧手段として構築したものだったが、「消えた書き込み」を可視化する診断ツールにもなった。第二に、プロフェッショナルサポート契約は選択肢の一つとして現実的だ。SQLite開発チームとの直接連携によってtmstmpvfsの開発と調査の加速が実現した。独力での調査では到達できなかったレイヤーに踏み込めたという。第三に、シングルライター・シリアライザブル構成は診断を大幅に単純化する。マルチライターDBでは不可能なトランザクション再生が、今回の調査の起点になった。
なお、Tailscaleのコントロールプレーンが扱うのは設定データ(tailnetのメタデータやデバイス情報)のみで、プライベート暗号鍵やネットワークトラフィックは含まれない。今回のデータ破損でユーザーの通信内容が漏洩した事実はないと明記されている。
詳細はHow Tailscale helped find the SQLite WAL-Reset bugを参照していただきたい。