リトライが障害を大きくする瞬間
一言でいうと
リトライは一時的な失敗を隠します。しかしバックエンドが過負荷のときにリトライを有効にしておくと、負荷を2–3倍に膨らませて障害を完成させてしまいます。そのため、リトライには常に上限と予算が必要です。
なぜ必要なのか
バックエンドの1つが遅くなる → タイムアウト → リトライ → 負荷2倍 → さらに遅くなる → リトライ → …
このフィードバックをリトライストーム(retry storm)といいます。メッシュはサービスごとにサイドカーを置くので、3段の呼び出しチェーンで各段が3回ずつリトライすると、一番末端のサービスは27倍の負荷を受けます。リトライは掛け算になります。
防ぐ仕組みは3つあり、それぞれ別の問題を解決します。
| 仕組み | 何を見るか | 何をするか |
|---|---|---|
| リトライバジェット | 全リクエストに対するリトライの割合 | リトライが一定の割合を超えたら、それ以上リトライしません |
| サーキットブレーカー | 同時接続数と待機リクエスト数 | 上限を超えたリクエストを即座に失敗させます(早く死にます) |
| 外れ値検出 | エンドポイントごとの連続失敗 | 問題のあるエンドポイントを一時的に外します |
どう動くのか
サーキットブレーカーは、名前に反して「遮断器」というより上限です。
# DestinationRule
trafficPolicy:
connectionPool:
tcp: { maxConnections: 100 }
http:
http1MaxPendingRequests: 20 # 커넥션을 기다리는 요청 상한
maxRequestsPerConnection: 100
上限を超えたリクエストは待たされず、すぐに503を受け取ります。残酷に見えますが、これが正しい動作です。待たせるとクライアントのスレッドまで塞がれて、障害が上流に広がるからです。早く失敗するほうがシステム全体にとって良いのです。
外れ値検出は、負荷分散プールから問題のあるエンドポイントを外す仕組みです。
outlierDetection:
consecutive5xxErrors: 5
interval: 10s
baseEjectionTime: 30s
maxEjectionPercent: 50 # 절반 넘게는 절대 빼지 않는다
maxEjectionPercentが重要です。これがないと、バックエンド全体が一時的に悪くなったときにエンドポイントをすべて外してしまい、サービスが丸ごと止まります。検出の仕組みが障害を作ってしまうのです。
よくある勘違い
「リトライは有効にしておくとよい」という考えは危険です。冪等でないリクエスト(POSTでの決済、注文)にリトライを設定すると、重複が発生します。EnvoyのretryOnのデフォルト値は安全な条件だけを含みますが、5xxを入れた途端、サーバーが処理の途中で失敗したリクエストも再送されます。
タイムアウトのないリトライ。リトライ3回でそれぞれのタイムアウトが30秒だと、最悪の場合90秒待つことになります。その間にユーザーはすでに離脱し、コネクションだけが塞がれたままです。リトライと一緒に全体の時間の上限(timeout)を決める必要があります。
リトライバジェットという安全装置
リトライを有効にするときに必ず一緒に設定するのが、リトライバジェット(retry budget)です。「全リクエストの何%までをリトライに使うか」を決めておくと、下流が崩れてもリトライのトラフィックが暴走しません。
# Envoy — 클러스터 단위 재시도 예산
circuit_breakers:
thresholds:
- priority: DEFAULT
max_retries: 3 # 동시에 진행 중인 재시도 상한
retry_policy:
retry_on: 5xx,reset,connect-failure
num_retries: 2
per_try_timeout: 1s
retry_back_off:
base_interval: 0.025s
max_interval: 1s
max_retriesは同時リトライ数の上限なので、リトライストームを物理的に防ぎます。これがなくnum_retries: 3だけを設定すると、下流が遅くなった瞬間にリクエスト1つが4つになり、負荷が4倍に跳ね上がります。崩れかけているサービスに4倍の量を送ることになるのです。
per_try_timeoutも重要です。これがないと、全体のタイムアウトの中で最初の試行が時間を使い切り、リトライする余裕がなくなります。次の関係が成り立つ必要があります: 全体のタイムアウト ≥ per_try × (リトライ + 1)
リトライしてよいものと悪いもの
| 条件 | リトライ | 理由 |
|---|---|---|
| 接続失敗、TCP reset | ✅ | サーバーがリクエストを受け取ってすらいません |
| 503、504 | ✅ | たいてい処理の前に拒否されます |
| 500 | ⚠️ | 処理中に失敗した可能性があります |
| タイムアウト | ⚠️ | サーバーは処理を終えているかもしれません |
| 4xx | ❌ | もう一度送っても同じです |
タイムアウトのリトライが最も危険です。決済リクエストが5秒でタイムアウトしたのに、サーバーは6秒で処理を完了していた場合、リトライによって二重に決済されます。そのため冪等でないリクエストでは、retry_onからタイムアウトを外すか、冪等キーを必ず使います。
Envoyはx-envoy-retry-onヘッダーでリクエストごとに異なる指定ができるので、参照は積極的に、書き込みは保守的にできます。
外れ値検出で不調なインスタンスを外す
リトライだけでは、特定のインスタンスが不調な場合は直せません。リトライが同じ不調な先に向かうことがあるからです。外れ値検出(outlier detection)がそれを外します。
outlier_detection:
consecutive_5xx: 5 # 연속 5번 5xx 면
base_ejection_time: 30s # 30초 뺀다
max_ejection_percent: 50 # 다만 절반 이상은 못 뺀다
interval: 10s
max_ejection_percentが安全装置です。全体が同時に不調になったとき(共通DBの障害)にすべてを外してしまうと、サービスが完全に止まります。半分を残しておくほうが、全滅するよりましです。
実務で本当に大切なこと
障害調査では、この3つを見分けるシグナルがそれぞれ異なります。
# 서킷 브레이커에 걸렸나 — 대기열 넘침
envoy_cluster_upstream_rq_pending_overflow
# 이상치 감지가 엔드포인트를 뺐나
envoy_cluster_outlier_detection_ejections_active
# 재시도가 얼마나 도나
envoy_cluster_upstream_rq_retry / envoy_cluster_upstream_rq_total
最後の割合が5%を超えていれば、すでにリトライが負荷の一部になっています。そこでリトライを増やすのは、火に油を注ぐことです。