Istio Rate Limitを活用した流量制限の取り組み

Istio Rate Limitのロゴ

はじめに

こんにちは、ECプラットフォーム基盤SREブロックの織田と、カート決済SREブロックの遠藤です。

本記事では、Istio Rate Limitの概要とZOZOTOWNでの導入事例を2つご紹介します。

目次

現在のZOZOTOWN

ZOZOTOWNではオンプレミス環境からクラウドへの大規模なリプレイスを行っており、クラウド移行と共にモノリシックな開発からマイクロサービス開発への移行を推進しています。クラウド、マイクロサービスへの移行では、ストアドプロシージャからの脱却やマイクロサービスごとにDBを分離するなどの対応を実施しています。

また、サービスメッシュの導入も並行して進めており、マイクロサービスの基盤にはKubernetes、サービスメッシュにはIstioを採用しています。 マイクロサービス化への取り組みについては、以前イベントを行っており、そのまとめとしてTECH BLOGを投稿しています。ご興味のある方はご覧ください。

techblog.zozo.com

Istioに関する取り組みについてもTECH BLOGを投稿しています。

techblog.zozo.com techblog.zozo.com

Istio Rate Limitの概要

Istio Rate Limitとは?

Istio Rate Limitは、Istioの機能の一部で特定のエンドポイントやAPIなどに対するリクエスト数を制限できる機能です。

リクエスト数を制限することで急激なトラフィックの増加によるアプリケーションやバックエンドサービスの過負荷を防ぎ、サービスの安定性の向上が実現できます。Rate LimitはIstio独自のものではなく、ネットワークトラフィックを制限することを意味する単語として利用されていることが多いです。

仕組み

Envoyのネイティブレートリミットを利用し、EnvoyFilterで動的にトラフィックを制限します。ここでは、外部リクエストと内部リクエストに対するRate Limitの仕組みを簡単に説明します。

まずは、外部リクエストについてです。外部からのリクエストに対してIstio Rate Limitがどのような通信をしてトラフィックを制御するか説明します。

external-istio-ratelimit

  1. 外部からのリクエストがIngress Gateways(istio-proxy Container)に届く
  2. ingressgatewayからRate Limitにリクエストを行う
  3. Rate LimitがRedisに書き込みを行い、ingressgatewayにレスポンスを返す
  4. 設定ファイル(ConfigMap)に定義されている制限に引っかかっていればingressgatewayが429を返し、制限に引っかかっていない場合はアップストリームにトラフィックが流れる

次に内部リクエストについて説明します。外部リクエストと基本的に同じですが、リクエスト元やRate Limitに問い合わせするリソースが異なります。

internal-istio-ratelimit

  1. Service Pod内のService Containerからサイドカーであるistio-proxy Containerにリクエストが届く
  2. istio-proxyからRate Limit Podにリクエストを行う
  3. Rate Limit PodがRedisに書き込みを行い、istio-proxyにレスポンスを返す
  4. 設定ファイル(ConfigMap)に定義されている制限に引っかかっていればistio-proxyが429を返し、制限に引っかかっていない場合はアップストリームにトラフィックが流れる

Redisに書き込まれるkeyとvalueは、<domain>_<key>_<value>_<unix-time>とリクエストカウントの組み合わせとなります。 domain、key、valueは、Rate Limitの設定ファイルに定義している値が使われます。Redisに書き込まれるkeyは、GenerateCacheKeyで生成され、以下のように設定ファイルを定義している場合、global-ratelimit_generic_key_zozotown_1687238460となります。

domain: global-ratelimit
descriptors:
  - key: generic_key
    value: zozotown
    rate_limit:
      unit: second
      requests_per_unit: 1

どのような制限ができるのか?

メッシュ内の全トラフィックに対して制限可能です。例えば、内部サービスAから内部サービスBへのリクエストや内部サービスCから外部サービスDへのトラフィックなど様々なケースに対して制限を設定できます。

ただ、外部へのリクエストに対して制限を設ける場合は、Service Entryを作成する必要があります。Service Entryを追加することで外部サービスを論理的にメッシュ内に取り込み、メッシュ内サービス同様にアクセスやルーティングできるようになります。

