Tailscaleが見つけたSQLiteの16年物WALリセットバグ
SQLiteは、ソースコードがパブリックドメインで公開され、世界で最も多くのデバイスに組み込まれているデータベースエンジンのひとつです。
サーバーを必要としない単一ファイルの設計は、スマートフォンからネットワーク機器まで、ありとあらゆる場所で静かに動いています。
そんなSQLiteに、16年間も潜んでいたというバグの話が、今年の夏に海外の技術者たちのあいだで話題になりました。
発端は、VPNサービスを提供するTailscaleという会社の報告です。
彼らは自社のコントロールプレーンでSQLiteを主要なデータベースとして使っており、そこに起きたデータベース破損の原因を追い続けた結果、SQLiteのWALリセット処理に古いバグがあることを突き止めた、というのです。
コントロールプレーンとSQLiteの関係
Tailscaleのコントロールプレーンは、外部からは単一のエンドポイントに見えますが、内部では複数のコーディネーションサーバー(シャード)に分割されています。
各シャードは、自分が担当するテイルネットの情報を保持するSQLiteデータベースをひとつだけ持ち、単一のGoプロセスが排他的にアクセスします。
この単一ライター構成は、SQLiteが本来意図された使われ方です。
彼らは2022年からSQLiteを主要データベースとして採用しており、数分おきにデータベース全体のスナップショットを取得してS3にアップロードするバックアップ体制を整えていました。
ところが昨年8月、そのS3バックアップを読むデータパイプラインが、あるデータベースのエラーを報告しました。
SQLiteのPRAGMA integrity_checkコマンドを実行すると、確かに破損が検出されました。
SQLiteの破損は可能ですが、通常の運用ではまず遭遇しないはずのものです。
彼らは最初の破損を修復し原因を調査しましたが、手がかりは得られませんでした。
そして、その後も破損は繰り返し発生しました。
最終的にバグを解決するまでの6か月間で、合計19件ものデータベース破損に直面したのです。
WALリセットバグの正体
SQLiteは、書き込み性能と耐クラッシュ性を高めるために、WAL(Write-Ahead Logging、先行書き込みログ)という仕組みを提供しています。
WALモードでは、変更内容をまずWALファイルに追記し、のちにメインのデータベースファイルへ反映(チェックポイント)します。
これにより読み取りと書き込みがブロックし合いにくくなり、突然の電源断でも整合性を保ちやすくなります。
WALファイルは使い終わるとリセットされ、サイズが縮められたり削除されたりします。
問題となったのは、このリセット処理のエッジケースでした。
きわめて特殊なタイミングと状態が重なったとき、WALのインデックス情報が不整合を起こし、データベースファイル自体が破損してしまう、というものです。
この不具合はSQLiteに16年前から存在していたとされ、Tailscaleの調査がなければ、おそらくこれからも長く見つからなかったかもしれません。
破損の連鎖と忍耐強い調査
原因究明は困難を極めました。
最初に疑ったのは最近のコード変更ですが、SQLiteと直接やり取りする低レイヤーのコードは数年前に書かれたままで、直近の変更はありませんでした。
破損事例に共通の要因も見つかりませんでした。
特定のシャード、顧客、機能、時間帯、負荷のいずれにも紐付かず、再現条件を人工的に作り出すことができなかったのです。
そこで彼らは、本番環境に受動的なフォレンジックテレメトリを展開し、破損が起きる瞬間を現行犯で捉える戦略に切り替えました。
破損は不定期に発生し、数時間おきのこともあれば数週間空くこともありました。
それでも地道な監視と解析を続けた結果、SQLiteのWALリセット処理に潜む古いバグへとたどり着いたのです。
TailscaleはSQLiteの開発チームと協力して修正を提供し、この問題は解決されました。
古いバグから学ぶこと
今回の出来事は、広く使われ成熟した技術であっても、まれな条件で発火する不具合が長期間眠り続けることがある、という事実を示しています。
SQLiteは「退屈な技術」として信頼されてきましたが、退屈であることと完全であることは同義ではありません。
それでも、透明性の高い調査報告と、修正をコミュニティに還元する姿勢は、技術の信頼を支える大切な行為だと感じます。
バグは見つけられるのを待っている、とも言えるのかもしれません。
私たちがシステムを深く見つめるとき、その輪郭がほんの少しだけはっきりする。
そういう地道な営みの積み重ねが、次の16年を作っていくのだと思います。