ほとんどのAPIレート制限の実装は、単純なプロキシローテーションで簡単に回避できます。しかし、その同じローテーションが数分以内に不正利用防止ヒューリスティックをトリガーします。本当の課題は、単に制限内に収まることではなく、ボットのように見えずにそれを実現することです。そのためには、2つの異なるレート制限スコープ(IP単位とキー単位)を理解し、ジッターを伴うバックオフを適用し、プールのローテーションをランダム化し、リクエストのタイミングを有機的なトラフィックに似せて整形する必要があります。プロキシプール上に階層化されたトークンバケットパターンは、堅牢な基盤を提供します。
IP単位 vs キー単位:レート制限の2つの軸
レート制限は、少なくとも2つの独立した次元(送信元IPとAPIキー(またはトークン))で動作します。1つのキーで1時間あたり5,000リクエスト(GitHubの認証済み制限)が許可される場合でも、同じキーを単一のIPから使用すると、より低いバースト上限でスロットルされる可能性があります。未認証のリクエストはさらに制約が厳しく、通常はIPあたり1時間あたり60リクエストです。IPの次元を無視することは、429 Too Many Requests(RFC 6585)応答をトリガーする最も早い方法です。プロキシプールは、単一の送信元を飽和させないようにリクエストを複数のIPに分散する必要がありますが、各IPは依然として同じキーを共有します。キーのグローバル制限が1時間あたり5,000で、50のプロキシがある場合、各プロキシはキーレベルのカウンターが作動する前に1時間あたり100リクエストしか実行できません。コードを1行も書く前に、両方の制限をマッピングしてください。
ジッター付きバックオフ:礼儀正しさと予測可能性の違い
ジッターのない指数バックオフは指紋のようなものです。サーバーは完全に倍増する間隔で到着するリクエストを確認し、自動化されたものとしてフラグを立てます。修正方法はフルジッターです:sleep(random.uniform(0, min(cap, base * 2 ** attempt)))。これにより、再試行が時間枠全体に分散され、パターンが実際のユーザーのバーストと区別できなくなります。AWS自身の指数バックオフに関するドキュメントでも、まさにこの理由からジッターが推奨されています。次のPythonスニペットは、固定レートで補充され、空のときにジッター付きでスリープし、取得成功ごとにプロキシをローテーションするトークンバケットを実装しています。
import time
import random
from collections import deque
class TokenBucketProxyPool:
def __init__(self, proxies, rate, burst):
self.proxies = deque(proxies)
self.rate = rate # tokens per second
self.burst = burst
self.tokens = burst
self.last_refill = time.monotonic()
def refill(self):
now = time.monotonic()
elapsed = now - self.last_refill
self.tokens = min(self.burst, self.tokens + elapsed * self.rate)
self.last_refill = now
def acquire(self):
while True:
self.refill()
if self.tokens >= 1:
self.tokens -= 1
self.proxies.rotate(1) # simple rotation, but see section below
return self.proxies[0]
else:
sleep_time = (1 - self.tokens) / self.rate
jitter = random.uniform(0, sleep_time * 0.5)
time.sleep(sleep_time + jitter)
# Usage: pool = TokenBucketProxyPool(proxies, rate=2.0, burst=10)
# proxy = pool.acquire() # blocks until token available
プロキシプールのローテーション:ラウンドロビンではなくランダム化
プロキシリストの順次ラウンドロビンは検出可能です。サーバーが1.2.3.4、5.6.7.8、9.10.11.12からのリクエストを連続して確認すると、IP間でパターンを相関させます。代わりに、プロキシを置換ありでランダムに選択し、プロキシごとにクールダウンを追加します。公開プロキシディレクトリでは60~80%の障害率が報告されています。429または接続エラーを返すプロキシは、少なくとも60秒間はデッドキューに降格させる必要があります。最近成功したプロキシが選択されやすくなる、重み付きランダム選択を実装してください。上記のトークンバケットでは、わかりやすくするために単純なrotate(1)を使用しています。本番環境では、それをrandom.choice()に置き換え、プロキシごとの障害回数を追跡してください。
リクエスト整形:限界を超えずに平均を模倣する
完璧なトークンバケットとプロキシプールがあっても、毎秒正確に2.0の一定のリクエストレートは不自然です。実際のユーザーは一時停止し、バッチ処理し、リクエスト間隔を変動させます。各acquire()呼び出しの前に小さなランダム遅延(例:time.sleep(random.uniform(0.1, 0.5)))を追加し、数分ごとにトークンバケットのレートを±10%変動させてください。ただし、APIの文書化されたレート制限ウィンドウを超える遅延を追加しないでください。それは目的を損なうことになります。目標は、制限内に収まりながら、正当なクライアントのクラスターのように見えることです。過剰な整形(人間のようなタイピング遅延の挿入など)は無駄な労力です。APIが気にするのはリクエスト頻度であり、キーストローク間のタイミングではありません。
倫理的な緩和策:線引きを理解する
ここで説明したすべての手法は、APIの利用規約に違反しない限り、正当な最適化です。ほとんどのToSは、「レート制限の回避」や「自動化された手段によるサービスへのアクセス」を明示的に禁止しています。ToSを読むことは必須です。APIがキーあたり毎分100リクエストを許可している場合、50のプロキシを使用してそれぞれが毎分2リクエストを実行するのは準拠しています。100のプロキシを使用してそれぞれが毎分100リクエストを実行するのは準拠していません。それは、ジッターがどれほど巧妙であっても、悪用です。違いは意図と量にあります。プロキシプール上に階層化されたトークンバケットはツールです。公開ディレクトリを倫理的にスクレイピングする同じツールが、小規模なAPIをDDoSするためにも使用される可能性があります。レート制限の目標を文書化し、ログで429を監査し、文書化されたキーあたりの上限を決して超えないでください。より多くのスループットが必要な場合は、より高い制限を要求するか、専用プランに料金を支払ってください。