【入門編】 レートリミット(Rate Limiting)のアルゴリズム:トークンバケットとリーキーバケット – Web APIアーキテクチャ・データ連携実践ガイド

こんにちは!Webサービスの裏側を支えるネットワークの世界へようこそ。インフラアーキテクトの私です。

私たちが普段何気なく使っているスマートフォンアプリやWebサイト。その裏側では、無数のAPI(Application Programming Interface)が目にも留まらぬ速さでデータをやり取りしていますよね。

さて、あなたがもし人気絶頂のチケット販売サイトや、世界中で使われるSNSのシステムを作ったとしたら、こんな心配をしたことはありませんか?
「もし、何万人ものユーザーが一斉にアクセスしてきて、サーバーが耐えきれずにダウンしてしまったらどうしよう…」と。

そんな「サーバーのパンク」を防ぐために、現代のWebインフラに欠かせないのが「レートリミット(Rate Limiting:流量制限)」という技術です。今回は、このレートリミットの裏側で動いている代表的な2つのアルゴリズム、「トークンバケット」と「リーキーバケット」について、現実世界の仕組みに例えながら、一歩ずつ優しく紐解いていきましょう!

—

1. なぜAPIには「交通整理」が必要なのか?

皆さんは、遊園地のアトラクションや、大人気ラーメン店の行列に並んだ経験はありますよね。お店側は一度に入れる人数を制限することで、店内がパニックになるのを防いでいます。

Web APIも全く同じです。もし、一人のユーザーが1秒間に10,000回もリクエストを送り続けてきたらどうなるでしょうか? サーバーはたった一人のためにリソースを使い果たし、他のまじめなユーザーが使えなくなってしまいます。これを防ぐために「あなた、少しペースを落としてくださいね」と交通整理をする仕組みがレートリミットなのです。

APIの世界では、この制限を超えたユーザーに対して、HTTPステータスコードの 429 Too Many Requests を返し、「少し待ってからやり直してね」と優しく(時には厳しく)伝えます。

—

2. 身近な例えで理解する「バースト(突発的なトラフィック)」

レートリミットを語る上で避けて通れないのが「バーストトラフィック」という概念です。バーストとは、普段は静かなのに、ある瞬間だけ「ドカン!」と急激にアクセスが押し寄せる現象のこと。

例えば、朝の通勤ラッシュ時の改札口を想像してみてください。
普段はパラパラと人が通っているだけですが、電車が到着した瞬間、一気に大量の人が改札へ押し寄せますよね。この「一瞬の混雑」を上手に受け止められるかどうかが、アルゴリズムの腕の見せ所なんです。

それでは、このバーストにどう立ち向かうのか、2つの賢い仕組みを見ていきましょう!

—

3. 「トークンバケット(Token Bucket)」:遊園地のエクスプレスパス方式

まずは、多くのモダンなWeb APIで採用されている「トークンバケット」アルゴリズムです。

仕組みのイメージ

  • バケット(バケツ)の中に、一定のペースで「トークン(入場券)」がポトポトと溜まっていきます。
  • バケツの容量には上限(最大値)があります。例えば「最大10枚まで」と決まっていれば、いくら放置しても10枚以上は溜まりません。
  • ユーザーがAPIにリクエストを送る時は、このトークンを1枚消費します。
  • トークンがバケツに残っていれば、リクエストは即座に許可されます。しかし、トークンが空っぽの時にリクエストが来ると、アクセスは拒否(あるいは順番待ち)されます。

バーストへの対応力

トークンバケットの最大の強みは、「溜まっていたトークンを使って、一瞬の爆発的なアクセス(バースト)を許容できること」です。
普段使わずにトークンが10枚溜まっていれば、電車の到着と同時に10人分の一気通貫(バースト)をスムーズに捌ききることができます。その後、トークンが補充されるまでの間は少しペースが落ちますが、実用上非常に心地よいユーザー体験を提供できるのが特徴です。

—

4. 「リーキーバケット(Leaky Bucket)」:底に穴の空いたバケツの交通整理

続いて、「リーキーバケット」アルゴリズムを見ていきましょう。名前の通り、英語の「Leak(漏れる)」が由来です。

仕組みのイメージ

  • 上から水(リクエスト)が注がれるバケツがあります。このバケツの底には小さな穴が空いており、水は一定のスピードでしか流れ出ません。
  • ユーザーからのリクエストは、まずこのバケツに注がれます。
  • バケツがいっぱいになる前にリクエストが来ればセーフですが、注がれるスピードが底から流れ出るスピードを超えると、バケツから水があふれてしまいます。あふれたリクエストはエラー(429)となります。

