대부분의 API 속도 제한 구현은 단순한 프록시 로테이션으로 쉽게 우회되지만, 동일한 로테이션이 몇 분 안에 남용 방지 휴리스틱을 트리거합니다. 실제 과제는 단순히 제한을 넘지 않는 것이 아니라, 봇처럼 보이지 않으면서 제한을 유지하는 것입니다. 이를 위해서는 두 가지 별개의 속도 제한 범위(IP별 및 키별)를 이해하고, 지터가 적용된 백오프를 적용하며, 풀 로테이션을 무작위화하고, 요청 타이밍을 유기적 트래픽과 유사하게 조정해야 합니다. 프록시 풀 위에 계층화된 토큰 버킷 패턴은 강력한 기반을 제공합니다.
IP별 vs 키별: 속도 제한의 두 축
속도 제한은 소스 IP와 API 키(또는 토큰)라는 최소 두 가지 독립적인 차원에서 작동합니다. 단일 키는 시간당 5,000개의 요청을 허용할 수 있지만(GitHub의 인증된 제한), 동일한 키라도 단일 IP에서 오는 경우 더 낮은 버스트 상한에서 제한될 수 있습니다. 인증되지 않은 요청은 더욱 제한적이며, 일반적으로 IP당 시간당 60개의 요청으로 제한됩니다. IP 차원을 무시하는 것은 429 Too Many Requests(RFC 6585) 응답을 트리거하는 가장 빠른 방법입니다. 프록시 풀은 단일 출처가 포화되지 않도록 여러 IP에 요청을 분산해야 하지만, 각 IP는 여전히 동일한 키를 공유합니다. 키의 전역 제한이 시간당 5,000개이고 50개의 프록시가 있는 경우, 각 프록시는 키 수준 카운터가 작동하기 전에 시간당 100개의 요청만 수행할 수 있습니다. 코드를 한 줄도 작성하기 전에 두 제한을 모두 매핑하십시오.
지터가 포함된 백오프: 정중함과 예측 가능성의 차이
지터 없는 지수 백오프는 지문과 같습니다. 서버는 요청이 완벽하게 두 배로 증가하는 간격으로 도착하는 것을 보고 자동화된 것으로 플래그를 지정합니다. 해결책은 전체 지터입니다: 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개의 요청을 허용하는 경우, 각각 분당 2개의 요청을 수행하는 50개의 프록시를 사용하는 것은 규정을 준수하는 것입니다. 각각 분당 100개의 요청을 수행하는 100개의 프록시를 사용하는 것은 규정을 준수하지 않습니다. 지터가 아무리 영리하더라도 이는 남용입니다. 차이는 의도와 볼륨에 있습니다. 프록시 풀 위에 계층화된 토큰 버킷은 도구입니다. 공개 디렉토리를 윤리적으로 스크래핑하는 동일한 도구가 소규모 API를 DDoS하는 데 사용될 수도 있습니다. 속도 제한 목표를 문서화하고, 로그에서 429를 감사하며, 문서화된 키별 상한을 절대 초과하지 마십시오. 더 많은 처리량이 필요하면 더 높은 제한을 요청하거나 전용 요금제를 지불하십시오.