レート制限と総当たり対策のアルゴリズム原理
なぜ境界バーストが2倍通り、分散環境でカウンタがずれるのか。トークンバケットからスライディングウィンドウまでの挙動差と一貫性、クレデンシャルスタッフィングへの実効的な防御を原理から設計できるようになります。
- 1.トークンバケットは平均速度を保ちつつ容量ぶんのバーストを許す。リーキーバケットは出力を完全に平滑化する。両者は数学的に双対だが、許すバースト形状が異なる。
- 2.固定ウィンドウは境界で上限の2倍が通る欠陥がある。スライディングウィンドウログは正確だがメモリを食い、スライディングウィンドウカウンタは前区間を加重平均してメモリ一定で近似する。
- 3.分散環境では各ノードが別カウンタを持つとずれるため共有ストアへ原子的操作で集約する。総当たり対策はIP単位だけでは破られ、クレデンシャルスタッフィングにはデバイス指紋・PoW・CAPTCHA を多層で重ねる。
レート制限を「アルゴリズム」として見る
レート制限の本質は、到着するイベント列を受理するか拒否するかを、過去の到着履歴に基づいて決めるオンライン判定問題です。設計者が選ぶアルゴリズムは、(1) どれだけのバーストを許すか、(2) どれだけのメモリと計算量を要するか、(3) 分散環境でどれだけ正確に協調できるか、という3軸でトレードオフします。基礎的な使い分けは レート制限 で扱ったので、本稿は各方式の内部挙動・誤差・分散一貫性、そして総当たりへの実効的な防御を原理から掘り下げます。
まず「レート」には2つの異なる量が混ざることを区別します。平均レート(長期的な毎秒あたりの上限)とバースト許容量(瞬間的にまとめて通せる量)です。アルゴリズムの個性は、この2つをどう独立に制御できるかに現れます。
トークンバケットとリーキーバケット:双対だが別物
トークンバケットは、容量 capacity のバケツに毎秒 rate 個のトークンを補充し、リクエスト1件ごとに1個消費します。空なら拒否します。重要なのは、トークンを毎回タイマーで足すのではなく、判定時に経過時間から遅延計算する点です。これで時間刻みのタイマーが不要になり、ストレージには「最後の補充時刻」と「残トークン数」の2値だけを置けます。
def allow(state, rate, capacity, now):
# 前回判定からの経過ぶんを補充。上限は capacity でクランプ
elapsed = now - state.ts
state.tokens = min(capacity, state.tokens + elapsed * rate)
state.ts = now
if state.tokens >= 1:
state.tokens -= 1
return True # 受理
return False # 拒否(429)
このとき許されるバーストは「補充が追いつかない短時間に最大 capacity 件」です。平均は rate に収束し、瞬間集中は capacity まで許す。2つのパラメータが直交して効くのがトークンバケットの強みです。
リーキーバケットは発想が逆で、出力を一定速度に固定します。到着はキュー(バケツ)に溜まり、底の穴から毎秒 rate 件だけ流れ出ます。キューが溢れたら捨てます。出力が常に平滑化され、下流は完全に一定ペースのトラフィックしか受けません。
| 観点 | トークンバケット | リーキーバケット |
|---|---|---|
| 制御する対象 | 入力(受理判定) | 出力(流出ペース) |
| バーストの扱い | 容量ぶん一気に通せる | 通さず平滑化して均す |
| 下流が見る形 | 山がそのまま通る | 常に一定レート |
| 遅延 | 原則ゼロ(即受理/即拒否) | キュー滞留ぶんの遅延が乗る |
| 典型用途 | API の課金・濫用防止 | 下流を保護する整形(シェーピング) |
トークンバケットは GCRA(Generic Cell Rate Algorithm) として等価に表現できます。GCRA は「次にいつ来てよいか」を表す理論到着時刻(TAT)を1つ保持し、到着が TAT - burst_tolerance より早ければ拒否、そうでなければ TAT = max(now, TAT) + 1/rate と更新します。状態が時刻1つで済み、加算減算だけで判定できるため、レート制限ミドルウェアの実装で広く使われます。トークン数を数える代わりに「許される最早到着時刻」を1つ持つ、と読み替えるとトークンバケットと同じ挙動になります。
ウィンドウ方式:境界バーストと近似精度
固定ウィンドウは「毎分0秒でカウンタをゼロに戻し、区間内の件数を数える」最も単純な方式です。しかし区間の境界に構造的欠陥があります。上限が毎分100件なら、ある分の最後の1秒に100件、次の分の最初の1秒にさらに100件を送れば、約2秒の間に200件——上限の2倍が通ります。攻撃者から見れば、これは確実に悪用できる抜け穴です。
これを塞ぐのがスライディングウィンドウです。2つの実装があります。
- スライディングウィンドウログ:各リクエストのタイムスタンプを保存し、判定時に「現在から過去 W 秒」より古いものを捨て、残った件数を数えます。完全に正確ですが、上限が高いキーではタイムスタンプの配列が膨らみ、メモリと計算量が件数に比例します。
- スライディングウィンドウカウンタ:現区間と直前区間の2つのカウンタだけを持ち、ウィンドウが直前区間にかかる割合で加重平均して推定します。メモリ一定で境界バーストを大幅に緩和できる、実務での定番近似です。
# スライディングウィンドウカウンタ(近似)
# 直前区間の件数を、ウィンドウが直前にかかる割合ぶん按分して足す
overlap = (window - elapsed_in_current) / window # 0..1
estimate = current_count + prev_count * overlap
allow = estimate < limit # 推定が上限未満なら受理
この近似は、直前区間の到着が一様分布だと仮定して件数を按分します。実際の到着が偏っていると推定が真値からずれますが、固定ウィンドウの2倍問題は消え、ログ方式のメモリ爆発も避けられます。正確さが要るならログ、一定メモリでよいならカウンタ、という選択になります。
複数ノードで動くサービスでは、各ノードがローカルにカウントすると、合計はノード数ぶん上限を超え得ます。かといって全判定を共有ストアへ問い合わせると、その1往復がホットパスのレイテンシと単一障害点になります。完全な正確さ・低レイテンシ・可用性を同時には満たせない——分散レート制限はこのトレードオフの中で「どれを捨てるか」を選ぶ設計問題です。
分散環境での一貫性
複数ノードに散らばったカウンタをどう一貫させるか。代表的な戦略は3つです。
| 方式 | 一貫性 | レイテンシ/可用性 | 向くケース |
|---|---|---|---|
| 共有ストア集約 | 強い(単一カウンタ) | 1往復ぶん増・ストアが単一障害点 | 正確さ最優先・中規模 |
| ローカル+定期同期 | 緩い(一時的に超過し得る) | 低レイテンシ・障害耐性高 | 大規模・多少の超過を許容 |
| トークン配分(リース) | 上限内に収束 | 中庸(リース更新時のみ通信) | 高スループットで上限厳守 |
最も多いのは Redis などの共有ストアへ原子的に集約する方式です。ここで肝心なのは、読み取りと書き込みの間に他ノードが割り込む競合(read-modify-write の競合)を避けることです。INCR は単一の原子的操作ですが、固定ウィンドウしか作れません。トークンバケットやスライディングウィンドウは複数の値を一貫して更新する必要があるため、サーバー側スクリプト(Lua)で複数コマンドを1つの原子的単位として実行するのが定石です。これで「補充→消費→残量書き戻し」が他の判定に割り込まれず、カウンタずれを防げます。
**トークン配分(リース)**は、グローバルな上限プールから各ノードへトークンの塊を貸し出し、ノードはローカルで消費して尽きたら追加リースを取りに行く方式です。判定のたびに通信せずに済むため低レイテンシで、かつグローバル上限は超えません。ただしリースを持ったノードが落ちると、その未使用ぶんが一時的に死蔵される点に注意します。
分散カウンタの正確さは時刻に依存します。トークンバケットの遅延補充は各ノードのクロックを使うため、**クロックスキュー(ノード間の時刻ずれ)**があると補充量がずれます。判定はストア側の時刻に統一するか、論理時刻で揃えるのが安全です。また、リトライで同一リクエストが二重カウントされないよう、リクエストに冪等キーを持たせて重複を弾く設計も問われます。
総当たり対策:頻度制御だけでは破られる
レート制限は総当たり(ブルートフォース)対策の土台ですが、IP 単位の頻度制御だけでは現代の攻撃に不十分です。攻撃の形を分けて考えます。
- 古典的ブルートフォース:1アカウントに多数のパスワードを試す。これは「アカウント+IP」単位の試行回数制限と、失敗時の指数バックオフ(失敗ごとに待ち時間を倍化)で現実的時間内に成立しなくできます。
- クレデンシャルスタッフィング:漏洩した「正しい」ユーザー名とパスワードの組を、多数のサイトへ大量に試す。1アカウントあたりの試行は1〜数回と少なく、IP も大量のプロキシ・ボットネットに分散します。アカウント単位でもIP単位でも閾値に達しないため、頻度制御をすり抜けます。
- パスワードスプレー:少数のよくあるパスワード(
Password1!等)を、多数のアカウントへ薄く広く試す。これもアカウント単位ではロックに達しません。
つまりクレデンシャルスタッフィングとスプレーは、頻度を意図的に閾値以下に保つことで設計上レート制限を回避します。対抗には、頻度以外のシグナルが要ります。
「N回失敗でアカウントをロック」する素朴な対策は、攻撃者がわざと他人のアカウントを失敗させてロックアウトする DoS に転用できます。ロックは「アカウント単位の永続ロック」ではなく「短時間の指数バックオフ」「該当セッション/IP の一時遅延」にとどめ、正規ユーザーの締め出しを避けるのが原則です。総当たりへの根本対策は、強い保存(パスワードハッシュの内部設計)と 多要素認証 で、たとえ正しいパスワードが流出しても突破させない多層化にあります。
CAPTCHA と Proof-of-Work:自動化のコストを上げる
頻度で区別できない攻撃には、人間か自動化かを区別する、あるいは自動化のコストを引き上げる仕組みを重ねます。
- CAPTCHA:人間には容易で機械には難しいタスク(画像認識、振る舞い解析)で人間性を判定します。現代の方式は明示的なパズルより、マウス軌跡・タイミング・デバイス指紋などの受動的シグナルでスコアリングし、疑わしいときだけチャレンジを出す方向に進化しています。ただし CAPTCHA 解決を代行する有人・AI サービスが存在するため、単独では完全な防御になりません。
- Proof-of-Work(PoW):クライアントに、検証は軽いが計算は重い問題(例:先頭 k ビットが0になるハッシュを探す)を解かせ、その解を添えさせます。サーバーは1回のハッシュで検証できますが、攻撃者は大量試行のたびに重い計算を強いられ、大規模自動化の単価が跳ね上がります。難易度 k を動的に上げれば、攻撃の経済性を能動的に破壊できます。
PoW チャレンジの例(先頭 k ビットが 0 のハッシュを探す):
サーバー: challenge と難易度 k を発行
クライアント: H(challenge, nonce) の先頭 k ビットが 0 になる nonce を総当たりで探索
期待試行回数は約 2^k 回(指数的に重い)
サーバー: nonce を受け取り H を1回計算して検証(軽い)
→ 検証は O(1)、生成は O(2^k)。この非対称性が自動化の単価を押し上げる。
PoW の利点は、CAPTCHA と違って人間の操作を要求せず正規ユーザーの体験を損ねにくい点と、サーバー側に攻撃者ごとの状態を持たずに済む点です。難点は、正規の低性能端末(古いスマートフォン等)にも計算負担がかかること、そして専用ハードを持つ攻撃者には相対的に効きが弱まることです。
実務では単一の銀の弾丸を置かず、頻度制御(レート制限)→ デバイス/IP 評価 → 疑わしいときだけ CAPTCHA/PoW → 認証段で MFA という多層で、各層が別のシグナルを使って攻撃の経済性を段階的に破壊します。
まとめ
レート制限のアルゴリズムは、平均レートとバースト許容量をどう独立に制御するかで性格が決まります。トークンバケットは容量ぶんのバーストを許して即時に受理・拒否し、リーキーバケットは出力を平滑化して下流を保護する——双対だが許すバースト形状が逆です。GCRA は両者を時刻1つで表す統一的な見方を与えます。固定ウィンドウは境界で上限の2倍が通る欠陥があり、スライディングウィンドウはログ方式で正確に、カウンタ方式でメモリ一定の近似として塞ぎます。
分散環境では正確さ・レイテンシ・可用性が三すくみになり、共有ストアへの原子的集約・ローカル+同期・トークン配分のいずれを選ぶかは要件次第です。総当たり対策としては、頻度制御は古典的ブルートフォースには効くものの、閾値以下に抑えるクレデンシャルスタッフィングやパスワードスプレーには単独で無力で、デバイス指紋・CAPTCHA・PoW・MFA を重ねて自動化のコストと突破後の価値を同時に削ぐのが要点です。可用性側の全体像は DoS/DDoS の増幅原理と緩和アーキテクチャ と併せて押さえると、頻度ベース防御の位置づけが立体的に見えてきます。
セキュリティ Article
レート制限と総当たり対策のアルゴリズム原理を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
レート制限
比較で見る軸
難易度: advanced / カテゴリ: セキュリティ / タグ数: 5
導入後に効く点
固定ウィンドウは境界で上限の2倍が通る欠陥がある。スライディングウィンドウログは正確だがメモリを食い、スライディングウィンドウカウンタは前区間を加重平均してメモリ一定で近似する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- セキュリティ
- タグ数
- 5
判断チェックリスト
- 自社の用途が「レート制限 / トークンバケット」に近いか確認する。
- 強みである「トークンバケットは平均速度を保ちつつ容量ぶんのバーストを許す。リーキーバケットは出力を完全に平滑化する。両者は数学的に双対だが、許すバースト形状が異なる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。