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

統合とデプロイ

相手が遅くなったのに、こちらが先に倒れた

TT Labで続きを見る

目標

上位のリクエスト1つに与えられた時間を予算として捉え、その予算を、接続・読み取り・再試行・下位呼び出しに分け与えるクライアントを、自分で作ります。予算が足りなければ、かけずに早く失敗し、残りの予算を、ヘッダーで下に渡します。

なぜ重要なのか

タイムアウトに余裕を持たせる習慣は、安全に見えますが、実際は逆です。相手が30秒に遅くなったとき、こちらの上限が60秒なら、こちらのワーカーは30秒間縛られ、こちらを呼ぶ側から見れば、遅くなったのはこちらです。その遅延は、1階層上にもまた広がります。 そのため、タイムアウトは、相手を待ってあげる思いやりではなく、こちらを守るブレーカーです。そして、1つの値ではなく2つの値です。接続は届くまでの時間なので、数秒かかるなら、遅いのではなくできていないのであり、読み取りは相手が働く時間なので、業務によって違います。 予算として考えると、ルールは3行です。上位のリクエストに総予算を決め、各呼び出しの読み取り上限は、その時点の残りの予算にし、残りの予算が下限より小さければ、かけません。再試行とその待機時間も、同じ予算から出ていきます。 採点ツールは、作成した文章を信じません。遅くなるパートナーを、採点ツールが選んだポートで直接起動し、作成した呼び出し器を実際に動かして、かかった時間と、出力の区間の名前を、一緒に測ります。

ステップ

  1. /root/budget/partner.pyを作成してポート8014で起動し、失敗の3つの形を測って、/root/budget/probe.jsonに書いてください。
  2. /root/budget/call.pyを作成して、接続と読み取りの上限を別々に設定し、どの区間で切れたかを答えるようにしてください。
  3. /root/budget/deadline.pyを作成して、総予算1つで複数の呼び出しを行い、残りの予算が、次の呼び出しの読み取り上限になるようにしてください。
  4. deadline.pyに下限(--min-ms)を付けて、残りの予算が下限より小さければ、かけずにno_budgetとして飛ばすようにしてください。
  5. /root/budget/retry.pyを作成して、再試行の待機を指数で伸ばしてランダムに散らしつつ、その待機まで予算から引くようにしてください。
  6. /root/budget/fanout.pyを作成して、そのときの残りの予算を、X-Budget-Msヘッダーで下位呼び出しに渡してください。
  7. /root/budget/budget_plan.jsonに、画面1つの予算表を、数字で書いてください。
  8. /root/budget/budget_report.mdに、4つの節で報告してください。

参考

失敗の3つの形を測ってみる

/root/budget/partner.pyを作成してポート8014で起動し、接続拒否・接続タイムアウト・読み取りタイムアウトの3つを自分で測って、/root/budget/probe.jsonに、kind・target・elapsed_ms・errorの3行で書いてください。

遅い応答は、パートナーがsleepで真似ます。接続拒否は、誰も待ち受けていない127.0.0.1のポートで、接続タイムアウトは、どこにもルーティングされないアドレスで作ります。3つの場合にかかった時間が、互いにどう違うかが、このステップの核心です。

接続と読み取りを分けて設定する

/root/budget/call.pyを作成して、--connectと--readを別々に受け取り、失敗したときに、phaseにconnectかreadかを書くようにしてください。応答を受け取ったなら、ステータスコードが4xx・5xxでも、phaseはdoneです。

requestsのtimeoutは、2つの値を入れたペアを受け取ります。例外も種類が分かれているので、接続側と読み取り側を区別できます。ただし、接続拒否はタイムアウトの例外ではないので、別に捕まえる必要があり、それも相手に届く前に終わったことなので、connectの区間です。

残りの予算が、次の呼び出しの上限になる

/root/budget/deadline.pyを作成して、--budget-ms1つで、複数の--urlを順に呼ぶようにしてください。各呼び出しの読み取り上限は、その時点の残りの予算である必要があり、その値をread_timeout_msに書く必要があります。

前のステップのcall.pyをモジュールとして読み込んで使えば、同じコードを2回書かずに済みます。開始時刻を1度決めておき、呼び出しの直前ごとに「予算引くこれまでに使った時間」を計算してください。こうすれば、呼び出しがいくつあっても、全体が予算を超えることができません。

見込みのない呼び出しは、かけない

deadline.pyに--min-msを付けて、残りの予算がその値より小さければ、呼び出しをかけずに、phaseをno_budgetとして記録し、skippedを増やすようにしてください。飛ばした呼び出しのelapsed_msは、0です。

200ms残っているのに、普段400msかかる呼び出しをかけるのは、失敗を200ms先に延ばすことです。その場で失敗すれば、残りの時間で、部分応答だけでも作れます。下限は引数で受け取り、状況ごとに異なる値を渡せる必要があります。

再試行も、予算から出ていく

/root/budget/retry.pyを作成して、失敗した呼び出しをもう一度かけつつ、待機の上限をbase-ms * 2^(시도-1)(プレースホルダーは試行回数です)で伸ばし、実際の待機は、0からその上限の間で選ぶようにしてください。待ったあとにもう一度かける余地が予算にないなら、stopped_reasonをbudgetにして止めます。

ランダムを混ぜなければ、同じ瞬間に失敗したクライアントたちが、同じ瞬間にまた押し寄せます。そして、待機時間も予算から出ていくので、寝て起きてから呼び出しをかける余地が残っているかを、寝る前に問う必要があります。止まった理由を3つに分けて書いておけば、あとでログだけを見て、原因がわかります。

残りの予算を下に渡す

/root/budget/fanout.pyを作成して、<BASE>/budget?ms=200を--n回呼びつつ、毎回X-Budget-Msヘッダーに、そのときの残りの予算を載せて送ってください。出力の各callに、sent_budget_ms・received・elapsed_msを書きます。

こちらに1200msしか残っていないのに、下位のサービスが自分の基準で5秒待つと、その4秒は、誰も見ない答えを作るために使われます。ヘッダーの名前は、こちらが決める約束で、受け取る側は、その値を自分の上限にします。パートナーの/budgetは、受け取ったヘッダーの値をそのまま返すので、実際に伝わったかを確認できます。

画面1つの予算表

/root/budget/budget_plan.jsonに、予算表を書いてください。total_ms・reserve_ms・worst_case_msと、calls(名前・budget_ms・max_attempts)が3つ以上、必要です。reserve_msは100以上、worst_case_msは、呼び出しの予算の合計に予備を加えた値で、total_msを超えてはいけません。

予備(reserve)は、こちら側のシリアライズやテンプレートのレンダリングのような、呼び出しではない時間です。これを抜かして呼び出しに予算をすべて分け与えると、画面は、いつも上限ぎりぎりです。再試行がある呼び出しは、その呼び出しの予算の中で、再試行まで終える必要があるという意味です。

予算の点検報告書

/root/budget/budget_report.mdに、## 지금 무엇이 시간을 먹는가、## 구간을 어떻게 나눴나、## 재시도가 먹는 몫、## 예산을 넘겼을 때 무엇을 하나(韓国語の見出しで、順に今何が時間を食っているか、区間をどう分けたか、再試行が食う分、予算を超えたときに何をするか、を意味します)の4つの節で書いてください。probe.jsonとbudget_plan.jsonの数字が、本文に入っている必要があります。

読む人は、「タイムアウトを少し延ばしてください」と言った人です。延ばすと何が悪くなるかを、数字で見せてください。再試行の分は、試行回数と待機の上限を掛けて、最悪の場合を計算して書きます。