みなさんこんにちは。
「あの人も読んでる」、第22回目の投稿です。maguro(X @yusuktan)がお届けします。
今回は、TailscaleがSQLiteに16年間潜んでいたバグを突き止めた記録、「How we tracked down a 16-year-old SQLite bug」を紹介します。
この記事を起点に、AIが書くコードが増えるなかで、形式仕様によるモデル検査やDSTなど、機械的に不具合を探す手法の重要性についても考えます。
僕は以前の「も読」で、分散システムのバグを再現可能な形で探す決定論的シミュレーションテスト(Deterministic Simulation Testing、DST)について書きました。
今回は、実装を動かして状態空間を探索するDSTとは少し違う方法で、膨大な組み合わせから不変条件を破る短い処理順序を取り出す過程についても触れていきます。
AI時代、ソフトウェアが期待通りに正しく動くことを機械的に検証できる方法の価値が上がっていると思います。
分散システムそのものの開発に携わる機会のある方はそこまで多くないかもしれませんが、「そんな手法もあるんだー」というくらいの気持ちで気軽に読んでいただければと思います。
「枯れた技術」で起きた19回のデータベース破損
Tailscaleは2022年から、端末やネットワークの設定を管理・配布するコントロールプレーンの主要なデータベースとしてSQLiteを使っています。
Tailscaleのコントロールプレーンは、ユーザーごとのプライベートネットワークであるtailnetを単位として、複数のシャードに分かれています。1つのシャードが複数のtailnetを担当し、その情報を1つのSQLiteデータベースへ保存します。各シャードでは、1つのGoプロセスだけがそのSQLiteデータベースへアクセスするsingle-writer構成になっています。
Tailscale自身も記事の中で、SQLiteを良い意味での「boring technology」と表現しています。
広く使われ、挙動がよく知られた技術を選んだので、データベースそのものに長期間悩まされることはないはずでした。
Tailscaleは数分おきにSQLiteデータベース全体のスナップショットを作り、そのファイルをS3へ保存していました。あるとき、そのバックアップを読み取るデータパイプラインが、とあるエラーを報告します。
データやインデックスなど、SQLiteファイル内部の整合性を検査するPRAGMA integrity_checkを実行すると、実際にデータベースが壊れていました。
その後も発生は止まらず、Tailscaleは約6カ月のあいだに19回のデータベース破損に見舞われました。
Tailscaleのデータベースには、ユーザーの秘密鍵やネットワークトラフィックは保存されていません。それでも、復旧中は対象シャードのコントロールプレーンを停止する必要があり、初期のインシデントでは停止が1時間を超えました。すでに接続している端末同士の通信は続きますが、新しく起動した端末は接続先の情報を取得できず、管理画面やAPIも利用できなくなります。
発生条件には、なかなか共通点が見つかりませんでした。特定のシャード、ユーザー、機能、時刻、負荷の高さとは結びつかず、発生間隔も数時間から数週間までばらばらでした。人工的に再現できないため、Tailscaleは次の発生を待ちながら、本番環境へ少しずつ観測点を追加していきます。
ここが一番厄介なところです。
データベース破損は確かに発生しているのに、そこに至るための手順が分からない。観測点を追加しながら、本番で次に起きる1回を待つしかありませんでした。めったに起きない並行処理のバグを、本番環境で追いかける難しさがよく表れています。
