16年間放置されていたSQLiteのバグを突き止めた方法
親愛なる読者の皆さまへ。ご存じの通り価格高騰などの悪影響でサーバー運営がとても苦しい状態です。回線や台数を整理し見直せる部分は全て見直しましたが、やはりまだ危険水域です。このままだと1ページを10分割ぐらいして無理矢理PVを増やさざるを得なくなってしまいます。そこで、GIGAZINEの物理的なサーバーたちを、たった1円でも良いので親愛なる読者の皆さまに支援してもらえればとっても助かります!今すぐ寄付は上のボタンから! ・ これまでGIGAZINEを支援してくれたメンバーのリスト
VPN構築・ネットワーク接続サービスの Tailscale が、自社のサービスが安定しない理由について数ヶ月にわたり調査を行ったところ、データベースの SQLite に16年間潜在していたバグが原因であったことが判明しました。Tailscaleは「何が問題だったのか」「どう対応したのか」「SQLiteの不具合発見にどう貢献したか」についての説明を自社のブログにて公開しました。 How Tailscale helped find the SQLite WAL-Reset bug https://tailscale.com/blog/sqlite-wal-reset-bug Tailscaleは2022年からSQLiteを主要データベースとして利用していました。ユーザーがTailscaleのエンドポイントにアクセスすると「コントロールプレーン」に接続しますが、コントロールプレーンは複数の「シャード」で構成されています。安全なプライベートネットワーク「テールネット」はシャードのうちのどれかの上にありますが、状況によってシャード間をシームレスに移行することができる仕組みになっています。各シャードは自分が管理するテールネットに関する全情報を格納するSQLiteデータベースを持っています。単一のGoプロセスがSQLiteデータベースに排他的にアクセスし、テールネットのコントロールプレーンを処理します。
バックアップの仕組みは、数分おきにデータベースの完全なスナップショットを取得し、SQLiteファイル全体を Amazon S3 のバケットにアップロードします。一連の構成は2023年初頭から何の問題もなく稼働してきましたが、2025年8月にS3バックアップでデータベースの破損が報告されたことから問題が始まりました。データベース破損は6ヶ月間で19件発生したものの、影響はコントロールプレーンの設定データのみでした。ただし破損が発生するたびにコントロールプレーンプロセスを停止してデータベースを修復または復元する必要があったため、対象シャードを内包するコントロールプレーンが利用できなくなるダウンタイムが発生しました。
問題解決に向けてまずは直近の変更を確認したものの、不具合に関連しそうな変更は見当たりませんでした。特にSQLiteと連携する低レベルコードはすべて数年前に記述されており、不具合発生がなかったためコードの修正は行われていませんでした。念のため隅々までコードの精査も行ったもののデータ破損を引き起こすような記述は見られませんでした。また問題が発生するトリガーも不明であったためバグの再現を行うことができず、ライブ環境でのフォレンジックテレメトリーに頼らざるを得ませんでした。さらに厄介なことに、インシデントが数時間置きに発生することもあれば数週間何も起こらないこともありました。簡単に修正できる問題ではないと判断したTailscaleはSQLiteの開発者とプロフェッショナルサポート契約を結び、協力して問題の検証を行うことになりました。 根本的な原因の調査中もプラットフォームは稼働しており、復旧を自動化したりダウンタイムを短縮したりするために必要な以下の措置を行いました。 ・データベースが破損したら直ちにハードストップするようにシャードを構成する ・自動バックアップを導入する ・運用手順書とオンコールトレーニングを改善する 上記の取り組みにより応答時間を1時間未満に短縮することができ、さらに予期せぬ不具合の手掛かりを発見するきっかけともなりました。 データ損失やリスクなしにサービスを復旧させる方法を模索していたTailscaleはトランザクションログパイプラインを構築し、データベースを変更するすべてのSQLステートメントをログファイルにストリーミングことにしました。最新の正常なバックアップに対してこれらのトランザクションを再現するとデータベースが最新の状態に復元され、データベース破損が安全に回避されるという狙いがありました。
トランザクションログパイプラインの導入は期待した通りに機能したのみならず、不具合の手掛かりを得ることもできました。2件のインシデントでトランザクションログの再現に失敗し、詳しく調べたところ、あるトランザクションによって書き込まれコミットされたはずのデータが後のトランザクションには表示されないことが判明したのです。 明らかになってきた状況からSQLiteのチェックポイントプロセスのどこかに問題がある可能性が浮上しました。SQLiteデータベースは一連の「ページ」、つまり情報の小さなブロックで構成されています。データベースを更新する際はページの一部を更新された情報を含むものに置き換える必要があります。パフォーマンスと同時実行性を向上させるためにSQLiteは「Write-Ahead Logging(WAL)」を有効にして実行されます。つまり、新しいページはデータベースファイルに直接書き込まれず、一旦WALに書き込まれます。
新しいページをWALに無期限に書き込むことはできないので、ある時点でメインデータベースファイルにコピーして戻す必要があります。このプロセスが「チェックポイント」です。
通常はエンドユーザーや開発者が明示的にチェックポイントプロセスを実行する必要はなく、SQLiteが自動的にチェックポイントを実行するようになっています。しかしながらコントロールプレーンでは高速かつ一貫したバックアップを実行するためにチェックポイントプロセスを手動で制御するという非標準的なアプローチを行っています。 手掛かりの一つは、データベース破損の際にSQLiteが実際に利用可能なページよりも多くのページをWALからコピーしているとメトリクスが示していたことです。SQLiteはいくつかのレイヤーで構成されています。最上位層はパーサーおよびコードジェネレーターで、SQLステートメントをSQLiteの内部データ構造に変換します。これらのデータ構造はページャーに渡され、ページャーによってディスクに書き込まれる個々のページに分割されます。実際のディスクへの書き込みはOSインターフェース、つまり「仮想ファイルシステム」によって処理されます。このアプローチにより、さまざまなレイヤーをさまざまな実装で置き換えたり、既存のレイヤーをラップしてより多くの情報を取得したりできるわけです。
問題の診断を支援するためにSQLite開発者は追加のトレース情報とデータベースへの変更に関するログを書き込む仮想ファイルシステムのラッパーを作成しました。このラッパーにはSQLiteの仮想ファイルシステム(VFS)層を拡張・ラップするデバッグ・診断用 VFSシム 「tmstmpvfs shim」が含まれています。
ラッパーを本番環境にデプロイした直後に発生したデータベース破損でtmstmpvfs shimが残した追加のログにより、ついにSQLite開発チームはバグを発見・修正することができました。バグの原因はチェックポイントと書き込みトラ
この要約はメディアの公開フィードから5Newsが集約したものです。 文脈も含めた全文はgigazine.netにあります — コンテンツの権利はGIGAZINEに帰属します。