【テクニカル・上級編】 レートリミットの設計:固定ウィンドウアルゴリズム – Web APIアーキテクチャ・データ連携実践ガイド

レートリミットの「固定ウィンドウ」はなぜ現場を焼き尽くすのか?――プロトコルレベルで紐解く設計の深淵

ネットワークエンジニアやアーキテクトがAPI設計を語る時、RESTの原則論ばかりに目を奪われがちだが、現場の現実はもっと冷徹だ。我々が守るべきは美しいリソース定義だけではなく、その裏側で呼吸するTCP/TLSセッションの整合性と、サーバーを死から守るための「流量制御」という泥臭い防波堤である。

今回は、最も単純にして最も危険なトラップを秘めた「固定ウィンドウ(Fixed Window)」アルゴリズムを、インフラの深淵から解剖していく。

固定ウィンドウの「境界問題」と物理的な代償

固定ウィンドウとは、「1分間に100リクエスト」という制約を、時計の針に合わせて機械的にリセットする手法だ。実装は極めて容易で、RedisのINCRとEXPIREだけで書ける。しかし、この「単純さ」がネットワークの平穏を脅かす。

バースト・パケットの衝撃

固定ウィンドウの最大の脆弱性は、ウィンドウの切り替わり境界で発生する「2倍のバースト」だ。
例えば、1分間の終了直前(59秒目)に50リクエストが押し寄せ、次の1分が始まった直後(0秒目)にまた50リクエストが押し寄せる。システム全体で見れば「1分間で100」という制約を守っているように見えるが、サーバー側から見れば、ミリ秒単位の極小時間で100リクエストを捌くことを強いられる。

これは、ロードバランサーの接続キューや、カーネルのbacklogキューを一気に溢れさせ、SYNパケットに対するRST(Reset)の嵐を招く。結果、APIは「429 Too Many Requests」を返す以前に、TCPハンドシェイクすら完了できない「接続タイムアウト」の地獄に陥るのだ。

パケットが語る「最適化」の真実

レートリミットを設計する際、単に「リクエスト数」を数えるだけでは不十分だ。パケットレベルの挙動を最適化しなければ、限られた帯域を有効に使えない。

TLSハンドシェイクとRTTの削減

APIがHTTPSである以上、TLSハンドシェイクのコストは避けられない。レートリミットに引っかかるようなリクエストを、ハンドシェイク完了後に「429」で弾くのはあまりにコストが高い。

  • TCP Fast Open (TFO): クライアントが過去に接続済みであれば、ハンドシェイクの初期段階でデータを送れる。これによりRTT(Round Trip Time)を削減し、リミット判定を早める。
  • TLS 1.3 0-RTT: 最初のパケットにアプリケーションデータを乗せる。ただし、リプレイ攻撃には十分な注意が必要だ。

これらの最適化を行う場合、レートリミットの判定は「アプリケーション層」ではなく、可能な限り「エッジ(WAFやNginxのlimit_reqモジュール)」で実行すべきである。

実装:Nginxで「境界」を制御する現実解

単なる固定ウィンドウで終わらせないためには、burstオプションとnodelayの制御が肝となる。これらはカーネルのTCPバッファと密接に関わっている。

# Nginxの設定例: 境界付近のバーストを物理的に抑制する
http {
    # ゾーンを定義(1分間に100リクエスト)
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/m;

    server {
        location /api/ {
            # burst=20: 溢れたリクエストを20個までキューイング
            # nodelay: キューを待たずに即時処理(バーストを許容)
            # ここを調整しないと、バックエンドのCPUが瞬時に飽和する
            limit_req zone=api_limit burst=20 nodelay;
            
            # TCPバッファの最適化(低レイテンシ環境向け)
            tcp_nodelay on;
            tcp_nopush on;
        }
    }
}

なぜこの設定が「設計」なのか

  • burst=20 は、固定ウィンドウの「切り替わり際」のバーストを物理的に吸収するためのバッファである。
  • nodelay を指定しないと、Nginxはキューイングされたリクエストを律儀に待たせる。これはUXを低下させるが、サーバー負荷を抑えるための「意図的な減速」としては有効だ。

セキュリティスペシャリストとしての警鐘

レートリミットは、単なる負荷制御ではない。DoS攻撃に対する第一線だ。
もしあなたが「固定ウィンドウ」の境界問題を放置すれば、攻撃者はウィンドウの切り替わりタイミングを正確に狙い撃ちし、あなたのAPIをピンポイントで麻痺させることができる。

プロダクション環境への提言

1. スライディングウィンドウへの移行: 固定ウィンドウではなく、スライディングウィンドウログやトークンバケットアルゴリズムを採用せよ。これらは時間の「境界」を滑らかにし、バーストの衝撃を分散させる。
2. ヘッダー圧縮の影響を考慮: HTTP/2以降ではHPACKが使われる。頻繁にレートリミット情報を返す場合、カスタムヘッダー(X-RateLimit-Remaining等)の動的な生成がCPUに与える負荷も無視できない。
3. TCPバッファの監視: ss -nt コマンドで、送信待ちキューが常に溢れていないか監視せよ。レートリミットが機能しているはずなのに接続が切れる場合、それはリミットの問題ではなく、OSレベルのTCPパラメーター(net.ipv4.tcp_max_syn_backlog 等)の限界である可能性が高い。

結局のところ、優れたAPI設計とは「コードの美しさ」ではなく、「ネットワークという不安定な媒体の上で、いかにして壊れないシステムを構築するか」という執念の産物なのだ。次回のデプロイ前には、ぜひ tcpdump を走らせ、パケットたちがどのようにしてあなたの「リミット」にぶつかっているのか、その目で確かめてみてほしい。

コメント

タイトルとURLをコピーしました