こんにちは。AbemaTV のバックエンドエンジニアのユシンです。本ポストでは、複数のマイクロサービスのアプリケーションコードに分散して管理されていた Ratelimit 機能について、運用の最適化を目的として yaml 設定ベースの共通 Ratelimit Middleware を導入した事例をご紹介します。

 

Ratelimitとは

Ratelimit とは、一定時間内に許可されるリクエスト数を制限することで、サービスの安定性および公平性を担保する仕組みです。特定のユーザーや API が過剰なトラフィックを発生させる状況を防ぎ、障害の波及やリソース枯渇からシステム全体を保護する役割を果たします。特にトラフィック規模が拡大するにつれて、Ratelimit は単なる防御手段にとどまらず、システムアーキテクチャの一部として、スケーラビリティや運用の安定性に直接影響を与える重要な要素となります。ABEMA では、ミッションクリティカルな API の保護や、異常なサービス利用、ならびに DDoS への対策として Ratelimit を運用しています。

 

ABEMAの Ratelimit システムアーキテクチャ

ABEMA では、多数のマイクロサービスが Kubernetes 上で稼働しており、各サービスへのリクエストは Gateway を経由してルーティングされています。Ratelimit についても、この構成を前提に、Gateway と Ratelimit サービスを分離したアーキテクチャを採用しています。各 Gateway には Ratelimit Middleware が組み込まれており、リクエストが流入すると、Middleware が Ratelimit サービスと RPC で通信し、当該リクエストを許可可能かどうかを判定します。Ratelimit サービスでは、リクエストに対するカウント情報を Redis などの外部ストレージに保存し、事前に定義されたポリシーに基づいて、許可または制限の可否を Gateway に返却します。このような構成により、Gateway の実装に依存することなく共通の Ratelimit ポリシーを適用でき、Ratelimit のロジックをアプリケーションコードから分離して中央集約的に管理できるようにしています。

 

Kubernetes(k8s)クラスタ内の構成図。クラスタ内には複数インスタンスの「gateway」サービスと「ratelimit」サービスが配置されている。gateway と ratelimit は RPC で双方向通信しており、ratelimit サービスはクラスタ外の Redis と接続し、カウンター情報を参照・更新してレート制限を行う構成を示している。

< 図 1 : ABEMA の Ratelimit システム構成 >

 

Ratelimit は一般的に Fixed Window、Token Bucket、Sliding Window などの方式で実装されており、ABEMA では Fixed Window Counter 方式を採用していました。Fixed Window Counter 方式は、一定時間単位でリクエスト数を集計し、そのウィンドウ内で事前に定義された上限を超過した場合にリクエストを制限する方式です。実装がシンプルで、Redis などの外部ストレージとの親和性が高く、大規模な分散環境においても安定して動作する点が利点として挙げられます。一方で、ウィンドウの境界付近にリクエストが集中した場合、一時的に burst トラフィックを許容してしまう可能性があるという課題も存在します。Token Bucket 方式は、一定のレートでトークンを補充し、リクエストごとにトークンを消費する仕組みで、瞬間的なトラフィックの増加をより細かく制御できます。また、Sliding Window 方式は、時間の経過に応じてリクエスト数を連続的に算出することで、Fixed Window における境界問題を緩和します。ABEMA では、分散環境におけるスケーラビリティと運用の安定性を重視し、比較的実装および運用が容易な Fixed Window Counter 方式を採用しました。

 

方式 動作方式 メリット デメリット
Fixed Window Counter 一定時間単位でリクエスト数を集計し、ウィンドウ内のリクエスト数が基準を超過した場合に制限 実装がシンプル
Redis などの外部ストレージとの親和性が高い
分散環境において安定的に動作
ウィンドウ境界のタイミングで burst トラフィックを許容する可能性がある
Token Bucket 一定のレートでトークンを補充し、リクエストごとにトークンを消費 burst トラフィック制御に強い
高精度なトラフィック制御が可能
実装の複雑度が高い
分散環境における状態管理が困難
Sliding Window 時間の経過に応じてリクエストを連続的に計算 Fixed Window の境界問題を緩和
より公平な制限が可能
計算コストが高い
ストレージおよびパフォーマンスへの負荷が大きい

