Carpe Diem

備忘録

Envoy, Istio で Outlier Detection を使う

概要

EnvoyやIstio では Outlier Detection という、連鎖障害を起こさないように不具合のあるホストを切り離す仕組みがあります。

  • ヘルスチェックで外されるほど完全に落ちてはいない
  • しかしサービスとしては無視できないほどエラーレートが高い

といった状況でうまく機能してくれます。

今回はこの機能の説明と、具体的な設定・挙動について紹介します。

環境

  • Istio: 1.28.2
  • Envoy: v1.37.0

前提知識

Envoy の Health Check と Outlier Detection の違い

まず混乱しやすい Health Check との違いを話します。

Envoy における Health CheckOutlier Detection の主な違いは、チェックの「能動性」にあります。

Health Check

能動的(Active)なチェックです。

自ら専用のリクエスト(L7のパス指定やL4の疎通確認)を定期的に送信し、エンドポイントが生存しているかを確認します。
実際のトラフィックが流れていなくても、強制的に状態を確認し続けます。

能動的&定期的に状態をチェック

ヘルスチェックに失敗すると…

ルーティングから外される

なのでホストが完全に落ちていれば必ず外されますが、中途半端に生きてるときは外してくれません。

Outlier Detection

受動的(Passive)なチェックです。

実際のユーザートラフィックの挙動を監視(観察)し、エラー率の上昇や連続した失敗などの異常を検知して、一時的にそのエンドポイントを負荷分散から外します。
実際にリクエストが流れて初めて異常に気づく仕組みです。

通常時

特定のサービスでエラーが発生

Outlier Detectionによる切り離し

パラメーター次第ですが、何回エラーが出たら一時的に外す、といった設定が柔軟にできるため、中途半端に生きていてもハンドリングできます。

具体的な使い方

Envoy で OutlierDetection を使ってみる

Envoy では Cluster 設定の中で outlier_detection フィールドを定義します。

clusters:
- name: my_service
  connect_timeout: 0.25s
  type: STRICT_DNS
  # ... (その他の設定) ...
  outlier_detection:
    consecutive_5xx: 5           # 5回連続で 5xx エラーが発生したら切り離す
    interval: 10s                # 10秒ごとにスイープ(チェック)を行う
    base_ejection_time: 30s      # 切り離す基本時間(30秒)
    max_ejection_percent: 10     # 切り離すホストの最大割合(デフォルト 10%)

パラメータ説明

フィールド (Envoy) 説明 デフォルト値
consecutive_5xx ホストを切り離す判断基準となる、連続した 5xx エラー数。 5
interval 異常検知のスイープを行う時間間隔。 10s
base_ejection_time ホストが切り離される最短時間。切り離し回数が増えるとこの値の倍数で時間が延びる。 30s
max_ejection_percent 負荷分散プールから切り離すことができるホストの最大割合。 10%
consecutive_gateway_failure ゲートウェイエラー(502, 503, 504)が連続した際の閾値。 5 (Envoy)
1 (Istioデフォルト動作時)
split_external_local_origin_errors 外部エラー(5xx)とローカル起因のエラー(接続タイムアウト等)を区別するか。 false
consecutive_local_origin_failure ローカル起因の失敗が連続した際の閾値。 5
max_ejection_time ホストが切り離される最大時間。 無制限
enforcing_consecutive_5xx consecutive_5xx による切り離しを実際に実行する確率(%)。 100%
failure_percentage_threshold ホストの全トラフィックに対する失敗率の閾値(%)。 85%
min_health_percent パニックモードに入らずに稼働し続けるべき最小の健康なホスト의 割合。 50%

Istio で OutlierDetection を使ってみる

Istioだと DestinationRuleで次のように設定します。

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: reviews-cb-policy
spec:
  host: reviews.prod.svc.cluster.local
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        http2MaxRequests: 1000
        maxRequestsPerConnection: 10
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 10s
      baseEjectionTime: 30s
      maxEjectionPercent: 10

Envoy とのデフォルト値の違い

Istio の DestinationRule では、いくつかのパラメータで Envoy の標準的なデフォルト値と異なる設定が採用されています。

パラメータ Envoy デフォルト Istio デフォルト 備考
minHealthPercent 50% 10% Istio はより多くのホストが異常になっても切り離しを継続する傾向にあります。
consecutiveGatewayErrors 5 1 (実質) Istio では 5xx エラーの一部として扱われ、より敏感に反応する設定が一般的です。