制限を設定できる範囲としては、Global Rate LimitLocal Rate Limitがあります。両者の違いは、以下のようになります。

  • Global Rate Limitは、メッシュ全体へ制限を適用します。外部のインメモリーデータストアにデータを格納し、各ワークロード(Pod)から書き込みや参照が行われます。
  • Local Rate Limitは、ワークロード(Pod)毎に制限を適用します。サイドカーとしてinjectされたistio-proxyごとにインメモリーでデータを持ちます。ワークロードごとに制限がされているため、ワークロードが増える度にアップストリームへの制限は緩くなります。

Rate Limitの評価は、Local Rate Limit、Global Rate Limitの順で実行されます。そのため、両者を組み合わせて利用することでGlobal Rate Limitの負荷を軽減できます。

制限可能な単位は、Rate limit definitionに明記されていて1秒、1分、1時間、1日です。例えば、1分間に100リクエストまで通常通り処理し、101リクエスト目以降は固定のレスポンスコードを返すというようなことができます。

1秒間で5リクエストの制限を設定できますが、2秒間で10リクエスト、3秒間で15リクエストなどの制限は設定できません。1秒間で5リクエストの設定にしておけば、2秒間で10リクエスト、3秒間で15リクエストの制限になりそうですが、実際の挙動は異なるので細かな設定が必要な場合は注意が必要です。

ただ、設定できる単位などについてはユーザの需要に基づいて機能が追加されていくとのことなので、issueを立てたりPRを作成してみると良いかもしれません。

このように柔軟性が高いため、マイクロサービス環境でのトラフィック制限を簡単かつ効果的に実現可能です。

Circuit Breakerとの違い

Rate Limitはトラフィック制限、Circuit Breakerは障害回避とタイムアウト制御のためのもので、両者ともに異なる問題に対するアプローチです。

Circuit Breakerは、あるサービスの障害を検知した場合に通信を遮断し、サービスの復旧を検知すると通信を復旧させる仕組みです。特定マイクロサービスの障害を検知した場合にそのマイクロサービスへの通信を遮断することによって、1つの障害が連鎖的な障害となるカスケード障害を避けることができます。

ZOZOTOWNでは、Istio Circuit Breakerについても採用しています。

techblog.zozo.com

導入事例

ここからはシステムへの導入事例を2つ紹介します。両事例ではメッシュ全体に制限を適用させ、ワークロードの増減に影響されずアップストリームへのリクエスト制限を一定にしたかったため、Global Rate Limitを採用しました。

  1. マイクロサービスから外部APIへのリクエスト制限
  2. オンプレミス環境のリクエスト制限

事例1. マイクロサービスから外部APIへのリクエスト制限

事例1では、マイクロサービスから外部APIへのリクエスト制限を導入した事例について紹介します。

2023年5月頃に利用者の年齢に応じたマーケティング施策を実施可能にするため、会員情報を扱うマイクロサービス(以降、会員基盤)で年齢認証の機能をリリースしました。実際に2023年6月1日から6月30日の間で、年齢認証を利用したマーケティング施策を実施しています。

年齢認証の機能では、本人確認を実施するために外部サービスLIQUID eKYCを利用しており、LIQUID社からプレスリリースも出されています。

liquidinc.asia

LIQUID eKYCは、AI審査で本人確認業務を自動化するサービスで、撮影画像の品質をチェックする画像処理技術や本人確認書類の文字を読み取るOCR技術などの精度の高さが特徴です。

会員基盤からLIQUID eKYCへのリクエスト数に制限を設けるため、Rate Limitを導入しました。制限を設けずに年齢認証が必要な現在行われている施策などを実施した場合、LIQUIDへのリクエストがスパイクし過負荷になってしまう可能性があるためです。

構成

会員基盤周辺の構成図は、以下のようになっています。

zozo-member-api-archtecture

会員基盤からLIQUIDへは、Service Entryでメッシュ内のサービスがLIQUIDにアクセスやルーティングできるようにし、Virtual Serviceでルーティングの設定を行い、Destination Ruleでルーティングが発生した際にトラフィックに適用されるポリシーを設定します。EnvoyfilterとRate Limit ConfigMapは、どのようなリクエストに対してどのような制限をするか設定します。

Rate Limitサービスはマイクロサービスと同じEKSにデプロイし、RedisはAWS ElastiCache for Redisを利用しています。現在、Rate Limitを利用しているマイクロサービスは多くないため、Rate LimitサービスとRedisはKubernetes Clusterごとに共通のものを使用しています。

