🔥

Istio で Envoy Global rate limiting を使う際に知っておくと良い 5 つのこと

2024/12/19に公開

本記事は、CyberAgent Group SRE Advent Calendar 2024 の 19 日目の記事になります。

Envoy Globa rate limiting は分散システムにおけるリクエストの Rate Limit を一元的に管理するための機能です。
Envoy proxy がリクエストの流量を制御し、システム全体の負荷を管理するために使用されます。
下記の記事にもあるように、私の所属する株式会社CAMでも、この機能を活用しリクエストの流量制限を実現しています。
https://developers.cyberagent.co.jp/blog/archives/41989/

今回の記事では先述した Envoy Global rate limiting を Istio がインストールされた Kubernetes クラスタに導入し、活用していくために知っておくと良い 5 つのことを紹介します。

1. ルートレベルで Rate Limit Service の宛先は変更できない

Istio で Envoy Global rate limiting を使用するには、下記のような Envoy FilterListener を変更する必要があります。

下記の例では、EnvoyFilter を root namespace である istio-system に workloadSelector を指定せずに適用しているため、メッシュ全体の Listener に対して変更を加えています。

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: istio-proxy-sidecar-outbound-http-filters
  namespace: istio-system
spec:
  configPatches:
    - applyTo: HTTP_FILTER
      match:
        context: SIDECAR_OUTBOUND
        listener:
          filterChain:
            filter:
              name: "envoy.filters.network.http_connection_manager"
              subFilter:
                name: "envoy.filters.http.router"

      patch:
        operation: INSERT_BEFORE
        value:
          name: envoy.filters.http.ratelimit
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
            domain: global-rate-limit-istio-proxy-sidecar-outbound
            failure_mode_deny: false # ratelimit service が動作していなくても、リクエストは通すため
            timeout: 1s
            rate_limit_service:
              grpc_service:
                envoy_grpc:
                  cluster_name: outbound|8081||istio-ratelimit.istio-system.svc.cluster.local
                  authority: istio-ratelimit.istio-system.svc.cluster.local
              transport_api_version: V3

この際、Rate Limit Service の宛先は cluster_name で指定しますが、ルートレベルでの設定はできません(参考)。

例えば、サービス A の /foo は Rate Limit Service A に、/bar は Rate Limit Service B に、といったような設定はできません。

もっと言うと、先述した設定例のようにメッシュ全体の Listener に対して Rate Limit Service を設定する場合、サービス A と サービス B が依存する Rate Limit Service を分割し、影響範囲の極小化を行うといったことができません。

これの何が困るかというと、Rate Limit Service が詰まってしまった場合、Kubernetes クラスタで稼働していて、Rate Limit が有効になっている全てのサービスが影響を受けてしまうということです。

予期しない突発負荷が発生するような環境で、マルチテナントな Kubernetes クラスタを構築している場合、ノイジーネイバー問題を避けるためにも、Rate Limit Service の設定には注意が必要です。

適切な単位で分離(例えば Namespace レベル)できるような設計を始めから考えておくのがおすすめです。

2. Rate Limit はサイドカーのアウトバウンド通信で行う

Istio で Envoy Global rate limiting を使用する場合、Rate Limit はサイドカーのアウトバウンド通信で行う事を推奨します。

理由は、VirtualService で作成する route はサイドカーのアウトバウンドルートに適用されるためです。

例えば、service-aservice-b が Pod として存在する場合に、下記のような VirtualService を作成したとします。

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: service-a
  namespace: default
spec:
  hosts:
    - service-a.default.svc.cluster.local
  gateways:
    - mesh
  http:
    - name: "request-to-api"
      match:
        - uri:
            prefix: "/api"
      route:
        - destination:
            host: service-a.default.svc.cluster.local
            port:
              number: 8080
      headers:
        request:
          set:
            x-request-to: "api"
    - name: "request-to-ui"
      match:
        - uri:
            prefix: "/ui"
      route:
        - destination:
            host: service-a.default.svc.cluster.local
            port:
              number: 8080
      headers:
        request:
          set:
            x-request-to: "ui"

この設定は、service-a のインバウンドルートには適用されず、service-a/service-b のアウトバウンドルートに適用されます。

istioctl pc r service-a-xxxxx-xxxxx で Envoy proxy の設定を確認すると inbound|8080|| のようなインバウンドのルートには VirtualService の設定が適用されていないことが確認できます。

