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

APIを守る「賢い門番」:トークンバケットアルゴリズムで実現する優雅なレートリミット

こんにちは!ネットワークの世界にどっぷり浸かっているインフラエンジニアです。

皆さんは、Web APIを作ったり使ったりしているときに「レートリミット(API利用制限)」という言葉を聞いたことはありますか? 例えば、「1分間に100リクエストまで」といった制限です。

一見すると単純なルールに見えますが、いざ実装しようとすると「急激なアクセス集中(バースト)をどう扱うか?」という問題にぶつかります。ガチガチに制限するとシステムは使いにくくなるし、緩めすぎるとサーバーがパンクしてしまう。

このジレンマを解決する、まるで「優秀なホテルのフロント係」のような仕組みが「トークンバケットアルゴリズム」です。今日は、この賢い仕組みを身近な例えと一緒に紐解いていきましょう!

—

郵便局の窓口で考えてみよう

イメージしてみてください。ある郵便局に「1分間に1通しか手紙を受理できない」という非常に厳しい窓口があるとします。

もし、お客さんが5人同時にやってきたらどうなるでしょう? 厳格に制限すると、4人は追い返されてしまいますよね。でも、これではあまりに不親切です。

そこで、「バケット(バケツ)」の登場です。

1. 窓口の横に「バケツ」を置きます。
2. このバケツには、1分ごとに1個ずつ「トークン(チケット)」が自動的に放り込まれます。
3. お客さんは、窓口で手紙を出すときに「バケツの中のトークン」を1枚支払います。

トークンバケットがもたらす「余裕」

ここで重要なのは、「バケツには最大10個までトークンを溜めておける」というルールです。

もし、しばらくお客さんが誰も来なければ、バケツは満タン(10個)になります。そこに5人同時にやってきたらどうなるでしょうか? バケツの中に貯金していたトークンをパパッと使って、5人とも一瞬で受け付け完了!

このように、「普段は規則正しく処理しつつ、たまにくるラッシュ(バースト)は貯金で受け流す」。これがトークンバケットの真骨頂です。

—

なぜこの仕組みが「美しい」のか

ネットワークの設計において、機械的に「1秒に1回」と制限をかけると、ネットワークの遅延などで少しだけタイミングがズレただけでエラー(429 Too Many Requests)を返してしまうことがあります。

トークンバケットを使えば、以下の2つのメリットを両立できます。

  • 平均レートの制御: 長期的に見れば、トークンが補充される速度(例えば毎秒1リクエスト)に落ち着く。
  • バーストの許容: 瞬間的なアクセスの波を、溜めておいたトークンで吸収できる。

ユーザー体験(UX)を損なわずに、サーバーの負荷をしっかり守れる。まさにインフラエンジニアが愛してやまない「美しい設計」なんです。

—

実装のイメージを掴んでみよう(Pythonコード)

では、この考え方を簡単なコードにしてみましょう。難しく考える必要はありません。「バケツの中身を管理する」だけです。

import time

class TokenBucket:
    def __init__(self, capacity, refill_rate):
        self.capacity = capacity      # バケツの最大容量
        self.refill_rate = refill_rate # 1秒あたりの補充数
        self.tokens = capacity        # 最初は満タンにしておく
        self.last_refill = time.time()

    def consume(self):
        # 前回の補充からの経過時間を計算
        now = time.time()
        elapsed = now - self.last_refill
        
        # 時間経過分だけトークンを補充
        self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
        self.last_refill = now

        # トークンがあれば1枚消費してTrueを返す
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False

# 使い方:バケット容量5、毎秒1個補充
bucket = TokenBucket(capacity=5, refill_rate=1)

# 連続でリクエストが来た時の処理
for i in range(7):
    if bucket.consume():
        print(f"リクエスト {i+1}: 通過!")
    else:
        print(f"リクエスト {i+1}: 制限に引っかかりました(429エラー相当)")
    time.sleep(0.5) # 0.5秒ごとにリクエスト

このコードを実行すると、最初の5回はバケツの貯金でスムーズに通過し、6回目以降はトークンの補充が追いつかずに制限がかかる様子が見て取れるはずです。

—

現場で役立つチューニングのコツ

実際に本番環境でこの仕組みを設計する際は、以下のパラメーターを意識してみてください。

1. バーストサイズ(容量): どのくらいの集中まで許容するか。大きすぎるとサーバー負荷が跳ね上がります。
2. リフィルレート(補充速度): 1秒間に何回までなら安全に処理できるか。DBの性能などから逆算します。

これらは固定値ではなく、システムの状態に応じてRedisなどの高速なインメモリDBで共有カウンターとして管理するのが定石です。

最後に:完璧な制限よりも「心地よい制限」を

技術はあくまで手段です。レートリミットの目的は、単に拒否することではなく、「システム全体が倒れないように守り、かつユーザーには可能な限り快適に使ってもらうこと」にあります。

今回ご紹介したトークンバケットは、そのバランスを保つための非常に強力な武器になります。皆さんの設計するAPIが、多くのユーザーに愛され、かつ強固なインフラの上に成り立つことを願っています!

また次回も、パケットの向こう側にある「エンジニアの美学」についてお話ししましょう。それでは、良い実装ライフを!

コメント

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