設定と計測のポイント

会員基盤へ導入するにあたって実施したことを2つほど紹介します。

1. レイテンシの計測

Rate Limitを導入することでレイテンシが悪化してしまうというような懸念も考えられます。私達は普段からAPIの性質に応じて目標レイテンシを設定しているため、今回のような外部APIをコールするAPIに対しても目標を設定しRate Limitの有無でどのような変化があるか確認しました。

実際に計測した方法は、Rate Limitの有無がAPIのレイテンシにどのように影響を与えるかというシンプルな方法です。EnvoyFilterの作成、削除でRate Limitの有無を設定できるため、とても容易に計測できました。

計測結果としては、Rate Limitを設定していた場合に約30msほどレイテンシが高くなるという結果となりました。マイクロサービスやインフラの構成によって差異が発生するため参考程度にしていただければと思います。

2. 1つのAPIに対して2重の制限を設定

LIQUID APIへのリクエストに対して1秒と1分単位の制限を2重で設定しています。ここでは、EnvoyFilterとRate Limit configの設定例を紹介します。

まずはEnvoyFilterです。特定のマイクロサービスからZOZOTOWNへのリクエストに対してRate Limitで制限するための設定になります。

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: zozotown
spec:
  configPatches:
...
    - applyTo: HTTP_ROUTE
      match:
        context: SIDECAR_OUTBOUND
        routeConfiguration:
          vhost:
            name: zozo.jp:443
            route:
              name: zozotown
      patch:
        operation: MERGE
        value:
          route:
            rate_limits:
              - actions:
                  - generic_key:
                      descriptor_value: zozotown-per-second
              - actions:
                  - generic_key:
                      descriptor_value: zozotown-per-minute

設定項目の説明をするとApplyToは、パッチを適用する対象を指定します。

Match(EnvoyConfigObjectMatch)は、パッチを適用する条件を指定します。SIDECARからHTTP_ROUTEのvhost(zozo.jp:443)に定義されているroute(zozotown)に対するOUTBOUNDトラフィックに適用されます。SIDECARに設定されているルートコンフィグの確認は、istioctl proxy-config route <pod name>で確認可能です。

Patchは、対象の扱い方を指定します。マッチしたリクエストに対してRate Limitで制御するためにdescriptorを付与しますが、どのような値を付与するかはdescriptor_valueに定義します。1秒間の制御に利用するzozotown-per-secondと1分間の制御に利用するzozotown-per-minuteを付与する設定となっています。

次にRate Limitの設定ファイルです。

domain: global-ratelimit
descriptors:
  - key: generic_key
    value: zozotown-per-second
    rate_limit:
      unit: second
      requests_per_unit: 10
  - key: generic_key
    value: zozotown-per-minute
    rate_limit:
      unit: minute
      requests_per_unit: 100

Rate Limitが制御する際に参照する設定ファイルのdescriptorsは、上記のように定義します。この例ではEnvoyFilterで定義したdescriptor_value(設定ファイル内では、value)を利用し、1秒間に10回、1分間に100回という制限を設定しています。

このようにすることで特定のマイクロサービスからZOZOTOWNへのリクエストに対して1秒間に10回、1分間に100回という制限を設けることができます。

事例2. オンプレミス環境のリクエスト制限

次に、オンプレミス環境の注文処理に対してRate Limitを導入した事例をご紹介します。

2023年6月現在、注文処理はオンプレミス環境上のレガシーシステム(Classic ASP)で稼働していますが、高負荷時のボトルネックとして顕著になってきたため、リプレイスを待たずに流量制限の仕組みを導入することになりました。

構成

この事例の特徴は、Istio Rate Limitを使って、サービスメッシュ外のオンプレミス環境で稼働するアプリケーションの処理を制御している点です。

注文処理RateLimit概要図

  1. オンプレミス環境の注文処理アプリケーションにリクエストがあると、Rate Limit用エンドポイントにリクエストを送ります。
  2. そのエンドポイントに対してRate Limitが設定されており、閾値を超えた場合はHTTPステータス429を返し、閾値を超えない場合はDummy Web ServerがHTTPステータス200を返します。
  3. 注文処理アプリケーションはそのHTTPステータスに従い、429の場合はカート画面にリダイレクトします。それ以外の場合は後続処理へ進み、SQLServerに接続します。

