TL;DR
- Tailscale(VPNサービスを扱っている会社)が約6ヶ月間で19回のSQLiteデータベース破損
- 原因はSQLiteのチェックポイント処理と書き込みトランザクションの間の稀な競合状態(データレース)が原因
- 元記事
背景
Tailscaleとは複数のデバイスを安全につなぐVPNサービスを提供している会社です。
2022年から、社内のシステムではSQLiteを利用していました。
シャードと呼ばれる複数のサーバーに処理を分けて、各シャードに1台のプロセスを専任でデータベースを操作するという仕組みになっていました。
しかし、2025年8月、データベースのバックアップに破損(DBファイルの中身が壊れてRead/Writeができない事象)が見つかりました。
その後も同じ現象が繰り返し発生し、約6ヶ月間で19回のデータベース破損に至り、サービスが停止するという事象が発生していました。
調査過程
直近のコード変更を洗い出しても、問題の箇所はしばらく変更されていませんでした。
また、特定のシャードや顧客、機能、時間帯に偏りもなく、共通点が見つからず、不具合を意図的に再現する方法もありませんでした。
再現できないため、本番環境に調査用の仕組みを組み込み、インシデントのたびに情報を集めていましたが、決定打には至りませんでした。
並行して、簡単には解決しない問題だと判断し、SQLiteの開発チームと有償サポート契約を結び、共同で調査を進めたところ、候補となる原因をいくつか挙げてもらいました。
さらに、復旧を早めるため、データベースへの書き込みをすべて別のログに記録し、最後の正常なバックアップにそのログを適用し直すことで、壊れたデータベースを直接修復せずに最新状態を復元できるようにする仕組みも作成しました。
この仕組みが光明となりました。
このログを調査したところ、記録したトランザクションログをうまく適用できない現象が見つかり、コミットしたはずのデータが、後から見ると存在しない状態になっていました。
この手がかりをもとに、SQLiteの開発チームが新しい調査ツールを作成し、本番環境に投入したところ、決定的な情報を確認することに成功しました。
原因究明 + WAL処理解説
原因を理解するには、まずSQLiteのWAL(Write-Ahead Logging)を知る必要があります。
SQLiteのデータはページという小さな単位に分かれてファイルに保存されています。
更新という処理は重いため、更新のたびにメインのデータベースファイルを直接書き換えると、処理が遅くなったり、他の処理と衝突したりしやすくなります。
そこでWALモードでは、更新内容をいったん別ファイル(WALファイル)に追記していくという仕組みを採用しています。
書き込みが速い + 他の処理を邪魔しにくく、データを読み取るときは、WALファイルとメインのデータベースファイルの両方を見て最新の内容を組み立てるようになっています。
(コミットする、、とは、更新内容をこのWALファイルに書き込む作業を指します)
WALファイルに更新をため込み続けるわけにはいかないため、ある程度たまったら、その内容をメインのデータベースファイルにまとめて書き写す必要があります。
この書き写し作業をチェックポイントと呼びます。
チェックポイントが終わるとWALファイルは空に戻り(リセットされ)、新しい更新をまた受け付けられるようになります。
多くのシステムでは、このチェックポイントはSQLiteが裏側で自動的に判断して実行するのですが、
Tailscaleはバックアップを一貫した状態で取得するため、チェックポイントを自動任せにせず、自分たちのタイミングで手動実行していました。
見つかったバグ
新しい調査ツールにより、チェックポイント処理とWALへの書き込みトランザクション(コミット)が特定のタイミングで重なると発生する、極めてまれなデータの競合状態(データレース)であることが判明しました。
SQLiteは書き込みが発生するたびに、チェックポイントがどこまで進んだか共有メモリ(wal-indexヘッダー)から確認します。
WALの内容がすべてメインのデータベースファイルに反映済みであれば、WALファイルをリセットして先頭から使い直します。
このリセット判定とチェックポイント処理のタイミングが特定の形で重なると、チェックポイント側がすべて書き写し終えたと誤認することがありました。
(実際にはまだ書き写されていないページが残っている)
結果として、メインのデータベースファイルに反映されるはずだったデータが失われ、そのデータを参照する他の情報(インデックスなど)は正常に書き込まれているため、参照先と参照元の整合性が崩れ、データベース全体が破損したと判定されていました。
その後、、
原因判明後、SQLiteの開発チームは修正版(3.52.0)をリリースし、Tailscaleは本番環境に展開しました。
ところが直後、13個のデータベースで破損の誤検知が発生しました。
これは修正版に同梱されていた別の変更(数値の丸め処理の最適化)が引き起こした、また別のバグが原因でした。
そのため開発チームは3.52.0を撤回し、WAL-Resetの修正のみを含む3.51.3を再リリース。Tailscaleもこちらに切り替え、現在は安定運用に至っているとのことです。
最後に
いや、こんなバグ事前に気づけるかぁ~~~~い![]()