概要
サービスの信頼性を高めるため、依存するマイクロサービスが落ちても障害範囲をそこだけに留め、それ以外のマイクロサービスは稼働し続けるのが理想です。
自分たちのサービスであれば各マイクロサービスに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
外部サービスはリクエストが制限されて負荷がかかっておらず、全て正常系で返っていることが確認できます。
アプリケーション側で正常系に返ったメトリクスと同じです。
その他
サンプルコード
今回のサンプルコードはこちら
外部サービスの上限値RPSをApplicationの数で割る必要がある
External APIのquotaが分かっていたとして、その上限に達しないようCircuit Breakerを設定することになります。
なのでApplicationの台数で上限値を割る必要があります。
これによって以下の問題が生じます。
- staticなconfigだとapp側がスケールしたタイミングでExternal APIへの流入が変化してしまう
- 特定のappにリクエストが偏ると、External APIにまだ余裕があってもCircuit BreakerがOpenしてしまう
これらの問題を考慮して使用する必要があります。
まとめ
EnvoyのCircuit Breakerを外部サービスへのリクエストに対して設定し、外部サービスがスローダウンしてもリクエストがスタックしないで済むアーキテクチャにできました。