- name: inbound|8080||
  validateClusters: false
  virtualHosts:
  - domains:
    - '*'
    name: inbound|http|8080
    routes:
    - decorator:
        operation: service-a.default.svc.cluster.local:8080/*
      match:
        prefix: /
      name: default
      route:
        cluster: inbound|8080||
        maxStreamDuration:
          grpcTimeoutHeaderMax: 0s
          maxStreamDuration: 0s
        timeout: 0s

このような挙動において、例えば下記のような EnvoyFilter を作成したとしても、service-a のインバウンド通信で Rate Limit が適用されることはありません。

インバウンド通信用の Listener を変更する EnvoyFilter。

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: service-a-inbound-http-filters
  namespace: default
spec:
  workloadSelector:
    labels:
      app: service-a
  configPatches:
    - applyTo: HTTP_FILTER
      match:
        context: SIDECAR_INBOUND # SIDECAR_INBOUND に設定することで、サイドカーのインバウンド通信用の Listener に適用される
        listener:
          filterChain:
            filter:
              name: "envoy.filters.network.http_connection_manager"
              subFilter:
                name: "envoy.filters.http.router"

      patch:
        operation: INSERT_BEFORE
        value:
          name: envoy.filters.http.ratelimit
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
            domain: global-rate-limit-istio-proxy-sidecar-outbound
            failure_mode_deny: false # ratelimit service が動作していなくても、リクエストは通すため
            timeout: 1s
            rate_limit_service:
              grpc_service:
                envoy_grpc:
                  cluster_name: outbound|8081||istio-ratelimit.istio-system.svc.cluster.local
                  authority: istio-ratelimit.istio-system.svc.cluster.local
              transport_api_version: V3

service-a の特定のルートで Rate Limit を有効にする EnvoyFilter。

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: service-a-inbound-rate-limits
  namespace: default
spec:
  workloadSelector:
    labels:
      app: service-a
  configPatches:
    - applyTo: HTTP_ROUTE
      match:
        context: SIDECAR_INBOUND
        routeConfiguration:
          vhost:
            name: "inbound|http|8080"
            route:
              name: request-to-api
      patch:
        operation: MERGE
        value:
          route:
            rate_limits:
              - actions:
                  - header_value_match:
                      descriptor_value: request-to-service-a-api
                      headers:
                        - name: ":authority"
                          exact_match: "service-a.default.svc.cluster.local:8080"
                        - name: ":path"
                          prefix_match: "/api/"

「VirtualService で設定した特定のルートに対してのみ Rate Limit を有効にしたい」みたいなユースケースがなければ、サイドカーへのインバウンド通信で Rate Limit を有効にすることは可能です。

しかし、Istio で Envoy Global rate limiting を使用する場合、個人的にはあまりおすすめできない手法かなと思います。

どうしてもインバウンドのルートにおいて特定のパスのみ Rate Limit を有効にしたい場合は、Envoy Filter での設定を行うことが可能です。

しかし、Envoy Filter は Alpha 状態のリソースであり、将来的には非推奨になる可能性があるため、やはり推奨される方法ではないかなと思います。

https://github.com/istio/istio/issues/35756#issuecomment-1176894839

3. istioctl コマンドの活用

Envoy Filter によって意図した設定が Envoy proxy に反映されているか確認するためには、istioctl コマンドを活用する必要があります。

例えば自分が意図した Listener が任意の Pod に適用されているか確認するためには、下記のようなコマンドを実行します。

istioctl pc l ${{POD_NAME}}

Route の場合は下記のようなコマンドを実行します。

istioctl pc r ${{POD_NAME}}

Rate Limit の設定確認においては 「Listener で Rate Limit フィルターが有効になっているか」と「Route で Rate Limit が有効になっているか」の確認をできる必要があるため、覚えておくと便利です。

ちなみに、Cluster の確認、BootStrap 設定の確認もできると Rate Limit の設定確認以外でも役に立ちます。

istioctl pc c ${{POD_NAME}}
istioctl pc bootstrap ${{POD_NAME}}

4. Rate Limit のアルゴリズムは Fixed Window

Envoy proxy が提供している Rate Limit Service のアルゴリズムは Fixed Window です。

Sliding Window ではないため、window が切り替わるタイミングなどにおいては自分達が想定した以上のリクエストが通過する可能性があります。

https://github.com/envoyproxy/ratelimit/issues/269#issuecomment-887331533

5. Fixed Window の算出方法は Unix Timestamp からの経過時間を基にする

Fixed Window のアルゴリズムにおいて、window の切り替わりは Unix Timestamp からの経過時間を基にしています。

https://github.com/envoyproxy/ratelimit/blob/6a2e8262874f012d08830cc34ba8058e66a33819/src/limiter/cache_key.go#L74

1秒や、1分、1時間などの単位であれば自分達が意図した window で Rate Limit が適用されるかと思いますが、例えば 1日などの長い時間単位で Rate Limit を設定する場合には注意が必要です。

Unix Timestamp からの経過時間を基に Rate Limit が適用される現在の実装においては、1日や1週間、1ヶ月などの長い間隔の制限を日本時間で意図した通りに設定することは難しいです。

そのため、長い時間単位で Rate Limit を「日本時間で厳密に」設定する場合には、自前で Rate Limit Service を構築するか、Envoy proxy の Rate Limit Service の実装をカスタマイズする必要があります。

まとめ

Istio で Envoy Global rate limiting を使用する際に知っておくと良い 5 つのことを紹介しました。

  • ルートレベルで Rate Limit Service の宛先は変更できない
  • Rate Limit はサイドカーのアウトバウンド通信で行う
  • istioctl コマンドの活用
  • Rate Limit のアルゴリズムは Fixed Window
  • Fixed Window の算出方法は Unix Timestamp からの経過時間を基にする

大分にっちな内容になってしまいましたが、自分が Istio で Envoy Global rate limiting を使用する際、あらかじめ知っておきたかったポイントをまとめてみました。

Istio がインストールされた環境で Envoy Global rate limiting の使用を考えている方にとって、参考になれば幸いです。

Discussion