動作確認

それでは実際の挙動を確認する構成を作成します。

アーキテクチャ

次のような構成で動作を確認します。

backend 1はリクエストでエラーレートを変更できるようにしています。

設定

まずはEnvoyの設定です。

envoy.yaml

outlier_detection を設定したクラスタを定義します。

static_resources:
  listeners:
  - address:
      socket_address: { address: 0.0.0.0, port_value: 10000 }
    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
          route_config:
            name: local_route
            virtual_hosts:
            - name: service
              domains: ["*"]
              routes:
              - match: { prefix: "/" }
                route: { cluster: backend_cluster }
          http_filters:
          - name: envoy.filters.http.router
            typed_config: {}

  clusters:
  - name: backend_cluster
    type: STRICT_DNS
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: backend_cluster
      endpoints:
      - lb_endpoints:
        - endpoint: { address: { socket_address: { address: backend1, port_value: 8080 }}}
        - endpoint: { address: { socket_address: { address: backend2, port_value: 8080 }}}
    outlier_detection:
      consecutive_5xx: 3
      consecutive_gateway_failure: 2
      interval: 5s
      base_ejection_time: 15s
      max_ejection_percent: 50
      split_external_local_origin_errors: true
      enforcing_consecutive_5xx: 100
      enforcing_consecutive_gateway_failure: 100
      success_rate_minimum_hosts: 5
      success_rate_request_volume: 100
      success_rate_stdev_factor: 1900

この設定では、単純な連続エラー(consecutive_5xx)だけでなく、ゲートウェイエラー(consecutive_gateway_failure)や、クラスタ全体の平均成功率と比較して異常なホストを特定する成功率ベースの検知(success_rate_...)も有効にしています。

また、split_external_local_origin_errors: true を設定することで、ネットワーク層のタイムアウト等の「ローカル起因の失敗」と、アプリケーションが返す 5xx 等の「外部起因の失敗」を切り分けてカウントしています。

実行

期待する挙動は以下です

  1. k6でリクエストを流す
  2. 正常にリクエストが流れていることを確認
  3. backend 1 のエラーレートを上昇させる
  4. envoyがbackend 1を切り離す
  5. backend 1のエラーレートを戻し、復旧する
  6. エラーレートが大きく上がらない状態が担保される

1. k6でリクエストを流す

k6を実行します。

$ k6 run k6/script.js

2. 正常にリクエストが流れていることを確認

エラーも特に出ず、2台とも正常にリクエストが流れています。

3. backend 1 のエラーレートを上昇させる

backend 1のエラーレートを50%にします。

curl -X POST "http://localhost:8081/error-rate?rate=50"

4. envoyがbackend 1を切り離す

envoyがbackend 1を切り離し、アクティブなホストが1つだけになります。

リクエストもbackend 2に偏ります。

トータルとしては若干エラーレートが出るようになります。

5. backend 1のエラーレートを戻し、復旧する

backend 1のエラーレートを0%にします。

curl -X POST "http://localhost:8081/error-rate?rate=50"

backend 1が復旧し、envoyにルーティングされるようになります。

リクエストも再び分散されます。

6. エラーレートが大きく上がらない状態が担保される

特定のサービスが50%ものエラーレートになりましたが、全体でのエラーレートは1%にも満たない状態に抑えられました。

  █ TOTAL RESULTS

    checks_total.......: 113724 189.491626/s
    checks_succeeded...: 50.00% 56862 out of 113724
    checks_failed......: 50.00% 56862 out of 113724

    ✗ is status 20099% — ✓ 56796 / ✗ 66
    ✗ is status 5xx
      ↳  0% — ✓ 66 / ✗ 56796

その他

サンプルコード

今回のサンプルコードはこちら

github.com

なぜ外してからもエラーレートが微少に出る?

外してからもEnvoyは一定時間後に復旧できるかどうかのチェックを行うため、トラフィックを backend 1 にも流します。

まだ backend 1 のエラーレートが高いままであればまたしばらくリクエストを流しませんが、その期間が過ぎると再びチェックのためリクエストを流します。

それによってトータルで微少にエラーが生まれています。

まとめ

Outlier Detection を使うことで特定のサービスでエラーレートが大きく上昇しても、障害影響を局所かすることが可能になります。

参考