< 表1 : Ratelimit アルゴリズム比較 >

 

Ratelimit Middleware の実装

Ratelimit を Envoy ベースで構成する場合、Service Mesh レベルで EnvoyFilter を活用する方法も選択肢の一つです。しかし ABEMA では、Ratelimit ポリシーをインフラ全体に共通適用するネットワークレベルの制御に限定するのではなく、アプリケーションのリクエストコンテキストをより柔軟に反映できる構成が求められていました。そのため、サービス単位・エンドポイント単位・ビジネスコンテキストに応じた制限基準を明確に表現できるよう、Gateway の Middleware レベルで Ratelimit を実装しています。

 

Middleware Ratelimit Service Mesh Ratelimit
インフラ内のリクエスト処理フローを示す図。外部からのリクエストが gateway に送信され、gateway は ratelimit service に対してチェックを行う。ratelimit service は制限内であれば「OK」、超過していれば「OVER」を返す。gateway は結果に応じてクライアントへ HTTP 200 または 429 を返却する。 インフラ内におけるリクエスト処理の構成図。外部からのリクエストは直接 ratelimit service に送信され、ratelimit service は gateway とリクエスト/レスポンスをやり取りする。判定結果に応じて、ratelimit service はクライアントへ HTTP 200 または 429 を返却する流れを示している。

< 表 2 : Middleware vs Service Mesh Ratelimit 比較 >

 

 

Ratelimit の実装には、ABEMA では envoyproxy/ratelimit を採用しています。envoyproxy/ratelimit は Envoy と連携する分散環境向けの Ratelimit サービスで、アプリケーションから分離した形で Ratelimit ポリシーを管理できる点が特徴です。Service Mesh へのマイグレーションが容易であり、リクエストに対する制限可否を別サービスで判定する構成のため、各アプリケーションに Ratelimit ロジックを直接実装する必要がありません。設定ファイル(yaml)の変更のみでポリシーの追加や修正が可能となります。このような構成により、サービス数やトラフィックが増加する環境においても Ratelimit ポリシーの一貫性を維持でき、運用負荷の低減につながります。

 

クライアントからのリクエストに対するレートリミット処理のシーケンス図。クライアントが HTTP または gRPC リクエストを Gateway に送信し、Gateway は Ratelimit Middleware にリクエストを転送する。Middleware はリクエストのパスやメソッドなどのコンテキストを抽出し、descriptor を用いて Ratelimit Service に CheckRateLimit を呼び出す。Ratelimit Service は Redis(カウンター)に対してカウントのインクリメントや期限設定を行い、現在のカウンター値を取得する。判定結果として「OK」または「OVER_LIMIT」が返され、Gateway はその結果に基づいてリクエストを処理するか、HTTP 429 をクライアントに返却する。

 

  1. Client → Gateway → Ratelimit Middleware へのリクエスト送信:クライアントは HTTP または gRPC リクエストを Gateway に送信します。この時点では通常の API リクエストと同様であり、Ratelimit の可否はまだ判定されていない状態です。Gateway では、まずメトリクスなどを生成したうえで Ratelimit Middleware を通過する構成となっており、実際のバックエンドサービスへ転送される前に、当該リクエストが制限対象かどうかを確認します。

  2. Ratelimit Middleware におけるリクエストコンテキストの抽出:Ratelimit Middleware は、リクエストから Ratelimit 判定に必要なコンテキスト情報を抽出します。HTTP の場合は IP、HTTP Method、Path などを、gRPC の場合は RPC Method 名や認証情報などを基に、ABEMA のポリシーに応じた制限基準となる Descriptor を生成します。

  3. Ratelimit Service による判定およびカウンター処理:Ratelimit Service は、リクエストコンテキストおよび Descriptor を基に、Redis に保存されたカウンターを参照します。Fixed Window Counter 方式に従い、該当キーに対して INCR を実行し、制限可否を判定したうえで、OK または OVER_LIMIT のレスポンスを Ratelimit Middleware に返却します。
  4. Middleware および Gateway におけるリクエスト処理、または 429 応答の返却:Middleware および Gateway では、Ratelimit Service の判定結果に応じてリクエストをそのまま処理するか、429(Too Many Requests)レスポンスを返却します。

 