バーストへの対応力

トークンバケットとは対照的に、リーキーバケットは「どれだけ急なアクセスが来ても、出口のスピード(処理速度)を完全に一定に保つ」のが得意です。
例えるなら、遊園地のアトラクションへ続く「一本道(行列)」ですね。どれだけ沢山の人が一気に並んでも、アトラクションに乗れるのは一定のペース(例えば1分間に2人)と決まっているため、後方のシステム(データベースなど)に急激な負荷がかかるのを完全に防ぐことができます。

—

5. 比較のまとめ:どっちを選ぶべき?

ここで、2つのアルゴリズムの違いを分かりやすく表にまとめて整理してみましょう。

| 比較項目 | トークンバケット (Token Bucket) | リーキーバケット (Leaky Bucket) |
| :— | :— | :— |
| 現実世界の例え | 回数券が溜まるポケット | 一定の太さのホース(穴の空いたバケツ) |
| バースト(突発的アクセス) | 得意(溜まったトークンで一気に処理可能) | 苦手(一定のペース以外はすべて弾かれる) |
| 下流への負荷 | 変動する(バースト時は一時的に高くなる) | 常に一定(平滑化される) |
| 主な用途 | 一般的なWeb API、クラウドサービスの利用制限 | ネットワークの帯域制御、動画配信のパケット制御 |

実務のWeb API開発においては、ユーザーの「サクサク動いてほしい!」という期待(バーストの許容)に応えつつ、自社のサーバーを守るために「トークンバケット」が選ばれるケースが非常に多いです。

—

6. 実装のイメージ(Pythonによる簡単なコード例)

「なるほど、概念は分かったけれど、実際にどうやって動かすの?」と思った方のために、Pythonを使ってトークンバケットの仕組みをごくシンプルなコードで表現してみましょう。

import time

class TokenBucket:
    def __init__(self, capacity: int, refill_rate: float):
        """
        capacity: バケツの最大容量(最大バースト数)
        refill_rate: 1秒あたりに補充されるトークンの数
        """
        self.capacity = capacity
        self.tokens = float(capacity)  # 初期状態では満タンにする
        self.refill_rate = refill_rate
        self.last_refill_time = time.time()

    def _refill(self):
        """時間が経過した分だけ、トークンを補充する内部メソッド"""
        now = time.time()
        elapsed = now - self.last_refill_time
        self.last_refill_time = now
        
        # 経過時間 × 補充レートの分だけトークンを増やす(ただし上限はcapacityまで)
        self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)

    def consume(self, tokens_to_consume: int = 1) -> bool:
        """
        リクエストが来たときに呼び出し、トークンを消費できるか判定する
        """
        self._refill()

        if self.tokens >= tokens_to_consume:
            self.tokens -= tokens_to_consume
            print(f"[許可] リクエスト成功! 残りトークン: {self.tokens:.2f}")
            return True
        else:
            print(f"[拒否] 429 Too Many Requests (トークン不足)")
            return False

# --- 動作確認シミュレーション ---
# 容量5個、1秒に1個のペースでトークンが回復するバケットを作成
bucket = TokenBucket(capacity=5, refill_rate=1.0)

# 連続で6回リクエストを送ってみる
for i in range(1, 7):
    print(f"--- リクエスト {i} 回目 ---")
    bucket.consume()
    time.sleep(0.1) # 少しだけウェイトを入れる

このコードを実行すると、最初の5回まではスムーズに「[許可]」され、6回目でバケツが空になって「[拒否]」される様子が確認できます。実務では、これをRedisなどのインメモリデータベースと組み合わせて、分散サーバー環境でも正確に動くように設計していきます。

—

まとめ

今回は、APIを守るための交通整理ルールである「レートリミット」について、トークンバケットとリーキーバケットという2つの強力なアルゴリズムを解説しました。

  • トークンバケットは、普段の余裕を貯めておいて、突発的なアクセス(バースト)を気持ちよく受け止めるのが得意。
  • リーキーバケットは、あふれるほどのアクセスが来ても、一定のペースを頑なに守って下流を保護するのが得意。

インフラやネットワークの世界は、こうした「現実世界のエッセンス」をデジタルに置き換えたロジックで満ち溢れています。一つひとつの仕組みを丁寧に紐解いていけば、決して難しいものではありません。

皆さんもAPIを設計・利用する際は、「今、自分のバケツにはいくつトークンが残っているかな?」と、少しだけ裏側の仕組みに思いを馳せてみてくださいね。それではまた、次回の深淵なネットワークの世界でお会いしましょう!

コメント

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