Carpe Diem

備忘録

外部サービスへのリクエストにEnvoyのCircuit Breakerを設定する

概要

サービスの信頼性を高めるため、依存するマイクロサービスが落ちても障害範囲をそこだけに留め、それ以外のマイクロサービスは稼働し続けるのが理想です。

自分たちのサービスであれば各マイクロサービスにCircuit Breakerを設定することで、スローダウンした際に即座に503を返しリクエストをスタックさせずに迅速に返すことが可能です。

一方外部サービスの場合はCircuit Breakerを備えているかはサービスによって異なります。場合によってはスローダウンした場合ずっとリクエストが返らず、依存しているマイクロサービスはリクエストがどんどんスタックしてOOMが発生する可能性があります。

Envoyを使うと外部サービスへのリクエストに対しても簡単にCircuit Breakerを設定することが可能です。

環境

  • Envoy v1.22.0
  • Go v1.18.3

前提知識

Circuit BreakerとThrottleの違い

よく混同されがちですが、Circuit BreakerとThrottleは異なります。

機能名 役割
Throttle Incoming traffic(中に入ってくるトラフィック)を制限する
Circuit Breaker Outgoing traffic(外に出ていくトラフィック)を制限する

図でイメージすると以下です。

Circuit Breakerは外に出ていくトラフィックを監視し、流量を制限します。

Ingress Circuit BreakerとEgress Circuit Breaker

EnvoyではIngress、EgressのListenerがあるので、先程の図を念頭に説明します。

Ingressの場合

以下のようにアプリケーションを外部のリクエストから保護するような形で働きます。

EnvoyからApplicationへ出ていくトラフィック(outgoing traffic)を監視します。

Egressの場合

Egressは逆で、外部サービスの過度のトラフィックを流さないように制限します。

EnvoyからExternal APIへ出ていくトラフィック(outgoing traffic)を監視します。

制限することで外部サービスがスローダウンしてもすぐにEnvoyがApplicationへ503を返してくれるので、Application側でそれをハンドリングしてフォールバック処理など入れれば正常系で返すことも可能です。

導入

それでは実際にEnvoyを使って実現してみます。

構成

構成としてはApplicationにsidecarのenvoyを置き、外部APIへのリクエストを制限するようにします。

Envoyの設定

通常のIngressのListenerに加え、EgressのListenerも追加します。

static_resources:
  listeners:
    - name: sidecar
      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
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains: ["*"]
                      routes:
                        - match: { prefix: "/" }
                          route:
                            cluster: app
                            timeout: 30s
                http_filters:
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
    - name: external  # here
      address:
        socket_address: { address: 0.0.0.0, port_value: 8082 }
      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: egress_http
                codec_type: AUTO
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains: ["*"]
                      routes:
                        - match: { prefix: "/" }
                          route:
                            cluster: external
                            timeout: 30s
                http_filters:
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

Egress用のClusterにCircuit Breakerを設定します。

  clusters:
...
    - name: external
      connect_timeout: 0.25s
      type: STRICT_DNS
      lb_policy: ROUND_ROBIN
      circuit_breakers:  # here
        thresholds:
          max_connections: 15
          max_pending_requests: 10
      load_assignment:
        cluster_name: external
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: external
                      port_value: 8002

動作検証

ではk6を使って負荷をかけてみます。

VUS: 30の負荷をかけるので常に5ユーザ分あふれる想定です。

メトリクス

app

自分のアプリケーションはほとんどのリクエストが503になっています。
依存する外部サービスから(実際はEnvoyから)503が返ってきたためです。

少量のリクエストが正常系で処理されていました。

activeコネクションやpending requestはCircuit Breakerに設定した通りの値になっています。

Circuit Breakerも期待通りの挙動でOpenしています。

external

外部サービスはリクエストが制限されて負荷がかかっておらず、全て正常系で返っていることが確認できます。

アプリケーション側で正常系に返ったメトリクスと同じです。

その他

サンプルコード

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

github.com

外部サービスの上限値RPSをApplicationの数で割る必要がある

External APIのquotaが分かっていたとして、その上限に達しないようCircuit Breakerを設定することになります。
なのでApplicationの台数で上限値を割る必要があります。

これによって以下の問題が生じます。

  • staticなconfigだとapp側がスケールしたタイミングでExternal APIへの流入が変化してしまう
  • 特定のappにリクエストが偏ると、External APIにまだ余裕があってもCircuit BreakerがOpenしてしまう

これらの問題を考慮して使用する必要があります。

まとめ

EnvoyのCircuit Breakerを外部サービスへのリクエストに対して設定し、外部サービスがスローダウンしてもリクエストがスタックしないで済むアーキテクチャにできました。

参考