ネットワーク呼び出しは関数呼び出しではない
一言でいうと
同期呼び出しで、デフォルトとして必ず決めておくべき3つは、タイムアウト、リトライする条件、そしてリトライしない条件です。
なぜ必要なのか
モノリスの中の関数呼び出しは、失敗のモードが1つでした。例外を投げるか、値を返すか。ネットワーク呼び出しは、失敗のモードが5つあります。接続の失敗、接続はできたのにレスポンスがない、遅いレスポンス、半分しか届かないレスポンス、そして最もたちが悪いもの、つまり相手は処理したのにレスポンスだけが失われた場合です。
最後の場合がなぜたちが悪いかというと、呼び出し側は「できなかった」と「できたのに知らない」を区別する方法がないからです。この区別できないことが、後に出てくる冪等性とサーガの出発点です。
どう動くのか
まずタイムアウトから見てみましょう。デフォルト値のないHTTPクライアントが多く、なければ事実上無限に待ちます。無限に待つことが危険な理由は、スレッドやコネクションが塞がれるからです。ダウンストリームが5秒ずつ遅くなると、アップストリームのコネクションプールが先に枯渇します。そうすると、ダウンストリームとまったく関係のないリクエストまで失敗します。これが、連鎖障害の最もよくある始まりです。
タイムアウトの値は、当てずっぽうではなく、相手のp99から出発します。相手のp99が120msなら、300–500ms付近が妥当です。p99の10倍に設定すると、タイムアウトがあってもないのと同じになります。
リトライには条件があります。リトライに意味があるのは、一時的なエラーだけです。接続の失敗、タイムアウト、503、429。400、401、404のような確定的なエラーは、何回送っても同じ答えです。そして、リトライは必ず指数バックオフとジッターを併用します。固定の間隔でのリトライは、サンダリングハード(thundering herd)を作ります。
ここで、数字を1つ覚えておくとよいでしょう。注文サービスのインスタンスが20台で、毎秒100件を処理しているときにmaxAttemptsを5にすると、ダウンストリームが不安定になった瞬間に、決済サービスが受けるリクエストは毎秒最大10,000件です。100 x 20 x 5。リトライは負荷を掛け算します。
gRPCを使えば変わるのかというと、シリアライズが速くなってストリーミングが使えるようになりますが、上の3つの問題はそのままです。ただし、デッドラインがプロトコルの第一級の概念で、ホップを越えて伝播する点は、RESTより優れています。
現場での姿
呼び出しのチェーンがA → B → Cのとき、タイムアウトをそれぞれ3秒にしてしまうミスがよくあります。すると、Aは最大3秒待ちますが、BはCを3秒待つので、Aのタイムアウトが先に発生しても、BとCは働き続けています。捨てられる作業にリソースを使っているのです。タイムアウトは、外側が大きく、内側が小さくなければなりません。そして、残りの予算をヘッダーやデッドラインで下に伝えれば、さらによくなります。
タイムアウトは階層ごとに変える
1つのリクエストが複数の階層を通るとき、タイムアウトは外側から内側に行くほど短くなければなりません。 逆だと、外側が先にあきらめて、内側は誰も待っていない答えを作り続けます。
브라우저 30s
게이트웨이 10s ← 바깥보다 짧다
API 6s
결제 서비스 3s
DB 1s
各階層にリトライがあると、掛け算になります。APIが3秒のタイムアウトで2回リトライするなら、 最悪で9秒になり、ゲートウェイの10秒をほとんど使い切ります。タイムアウト × (リトライ+1)が 外側のタイムアウトより小さくなければなりません。
期限をヘッダーで伝える方法もあります(deadline propagation)。gRPCはデフォルトで
サポートしていて、HTTPではX-Request-Deadlineのようなヘッダーを自分で作ります。残り時間が
0に近ければ、そもそも開始しないことが、リソースを節約します。
サーキットブレーカーが開く条件
リトライだけでは、崩れた下流を救えません。むしろ負荷を増やします。サーキット ブレーカーは、失敗が続くと、試行そのものを止めます。
닫힘(정상) ──실패율 임계 초과──> 열림(즉시 실패)
↑ │
└──성공── 반열림(몇 개만 보내 봄) <──일정 시간 뒤──┘
このコードブロックの韓国語コメントは、正常な閉の状態が失敗率のしきい値の超過で即座に失敗する開の状態になり、一定時間の後に少数だけ送って様子を見る半開の状態を経て、成功すれば閉に戻る、という意味です。
しきい値は、個数ではなく、割合と最小サンプル数で決めます。「10個中5個が失敗」は 意味がありますが、「2個中1個が失敗」は偶然です。
# 예: 20건 이상일 때, 실패율 50% 넘으면 30초 열림
minimumNumberOfCalls: 20
failureRateThreshold: 50
waitDurationInOpenState: 30s
permittedNumberOfCallsInHalfOpenState: 3
このコードブロックの韓国語コメントは、呼び出しが20件以上のとき、失敗率が50%を超えると30秒間開く、という意味です。
開いたときに何を返すかが、設計です。キャッシュされた古い値、縮小したレスポンス、または 明確なエラー。何の計画もなく500を投げると、サーキットブレーカーがあってもないのと同じです。
部分的な失敗をレスポンスに含める
複数のサービスを呼んで1つの画面を作るとき、1つが失敗したからといって全体を失敗させは しません。何がないのかを知らせるレスポンスのほうが優れています。
{
"order": {"id": 1043, "amount": 52000},
"recommendations": null,
"degraded": ["recommendations"]
}
画面は、おすすめの領域だけを空にして、残りを表示します。こうするには、必須と任意を 先に分けておく必要があります。注文情報は必須、おすすめは任意。この区別がないと、 すべての呼び出しが必須になり、可用性が掛け算されます。
次のラボですること
意図的に遅く、意図的に失敗するダウンストリームを相手に、タイムアウトを設定し、指数バックオフとジッターを実装し、4xxはリトライしないようにし、最後にリトライバジェットを計算してみます。