Rate Limit用エンドポイントであるDummy Web Serverは、サービスメッシュ内の空のAPIであり、常にHTTPステータス200を返します。

設定と計測のポイント

1. 商品別の注文リクエスト数制限

ZOZOTOWNの注文確定時の処理に対して、2段階の制限をかけています。

  • 注文処理の全リクエスト数に対する制限
  • 商品ID単位のリクエスト数に対する制限

同一商品へのリクエストが集中することで、データベースの同一ページに対するラッチ競合が発生します。 これを防ぐために商品ID単位での制限値も設ける必要がありました。

仕組みとしては、クライアント側でリクエストヘッダーに商品ID(以下goods-id)を付与し、Istio Rate Limitでヘッダーの値ごとにカウントすることで実現しました。基本的な設定内容は事例1と同じですが、request_headersでヘッダー名を指定することで、ヘッダーの値ごとにカウントできます。

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: zozotown
spec:
  configPatches:
...
    - applyTo: HTTP_ROUTE
      match:
        context: ANY
        routeConfiguration:
          vhost:
            name: <FQDN>:80
            route:
              name: zozotown
      patch:
        operation: MERGE
        value:
          route:
            rate_limits:
              - actions:
                  - generic_key:
                      descriptor_value: zozotown-order-count-by-goodsid
                  - request_headers:
                      header_name: goods-id
                      descriptor_key: goods-id

2. 閾値決定のための負荷試験

以下のように設定することで、全体数の閾値とgoods-idごとの閾値を設定できました。

descriptors:
  - key: generic_key
    value: zozotown-order-count
    rate_limit:
      unit: second
      requests_per_unit: 200
  - key: generic_key
    value: zozotown-order-count-by-goodsid
    descriptors:
      - key: goods-id
        rate_limit:
          unit: second
          requests_per_unit: 100

これらの閾値を決めるために負荷試験を実施しました。

まずは単体負荷試験として、Rate Limit Podの処理性能を確認しました。Rate Limit Podは今後複数の機能で共通利用していく想定のため、キャパシティ計画を立てやすいように1Podあたりが処理できるリクエスト数を明確にしました。

次に、Rate Limitが発動しないように閾値を非常に大きくした状態で、注文処理の結合負荷試験を実施しました。

徐々に注文処理のリクエスト数を増やし、データベースでページラッチが発生してパフォーマンス影響が出たところを限界値とし、それよりも小さい閾値でRate Limitの閾値を設定しました。

余談ですが、Rate Limitを発動させたくない場合は、shadow_modeを有効化することで透過モードとなり、Rate Limitの閾値を超えても常にOKと判定されるようになります。

パフォーマンスについても、Rate Limit Pod単体でのレイテンシは5ms程度で、上記の構成によるオンプレミス環境側へのレスポンスも20ms~30ms程度と短く、注文機能として許容できるものでした。

今後の展望

リプレイスが進む中でシステムのキャパシティは変わっていくため、今後もRate Limitの閾値を調整していくことが考えられます。

現時点で基本的な監視は導入済みで、各Podの負荷状況、Rate Limit用エンドポイントへの総リクエスト数、Limitを超過したリクエスト数を監視しています。

課題として、ヘッダーのgoods-id別でカウント数を可視化する必要があり、監視の改善を進めています。

envoy proxyのドキュメントによると、detailed_metricを有効化することで、今回のようにヘッダー値を指定しない場合でもヘッダー値をmetricsに含めることができるそうです。

最終的には他のIstioのメトリクスと同じようにDatadogで可視化できないかを検証中です。

また、現在カート投入のリクエストに対しても同じ対応を進めており、Istio Rate Limitの機能を横展開する予定です。

感想

EnvoyFilterの利用が初めてで普段読み慣れているIstio公式ドキュメントのEnvoyFilterには詳細な情報があまりなかったため、Envoyのドキュメントを読みながら試行錯誤するのが大変でとても苦労しました。

Istio Rate Limitは柔軟で詳細な設定ができ、様々なケースに対応できそうだと感じました。また、パフォーマンスや安定性は非常に高く、満足のいく構成となりました。

最後に

現在、私たちと共にサービスを支える方を募集しています。少しでもご興味のある方は、以下のリンクからぜひご応募ください。

hrmos.co

カテゴリー