func GrpcInterceptor(...) error {
	req := RateLimitRequest{
		Domain:      domainName,
		Descriptors: buildDescriptorFromContext(ctx),
	}

	res, err := ratelimitService.ShouldRateLimit(ctx, req)

	if err != nil {
		return invoker(...)
	}

	if res.Code == OVER_LIMIT {
		return ResourceExhausted("rate limit exceeded")
	}

	return invoker(...)
}

 

上記のコードは、gRPC Gateway 上で動作する Interceptor のフローを簡略化して表現した例です。Interceptor は、リクエストが実際にバックエンドへ転送される前に実行されます。Ratelimit のリクエストは RPC を通じて送信され、Ratelimit Service の呼び出し中にエラーが発生した場合でも、Ratelimit の失敗がシステム全体の障害につながらないよう、Ratelimit を適用せずにリクエストをそのまま処理する設計としています。レスポンス結果が OVER_LIMIT の場合は、gRPC では ResourceExhausted エラーを返却してリクエストを遮断し、HTTP では 429 レスポンスを返却するよう実装しています。

 

Descriptor と YAML 設定

このプロセスにおいて最も重要な概念の一つが、Descriptor です。Descriptor は「どのような基準でリクエストを制限するか」を構造的に表現する単位であり、Key–Value ペアの階層構造として定義されます。これにより、「どの基準でリクエストをグルーピングしてカウントするか」を明確に示す役割を担います。例えば IP という Descriptor を定義することで、ABEMA API に対するリクエストを 1 分間あたり 100 回までに制限することが可能となります。

 

domain: api
descriptors:
  - key: ip
    rate_limit:
      unit: minute
      requests_per_unit: 100

 

もし Path、Method、ユーザー ID の Descriptor を用いて制限を行う場合、以下のような構造で制御することが可能です。次の例は、POST /some/path に対するリクエストを、ユーザーごとに 1 分あたり 10 回までに制限する例となります。

 

domain: api
descriptors:
  - key: path
    value: /some/path
    descriptors:
      - key: method
        value: POST
        descriptors:
          - key: user
            rate_limit:
              unit: minute
              requests_per_unit: 10

 

ABEMA では、異常なサービス利用や攻撃的なトラフィックへの防御を目的とした Ratelimit と、ビジネス上の文脈においてミッションクリティカルな API に対して、より細かな制約を設けるための Ratelimit を明確に区別しています。そのうえで、Descriptor を組み合わせたポリシーを中心に設定し、ユースケースに応じた柔軟な制御を行っています。Descriptor の設計については、チーム内で一定のポリシーを設け、無秩序に増えないよう運用しています。

 

クライアントとレートリミット処理の関係を示す概念図。クライアントからのリクエストは Ratelimit コンポーネントに送られ、内部で複数の Descriptor Combination(A、B、C)が順に評価される。各ディスクリプタの組み合わせに基づいてレート制限が判定され、その結果がクライアントに返却される構造を表している。

 

結論

ABEMA では、YAML ベースの共通 Ratelimit Middleware を導入することで、Ratelimit ポリシーをアプリケーションコードから分離し、中央集約的に管理できるようになりました。これにより、単純な制限値の調整やポリシー追加のたびに、繰り返しコード修正やデプロイを行っていた従来の運用負荷を大幅に削減しています。現在、Ratelimit サービスは P999 で 20ms のレイテンシを維持しており、Descriptor ベースのポリシー定義を通じて、異常なサービス利用や攻撃的なトラフィックに対して迅速に防御できる手段を確保しています。同時に、ビジネス上ミッションクリティカルな API に対しては、より細かな制約を適用することが可能となっています。これらの取り組みにより、サービスの安定性とユーザー体験の双方を考慮した Ratelimit 運用を実現できました。今後も ABEMA では、サービスの成長に伴って変化するトラフィック環境に対応するため、Ratelimit 基盤を継続的に改善していく予定です。お読みいただき、ありがとうございました。

 

Reference
  • https://github.com/envoyproxy/ratelimit
  • https://learn.microsoft.com/en-us/azure/architecture/patterns/rate-limiting-pattern
  • https://bytebytego.com/courses/system-design-interview/design-a-rate-limiter
  • https://www.geeksforgeeks.org/system-design/rate-limiting-in-system-design/