Hầu hết các triển khai giới hạn tốc độ API đều dễ dàng bị vượt qua bằng cách xoay vòng proxy đơn giản — nhưng chính việc xoay vòng đó lại kích hoạt các thuật toán chống lạm dụng trong vòng vài phút. Thách thức thực sự không chỉ là ở dưới giới hạn, mà còn là làm điều đó mà không bị coi là bot. Điều này đòi hỏi phải hiểu hai phạm vi giới hạn tốc độ riêng biệt (theo IP và theo khóa), áp dụng backoff có jitter, ngẫu nhiên hóa vòng quay pool, và định hình thời gian yêu cầu để bắt chước lưu lượng truy cập tự nhiên. Mô hình token bucket, được xếp lớp trên một proxy pool, cung cấp một nền tảng vững chắc.
Per-IP vs Per-Key: Hai Trục của Giới hạn Tốc độ
Giới hạn tốc độ hoạt động trên ít nhất hai chiều độc lập: IP nguồn và khóa API (hoặc token). Một khóa đơn lẻ có thể cho phép 5.000 yêu cầu mỗi giờ (giới hạn đã xác thực của GitHub), nhưng cùng một khóa từ một IP duy nhất có thể bị giới hạn ở một mức bùng nổ thấp hơn. Các yêu cầu chưa xác thực thậm chí còn bị ràng buộc nhiều hơn — thường là 60 yêu cầu mỗi giờ mỗi IP. Bỏ qua chiều IP là cách nhanh nhất để kích hoạt phản hồi 429 Too Many Requests (RFC 6585). Một proxy pool phải phân phối các yêu cầu trên nhiều IP để tránh làm bão hòa bất kỳ nguồn gốc đơn lẻ nào, nhưng mỗi IP vẫn chia sẻ cùng một khóa. Nếu giới hạn toàn cục của khóa là 5.000/giờ và bạn có 50 proxy, mỗi proxy chỉ có thể thực hiện 100 yêu cầu mỗi giờ trước khi bộ đếm cấp khóa kích hoạt. Hãy lập bản đồ cả hai giới hạn trước khi viết một dòng mã nào.
Backoff với Jitter: Sự Khác Biệt Giữa Lịch Sự và Có Thể Dự Đoán
Backoff theo cấp số nhân mà không có jitter là một dấu vân tay. Máy chủ thấy các yêu cầu đến với khoảng thời gian nhân đôi hoàn hảo và gắn cờ chúng là tự động. Giải pháp là full jitter: sleep(random.uniform(0, min(cap, base * 2 ** attempt))). Điều này trải rộng các lần thử lại trên khung thời gian, làm cho mẫu không thể phân biệt được với một đợt bùng nổ người dùng thực. Tài liệu của AWS về backoff theo cấp số nhân khuyến nghị jitter chính xác vì lý do này. Đoạn mã Python sau đây triển khai một token bucket nạp lại với tốc độ cố định, ngủ với jitter khi rỗng, và xoay vòng proxy sau mỗi lần acquire thành công:
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
Xoay Vòng Proxy Pool: Ngẫu Nhiên Hóa, Đừng Round-Robin
Round-robin tuần tự qua một danh sách proxy có thể bị phát hiện. Một máy chủ thấy các yêu cầu từ 1.2.3.4, sau đó 5.6.7.8, sau đó 9.10.11.12 theo cùng một nhịp sẽ tương quan mẫu trên các IP. Thay vào đó, hãy chọn proxy ngẫu nhiên có thay thế, và thêm thời gian chờ (cooldown) cho mỗi proxy. Các thư mục proxy công cộng báo cáo tỷ lệ thất bại 60–80% — một proxy trả về 429 hoặc lỗi kết nối nên được hạ xuống hàng đợi chết trong ít nhất 60 giây. Triển khai lựa chọn ngẫu nhiên có trọng số, trong đó các proxy gần đây thành công có nhiều khả năng được chọn hơn. Token bucket ở trên sử dụng một rotate(1) đơn giản cho rõ ràng; trong sản xuất, hãy thay thế bằng random.choice() và theo dõi số lần thất bại của từng proxy.
Định Hình Yêu Cầu: Bắt Chước Mức Trung Bình Mà Không Vượt Quá Giới Hạn
Ngay cả với một token bucket và proxy pool hoàn hảo, tốc độ yêu cầu không đổi chính xác 2,0 mỗi giây là không tự nhiên. Người dùng thực tạm dừng, xử lý theo lô và thay đổi khoảng thời gian giữa các yêu cầu. Thêm một độ trễ ngẫu nhiên nhỏ (ví dụ: time.sleep(random.uniform(0.1, 0.5))) trước mỗi lần gọi acquire(), và thay đổi tốc độ của token bucket ±10% sau mỗi vài phút. Tuy nhiên, đừng thêm độ trễ vượt quá cửa sổ giới hạn tốc độ đã được tài liệu hóa của API — điều đó phản tác dụng. Mục tiêu là ở trong giới hạn trong khi trông giống như một cụm khách hàng hợp pháp. Định hình quá mức (ví dụ: chèn độ trễ gõ phím giống con người) là lãng phí công sức; API quan tâm đến tần suất yêu cầu, không phải thời gian giữa các lần gõ phím.
Giảm Thiểu Đạo Đức: Biết Ranh Giới Ở Đâu
Mọi kỹ thuật được mô tả ở đây đều là tối ưu hóa hợp pháp — cho đến khi nó vi phạm Điều khoản Dịch vụ của API. Hầu hết ToS đều cấm rõ ràng 'vượt qua giới hạn tốc độ' hoặc 'sử dụng phương tiện tự động để truy cập dịch vụ.' Đọc ToS là không thể bỏ qua. Nếu API cho phép 100 yêu cầu mỗi phút mỗi khóa, sử dụng 50 proxy mỗi proxy thực hiện 2 yêu cầu mỗi phút là tuân thủ. Sử dụng 100 proxy mỗi proxy thực hiện 100 yêu cầu mỗi phút thì không — đó là lạm dụng, bất kể jitter của bạn thông minh đến đâu. Sự khác biệt là ý định và khối lượng. Một token bucket được xếp lớp trên proxy pool là một công cụ; cùng một công cụ dùng để thu thập dữ liệu thư mục công cộng một cách đạo đức cũng có thể được sử dụng để tấn công DDoS một API nhỏ. Hãy ghi lại các mục tiêu giới hạn tốc độ của bạn, kiểm tra nhật ký để tìm 429, và không bao giờ vượt quá mức trần mỗi khóa đã được tài liệu hóa. Nếu bạn cần thông lượng cao hơn, hãy yêu cầu giới hạn cao hơn hoặc trả tiền cho một gói chuyên dụng.