APIゲートウェイの「門番」を極める:レートリミットとスロットリングの深淵
こんにちは。現場で叩き上げのインフラエンジニアをしていると、必ず一度は「APIがなぜか503で死ぬ」「特定のクライアントにリソースを食いつぶされる」という悪夢に出くわします。
そんな時、我々ネットワークスペシャリストが真っ先に目を向けるのが「レートリミット(Rate Limiting)」と「スロットリング(Throttling)」です。これらは単なる設定項目ではありません。バックエンドの堅牢性を担保し、サービス全体のQoS(Quality of Service)を維持するための、いわば「デジタルな治安維持機構」なのです。
今回は、APIゲートウェイにおけるこれらの制御アルゴリズムの正体と、現場で「動く」設計の勘所を紐解いていきましょう。
—
なぜレートリミットが必要なのか?
REST APIの原則において、ステートレス性は美徳ですが、それはリソースが無限であることを意味しません。無制限なアクセスを許せば、一人の悪意ある(あるいは設定ミスをした)クライアントがバックエンドのDBを枯渇させ、全ユーザーに影響が出る。これはエンジニアとして絶対に避けなければならない事態です。
RFC 6585で定義された 429 Too Many Requests は、単なるエラーではなく、「今はこれ以上受け付けられないから、少し落ち着いてから来てくれ」という、サーバーからの切実なメッセージなのです。
—
現場で選ぶべきアルゴリズム:トークンバケット vs リーキーバケット
APIゲートウェイ(Nginx, Kong, AWS API Gateway等)で一般的に採用されているのは、以下の二つのアルゴリズムです。
1. トークンバケット(Token Bucket)
バケット(バケツ)の中に一定のレートで「トークン」が追加され、リクエストごとにトークンを消費する方式です。
- 強み: 「バースト」を許容できる点です。バケツが満タンなら、短時間に連続したリクエストを捌けます。ユーザー体験を損なわないため、多くのWeb APIで標準的に採用されます。
2. リーキーバケット(Leaky Bucket)
穴の空いたバケツに水が注がれるイメージです。一定の速度でしかリクエストを処理しません。
- 強み: 出力レートが完全に一定になります。バックエンドの処理能力が物理的に決まっている場合に有効ですが、スパイクに対応できないため、柔軟性に欠ける側面があります。
—
実践:Nginxによるレートリミット設定
現場で最も汎用的なNginxを用いた実装例を見てみましょう。ここでは「IPアドレスごとに1秒間あたり10リクエストまで、バーストは20まで許容」という設定を組んでみます。
# httpブロックで共有メモリゾーンを定義
# 10MBの領域を確保し、'api_limit'という名前で管理
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
location /api/ {
# burst=20: 溢れたリクエストを20個までキューイングする
# nodelay: キューイングされた分を即時処理する(レスポンス遅延を防ぐ)
limit_req zone=api_limit burst=20 nodelay;
# 制限に達した際のエラーコードを429に変更(デフォルトは503)
limit_req_status 429;
proxy_pass http://backend_cluster;
}
}
この設定のポイントは、nodelay を指定している点です。キューイングでレスポンスを待たせると、TCPのコネクションが滞留し、結果としてバックエンドのFD(ファイルディスクリプタ)を枯渇させる原因になります。「遅延させる」のではなく「即座に拒否する」のが、高負荷対策の鉄則です。
—
クライアント側の振る舞い:429を受け取ったらどうすべきか?
APIゲートウェイが 429 Too Many Requests を返したとき、優秀なクライアントは「即座にリトライ」してはいけません。RFC 6585では、以下のヘッダーを返すことが推奨されています。
Retry-After: あと何秒待てばリクエスト可能かを示す(秒数、またはHTTP日付)。
Python等でAPIクライアントを書く際は、必ずこのヘッダーをハンドリングしましょう。
import time
import requests
def call_api_with_retry(url):
response = requests.get(url)
if response.status_code == 429:
# Retry-Afterヘッダーがあれば取得、なければデフォルトの待機時間
retry_after = int(response.headers.get("Retry-After", 5))
print(f"Rate limited. Waiting for {retry_after} seconds...")
time.sleep(retry_after)
return call_api_with_retry(url) # 再帰的にリトライ
return response.json()
—
プロフェッショナルへのアドバイス
現場で遭遇するトラブルの多くは、この 429 への対応不足です。特にモバイルアプリやIoTデバイスで、バックグラウンド同期が同じタイミングで一斉に叩かれる「サンダリング・ハード(Thundering Herd)」問題には注意が必要です。
1. ジッター(Jitter)の導入: リトライ間隔を固定せず、ランダムな揺らぎ(例:2秒±0.5秒)を混ぜることで、サーバーへの負荷集中を分散させてください。
2. メトリクスの可視化: どのエンドポイントで 429 が多発しているかを常に監視(Prometheus + Grafana等)し、「正当なユーザーが制限にかかっていないか」を定期的にチューニングするのが、プロのインフラエンジニアの仕事です。
ネットワークは生き物です。アルゴリズムを理解し、パケットの流れを想像できるようになれば、あなたはもう単なる設定屋ではなく、サービスの「門番」として確固たる地位を築けるはずです。
それでは、また次回の深淵でお会いしましょう。
コメント