概要
ネットワーク越しの通信は基本的に不安定であるため、外部APIとの通信で発生したエラーを都度クライアントに返すとエラー率が上がってしまいユーザ体験が悪くなってしまします。
そこでクライアントに返す前に何度かリトライすることで、エラー率を下げるようにするという実装を行うことが多いです。
例えばGCPのSDKではgax-goというライブラリがそこを保証してくれています。
google-cloud-go/invoke.go at 20e9b2acf7766594da827da62858d83cb03fed81 · googleapis/google-cloud-go · GitHub
gax-go/invoke.go at ce75229dd089591008bbaf03b1518474f9830646 · googleapis/gax-go · GitHub
しかしこういった処理は本来アプリケーションで表現したいドメイン部分ではないので、できればEnvoyやIstioといったソリューションで解決したいです。
今回はEnvoyでの設定方法を説明します。
環境
- Envoy 1.22.0
構成
以下のように前段にEnvoyを挟んでバックエンドのリトライを自動化します。
backend service
バックエンドサービスはエラーが出やすいよう、以下のようにランダムでresponse codeを変えるようにします。
func (h *handler) hello(w http.ResponseWriter, r *http.Request) { dur := rand.Intn(1000) time.Sleep(time.Duration(dur) * time.Millisecond) // 処理を表現するためのsleep n := rand.Intn(4) // エラーレスポンスを返すためのランダム値 switch n { case 0: w.WriteHeader(http.StatusOK) fmt.Fprintln(w, "Hello World") case 1: w.WriteHeader(http.StatusBadRequest) fmt.Fprintln(w, "Bad Request") case 2: w.WriteHeader(http.StatusInternalServerError) fmt.Fprintln(w, "Internal Server Error") case 3: w.WriteHeader(http.StatusServiceUnavailable) fmt.Fprintln(w, "Server Unavailable") } }
動作検証
では動作検証していきます。検証する上で正しくリトライされたのか確認できるよう、以下のstatsをGrafanaで可視化しておきます。
ref: https://www.envoyproxy.io/docs/envoy/latest/configuration/upstream/cluster_manager/cluster_stats
retryがない場合
envoy.yaml
特に追加の設定はしません。
結果
期待通りEnvoy〜Backendのリクエスト数及びエラー数と、クライアント〜Envoyのリクエスト数及びエラー数が一致しています。
retry_policyを付けた場合
Envoyでリトライの設定をするには
Network Filter chain > HTTP Connection Manaager > route_config
にretry_policyをつける必要があります。
envoy.yaml
static_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 8080 } filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http codec_type: AUTO access_log: - name: envoy.access_loggers.file typed_config: "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog path: /dev/stdout route_config: name: local_route virtual_hosts: - name: backend_service domains: ["*"] routes: - match: { prefix: "/" } route: cluster: backend_service retry_policy: # here retry_on: "5xx" num_retries: 3 per_try_timeout: 10s http_filters: - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
各パラメータの説明は以下です。
パラメータ | 説明 | デフォルト値 |
---|---|---|
retry_on | HTTPリトライ条件 gRPCリトライ条件 |
- |
num_retries | 再試行回数 | 1 |
per_try_timeout | リトライ1回あたりのタイムアウト | routes.route.timeoutと同値。 |
ポイント
retry_policyと同一階層にrouteのtimeoutの設定があります。
これはリトライを含めたtimeout値になるため、
routes.route.timeout > per_try_timeout * num_retries
にしないと意味がありません。
また前述の通り未指定の場合はroutes[].route.timeout
の値がセットされるため、実質リトライはされません。
結果
Envoy→Backendへのリクエスト数 > クライアント→Envoyのリクエスト数
となったことから、リトライが行われていることが分かります。
ただ思ったよりも5xx系エラーが多いです。その理由を次で説明します。
retry_overflowが多いのはなぜ?
これはCircuit Breakerのretry_budget
が影響しているためです。
Circuit Breakerは未指定であればデフォルトの値が設定されますが、ここに含まれるretry_budgetはアクティブなリクエストの内20%はretryによるリクエストであってもよいとなっています。
したがってupstreamへのretry割合が増えるとこのCircuit Breakerに引っかかってretry overflow
になってしまいます。
retry_policy + retry_budget
先程retry_budget
によってリトライが制限されたので、今回は検証のためバジェットを100%にしてみます。
envoy.yaml
先の設定に加えて、cluster側にCircuit Breakerの設定を追加します。
clusters: - name: backend_service connect_timeout: 0.25s type: STRICT_DNS lb_policy: ROUND_ROBIN circuit_breakers: # here thresholds: - retry_budget: budget_percent: value: 100.0 load_assignment: cluster_name: backend_service endpoints: - lb_endpoints: - endpoint: address: socket_address: address: backend port_value: 8000
結果
期待通りCircuit Breakerによるリトライの抑制は無くなりました。結果5xx系エラーも先程よりかなり減りました。
リトライ上限を超えたものに関してはretry_exceeded
となって5xxエラーが返っています。
今回は検証のため100%にしましたが、これによってアクティブなリクエストが全てリトライで埋まる可能性もあります。
なので実際はretry_overflowのstatsを確認しながら調整するのが良いでしょう。
より細かいリトライ条件
先程のretry_on
では5xx
という5xx系エラー全般を対象としましたが、Envoyではより細かい設定が可能です。
例えば以下のような条件があります。
リトライ条件 | 説明 |
---|---|
5xx | 5xx系エラーの場合 |
retriable-4xx | 409のようなリトライしても問題ない4xx系エラーの場合 |
retriable-status-codes | 指定したstatus codeの場合 |
retriable-headers | 特定のレスポンスヘッダを持っていた場合 |
例としてretriable-status-codes
で500
の場合のみリトライするように変更してみます。
envoy.yaml
routes: - match: { prefix: "/" } route: cluster: backend_service retry_policy: retry_on: "retriable-status-codes" #here num_retries: 3 per_try_timeout: 10s retriable_status_codes: # here - 500
retry_on
をretriable-status-codes
にし、retriable_status_codes
フィールドにstatus codeを書きます。
結果
期待通り500
のときのみリトライしてくれました。1件だけretry_limit_exceed
にはなりましたが、それ以外はretry_success
になりました。
その他
サンプルコード
今回のサンプルコードはこちら
まとめ
Envoyの自動リトライ機能の使い方を説明しました。
検証するまではCircuit Breakerがリトライを抑制すると考えていなかったので、サービス導入する前に知見を得られて良かったです。