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

ICA — Istio認定アソシエイト

応答の失敗と業務の失敗は同じ出来事ではない

TT Labで続きを見る

一言でいうと

リトライは、応答をもう一度得ようとする試みであり、先行した業務を取り消す命令ではありません。このモジュールは、実際のIstioメッシュで、HTTPの受信回数とSQLiteの業務記録を別々に数え、応答が失われたりプロセスが落ちたりしても、同じ業務を重複して実行しない境界を作ります。

なぜ必要なのか

ユーザーが決済ボタンを1回押したのに、画面にはタイムアウトが表示されたとします。カスタマーサポートの担当者は、失敗したのでもう一度押してみるように案内します。サーバーの運用者は、プロキシが自動でさらに2回リトライすると説明します。ところが、業務の担当者は、同じ注文の決済記録が3件あると言います。3人の観測は、どれも事実である可能性があります。

画面のリクエスト1回、プロキシの送信の試行が複数回、データベースへの業務の書き込みは、それぞれ別の出来事です。HTTP 200だけを数えると、重複した業務が見えず、504だけを数えると、すでにコミットされた業務を、取り消されたものと誤解します。したがって、業務キー、送信のリクエストID、応答コード、実際に保存された業務IDを、一緒に残す必要があります。障害のレポートに「3回実行された」と書く前に、何を数えたのかをまず明らかにする必要があります。

LabHubの先行する合成実験でも、この違いを観測しました。同じ業務キーの同時リクエストのうち、一部のクライアントは504を受け取りました。そのうちの1つのリクエストは、サーバーの受信記録の最後の状態が200でした。業務記録は、1つだけ追加されました。この比率を、あらゆる環境の法則として覚えてはいけません。このラボは、負荷による偶然の比率の代わりに、業務のコミットのあとに応答を2秒遅らせる、比較用の経路を使います。

どう動くのか

3つの帳簿

clientのPodでcurlを1回実行すると、リクエストIDを1つ付けます。Istioが同じリクエストを再送すると、アップストリームのreceipts帳簿に、同じリクエストIDが複数回現れます。業務キーは、別のヘッダーです。送信を新しく始めても、同じ注文を回収しようとするリクエストなら、同じ業務キーを維持します。逆に、内容が同じでも、別の注文なら、異なるキーである必要があります。

サーバーには、意図的に誤った/naiveの経路と、重複防止の記録を使う/flakyの経路があります。どちらも、最初の2つの応答は実際のアップストリームの503、その次の応答は200です。プロキシだけで作られた偽のエラーではありません。/naiveは、受信するたびに業務を追加し、/flakyは、業務キーの既存の応答を再利用します。同じHTTP応答の並びでも、業務への効果が違うことがあります。

回数の設定は保証ではなく上限

VirtualServiceのattemptsは、最初の送信に加えるリトライの上限です。attemptsが2なら、最初の試行を含めて最大3回です。全体のtimeoutは、これらの試行が共有する予算です。perTryTimeoutは、各試行が待てる上限であり、速く失敗したリクエストが、その時間をすべて使ったものとは計算しません。

retryOnは、リトライする失敗の種類を選びます。HTTP 503を指定したからといって、すべての接続エラーや読み取りのタイムアウトまで、同じ条件にまとまるわけではありません。このラボは、単一のアップストリームを比較するために、以前のホストをもう一度選べるように設定します。実際のサービスでのリトライの暴走、バックオフ、サーキットブレーカー、容量計画まで、これで解決したとは考えないでください。

504のあとにも残っている業務

/lostは、先に業務と重複防止の記録をコミットし、応答だけを2秒遅らせます。この経路のプロキシの予算を500msにして、自動リトライを切ります。クライアントの504と、サーバーの業務記録を、同じキーで照会します。続いて、速い/chargeの経路に同じキーを送り、既存の業務の応答を回収します。新しい注文を作ろうとしているのではないので、ここで新しいキーを発行してはいけません。

これは、外部の決済会社のAPIではありません。合成の業務をローカルのSQLiteに書き込むサーバーです。応答の記録の200は、サーバーが作ろうとした応答であって、クライアントがそのバイト列を受け取った証明でもありません。受信記録とクライアントの原文を区別する理由です。

原子性は同じトランザクションの中で

業務を先にコミットして、重複防止のキーをあとで書くと、2つのコミットの間に隙間ができます。その隙間でプロセスが落ちると、業務は残るのにキーはありません。同じキーのリトライは、新しい業務と誤認されます。業務の書き込みと、キー・内容のフィンガープリント・応答の保存を、1つのトランザクションにまとめる必要があります。

SQLiteは、ある時点で1つの書き込みトランザクションだけを許可します。BEGIN IMMEDIATEで書き込みトランザクションを開始しても、永遠にロックを得られるわけではなく、競合するとBUSYやタイムアウトを扱う必要があります。このラボは、1つのローカルのDBファイルと、短い合成の作業だけを使います。複数のDBや外部の決済呼び出しを一度にアトミックにする、分散トランザクションを実装したものではありません。

現場での姿

サービスが再起動しても、メモリ上の辞書は戻ってきません。このラボでは、Podを実際に削除・再作成して、UIDとプロセスのinstanceが変わったかを確認します。同じVMのディスクに残したSQLiteファイルから、既存の応答を読み取ります。Podの交換に耐えたという事実を、VMの削除・ディスクの破損・電源障害からの復旧に広げてはいけません。

内容のフィンガープリントも必要です。同じキーで金額や顧客を変えて送ったとき、既存の応答を無条件に返す代わりに、衝突として拒否する必要があります。一方、JSONのプロパティの順序が変わっただけなら、同じ内容と見なせる必要があります。金額が同じ別の注文を、payloadのフィンガープリントだけを見てまとめてしまうのも、エラーです。

最後に、提供されたtransaction.pyの、コミットが分かれている欠陥を直します。検査器は、受講生の実装を新しい一時DBで実行し、実際の子プロセスにSIGKILLを送ります。コミット前の2つの地点では、業務とキーが一緒にない必要があり、コミット後は、一緒に残っている必要があります。そのあと、新しい接続でリトライしたときに、業務が1つかを見ます。例外を投げるだけで、強制終了の代わりにはなりません。

次のラボですること

Podのインベントリ、リトライのルート、業務の重複の比較、タイムアウト、内容の衝突、Podの交換を観測します。続いて、トランザクションのコードを修正し、同時リクエストと別の業務キーまで検査します。観測ツールは、実際の原文をファイルに残し、同じ観測ファイルがあれば、再実行しません。採点は、そのファイルと、サーバーに残った受信記録を突き合わせます。

業務DB・テストファイル・ネームスペースは、この学習用のVMの中にだけあります。秘密鍵や実際の決済データを入れないでください。ラボの終了時に、すべて回収されます。外部システムと結合するときに必要な、相手のAPIの冪等キー、outbox、再調整、補償処理は、あとの設計課題であり、ここで実装したと主張しません。

公式ドキュメントでさらに確認する