16年前のSQLite WAL-reset bug、Tailscaleの19回の障害が突き止めた競合

Tailscaleのcontrol planeでは、6カ月の間にSQLite databaseが19回破損した。最近のdeploy、特定のshard、顧客機能、負荷、時間帯との共通点はなく、synthetic testでも再現できない。手掛かりになったのは、あるtransactionでcommitしたdataが後続transactionから消える現象と、WALに存在する数より多くのpageをcheckpointしたと報告するmetricだった。

構成は意図的に単純で、各control-plane shardのSQLiteを単一のGo processが専有していた。SQLiteが得意とするsingle-writer設計である。ただし高速で一貫したbackupを作るため、checkpointを自動動作へ任せず、手動で高頻度に実行していた。公開され、support対象でもある使い方だが、一般的な運用経路ではない。極めて狭いtiming windowに当たる確率が、この環境では高くなった。

原因を捉えたのはSQLite開発者が作ったtmstmpvfs shimだった。virtual filesystem層を包み、page writeとcheckpointの変化を詳しく記録する。次の本番破損後、そのtraceからwrite transactionとWAL resetのdata raceが見つかった。特定の瞬間にwriteが重なると、checkpointは一部のpageをmain databaseへコピー済みだと誤認する。実際には書かれていないpageが永久に失われ、indexなど参照側だけが残ってfileが不整合になる。

SQLiteは、別threadがWALをresetしたか確認するcheckをcheckpoint関数へ追加した。最初に修正を含んだ3.52.0は、textからfloating-pointへの変換変更によりstale expression indexを破損と誤検出する別問題を起こし、withdrawされた。その後、WAL-reset修正だけを含む3.51.3が公開された。Tailscale側もtimestampの精度を整数秒へ下げ、expression indexの曖昧さを避けた。

事故が止まっただけでは証明にならないため、driverにも観測点を加えた。writeとWAL resetが重なると警告するようにし、2カ月後にまさにその条件が発生した。警告は出たがdatabaseは壊れず、その後も記事公開まで4カ月間、追加事故はなかった。長い静穏期間より、修正が実際の衝突を防いだnear missの方が強い確認材料になった。

コミュニティでは、TailscaleがSQLiteのprofessional supportを契約し、競合を切り分けたopen-source VFS tracing toolへ資金を出した点も評価された。詳細な技術回顧は復旧と調査の全体を説明し、Hacker Newsの議論はconnectionとcheckpointの条件を掘り下げる。教訓はSQLiteを避けることではなく、十分にテストされた通常経路から外れるなら観測と復旧を追加設計することだ。