TT Lab
はじめる
学ぶ 学習パス コース

ACKの前に止まったお菓子の自販機

在庫とカーソルを同じ時点で記録する

TT Labで続きを見る

一言でいうと

スナップショットはきれいなJSONファイルではなく、状態と、その状態が含む最後のイベントを、同じ時点に束ねる約束です。

なぜ必要なのか

倉庫の在庫が7で、最後の番号が0のとき、表示板が全体の復旧を要求しました。サーバーが先にlast=0を読み、そのあいだに入庫5がコミットされたとします。続けて在庫を読むと、total=12になります。それぞれの値は実際に存在した値ですが、last=0とtotal=12という組み合わせは、存在したことのない状態です。このスナップショットを設置した表示板は、番号1のイベントの+5をもう一度受け取って、17を表示します。

このミスは、JSON文法のチェック、型のチェック、ファイルハッシュの比較だけでは見つかりません。サーバーが自分で作ったJSONなので出どころも正しく、数字も範囲内で、転送中に変わったバイトもありません。誤りは、2つの照会が別々の時点を読んだことにあります。整合性の検査が何を確認し、何を確認しないのかを、区別する必要があります。形式を厳格に検証することと、意味が一貫したスナップショットを作ることは、どちらも必要です。

どう動くのか

ラボのexport_snapshotは、読み取りトランザクションの中で、checkpointのepochとlastを先に読み、次にstock.totalを読みます。途中のbetweenフックは、別の接続に新しいイベントをコミットさせるためのテストポイントです。戻り値はversion・epoch・last・totalの4つのキーで、versionは整数の1です。同じトランザクションの2つの照会は1つの状態を構成し、トランザクションを終えたあとで新しく照会すると新しい状態が見えるかも、別に確認します。

SQLiteのWALモードの、読み取り時点の分離を使います。読んでいるあいだに別の接続がコミットしても、その読み取りトランザクションは、もとの時点を維持できます。逆に、exportをBEGIN IMMEDIATEで始めると、書き込みの席を先取りして、テストの別の接続を塞いでしまいます。元データへの書き込みには直列化が必要ですが、照会だけを行うエクスポートが、すべての入庫を塞ぐ必要はありません。いつ読み取りトランザクションを使い、いつ書き込みトランザクションを使うのか、意図を分ける必要があります。

テストでは、readerの接続の最初の照会のあとで、writerの接続が+5をコミットし、readerが2回目の照会を行います。結果は以前のカーソルと以前の在庫でなければならず、writerをもう一度見ると、新しい在庫があるはずです。偽のsleepで、運よく競合が起こるのを待つことはしません。フックで照会の間を直接指定するので、同じミスは毎回同じ失敗になります。これは、同時の読み取りと書き込みの論理的な重なりを検証するものであり、多くのスレッドのスループットを測る負荷テストではありません。

エクスポートしたスナップショットをレプリカに入れる過程も、アトミックでなければなりません。以前のeventsを消し、新しい在庫を入れてから、カーソルを変えるすべての作業が、1つのトランザクションです。途中でプロセスが落ちたら、以前の状態が丸ごと残るか、新しい状態が丸ごと残らなければなりません。古いtotalと新しいlastが混ざると、次の再生で欠落が起こります。コミットが終わったあとで応答が消えたことは、インストールがなかったという意味ではないので、同じスナップショットの再インストールは、状態を変えずにFalseを返すように設計します。

現場での姿

スナップショットを作るためにDBファイル1つをコピーする方法は、論理的な状態のエクスポートとは別物です。WALモードでは、別のファイルに、まだ反映されていない確定済みの内容があることがあります。このラボは、元の.dbファイルをコピーせず、クエリで必要な業務状態だけを取り出します。実際のバックアップは、使用するDBのバックアップAPIと復旧手順で、別に検証する必要があります。表示板用のJSONを作ったからといって、サーバー障害から復元できるバックアップも備えているとは言いません。

もう1つの運用上の問題は、読み取り時間を無闇に長くすることです。スナップショットをネットワークで送信するあいだ、トランザクションを握っておくと、遅い受信者がDBの古い状態を引き留めることがあります。ここでは、小さく固定されたメタデータと整数の在庫をメモリに読んでから、トランザクションを閉じます。実際の大きな状態は、分割・チェックサム・完成印・アクセス権限・転送の再開まで設計する必要があります。整数1つの例が、そのコストをなくしてくれるわけではありません。

バージョンも観察の対象です。確認したLinuxのラボイメージで、SQLiteが報告するバージョンは3.45.1です。公式のWALドキュメントは、3.51.3以降と一部のバックポートで、WAL-resetの欠陥が修正されたと説明しています。このラボは有限な単一の書き込みの流れで、各接続の自動チェックポイントをオフにし、同時のチェックポイント作業は作りません。接続の終了も、書き込みが終わったあとで順番に行います。これは、複数の書き込みがある本番サーバーの、安全な基本設定を提案するものではありません。実際のデプロイでは、ディストリビューションがパッチを含んでいるかと、修正リリースを確認する必要があり、バージョン文字列1つでパッチの状態を断定してはいけません。

次の確認ですること

続くクイズで、ずれたスナップショットと書き込みロックの違いを判断します。あとの総合ラボで、export_snapshotとinstall_snapshotを実装します。2つのSELECTの間のコミットを誤って差し込むと、実際の値total=12と、期待する値total=7が一緒に出てきます。インストール過程のafter-clearとafter-installのポイントには、例外と実際の子プロセスの終了を、それぞれ入れます。例外処理のロールバックが正しいかと、後始末のコードがまったく実行されなくてもDBが復旧するかを、区別して観察します。

参考: SQLiteの読み取り時点の分離、WALのファイル・並行性・既知の欠陥。このラボの在庫JSONは、データベース全体のバックアップではありません。