การใช้งาน rate limit ของ API ส่วนใหญ่สามารถหลีกเลี่ยงได้ง่ายๆ ด้วยการหมุนพร็อกซีแบบง่ายๆ — แต่การหมุนแบบเดียวกันนั้นกลับกระตุ้น heuristics ต่อต้านการใช้งานอัตโนมัติภายในไม่กี่นาที ความท้าทายที่แท้จริงไม่ใช่แค่การอยู่ภายใต้ขีดจำกัด แต่คือการทำเช่นนั้นโดยไม่ดูเหมือนบอท สิ่งนี้ต้องอาศัยความเข้าใจในขอบเขตของ rate limit สองแบบที่แตกต่างกัน (ต่อ IP และต่อคีย์) การใช้ backoff แบบมี jitter การสุ่มการหมุนพูลพร็อกซี และการปรับจังหวะการส่งคำขอให้เลียนแบบการเข้าชมจากผู้ใช้จริง รูปแบบ token bucket ที่วางซ้อนบนพูลพร็อกซีเป็นพื้นฐานที่แข็งแกร่ง
Per-IP vs Per-Key: สองแกนของการจำกัดอัตรา
Rate limit ทำงานบนมิติอิสระอย่างน้อยสองมิติ: IP ต้นทางและคีย์ API (หรือโทเคน) คีย์เดียวอาจอนุญาตให้ส่งคำขอได้ 5,000 ครั้งต่อชั่วโมง (ขีดจำกัดของผู้ใช้ที่ยืนยันตัวตนของ GitHub) แต่คีย์เดียวกันจาก IP เดียวอาจถูกจำกัดที่เพดาน burst ที่ต่ำกว่า คำขอที่ไม่ผ่านการยืนยันตัวตนจะถูกจำกัดมากยิ่งขึ้น — โดยทั่วไป 60 คำขอต่อชั่วโมงต่อ IP การละเลยมิติของ IP เป็นวิธีที่เร็วที่สุดในการกระตุ้นการตอบสนอง 429 Too Many Requests (RFC 6585) พูลพร็อกซีต้องกระจายคำขอไปยังหลาย IP เพื่อหลีกเลี่ยงการทำให้ต้นทางใดต้นทางหนึ่งอิ่มตัว แต่แต่ละ IP ยังคงใช้คีย์เดียวกัน หากขีดจำกัดรวมของคีย์คือ 5,000/ชั่วโมงและคุณมีพร็อกซี 50 ตัว พร็อกซีแต่ละตัวสามารถส่งคำขอได้เพียง 100 ครั้งต่อชั่วโมงก่อนที่ตัวนับระดับคีย์จะทำงาน ให้ทำแผนที่ขีดจำกัดทั้งสองก่อนเขียนโค้ดแม้แต่บรรทัดเดียว
Backoff with Jitter: ความแตกต่างระหว่างความสุภาพและความคาดเดาได้
Exponential backoff ที่ไม่มี jitter คือลายนิ้วมือ เซิร์ฟเวอร์เห็นคำขอมาถึงในช่วงเวลาที่เพิ่มขึ้นเป็นสองเท่าอย่างสมบูรณ์แบบและตรวจจับว่าเป็นอัตโนมัติ วิธีแก้คือ full jitter: sleep(random.uniform(0, min(cap, base * 2 ** attempt))) ซึ่งกระจายการลองใหม่ทั่วช่วงเวลา ทำให้รูปแบบแยกไม่ออกจากการระเบิดของผู้ใช้จริง เอกสารของ AWS เกี่ยวกับ exponential backoff แนะนำให้ใช้ jitter ด้วยเหตุผลนี้เอง โค้ด Python ต่อไปนี้ใช้ token bucket ที่เติมในอัตราคงที่ หน่วงเวลาด้วย jitter เมื่อว่าง และหมุนพร็อกซีทุกครั้งที่ได้ token สำเร็จ:
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
การหมุนพูลพร็อกซี: สุ่ม อย่าใช้ Round-Robin
การหมุนแบบ round-robin ตามลำดับผ่านรายการพร็อกซีสามารถตรวจจับได้ เซิร์ฟเวอร์ที่เห็นคำขอจาก 1.2.3.4 จากนั้น 5.6.7.8 จากนั้น 9.10.11.12 ตามลำดับ จะเชื่อมโยงรูปแบบข้าม IP ให้เลือกพร็อกซีแบบสุ่มโดยแทนที่ และเพิ่มระยะพักต่อพร็อกซี ไดเรกทอรีพร็อกซีสาธารณะรายงานอัตราความล้มเหลว 60–80% — พร็อกซีที่ส่งคืน 429 หรือข้อผิดพลาดการเชื่อมต่อควรถูกลดลำดับไปยังคิวที่ตายแล้วเป็นเวลาอย่างน้อย 60 วินาที ใช้การเลือกแบบสุ่มแบบถ่วงน้ำหนักโดยที่พร็อกซีที่ประสบความสำเร็จล่าสุดมีโอกาสถูกเลือกมากกว่า token bucket ข้างต้นใช้ rotate(1) แบบง่ายเพื่อความชัดเจน ในระบบจริงให้แทนที่ด้วย random.choice() และติดตามจำนวนความล้มเหลวต่อพร็อกซี
การปรับจังหวะคำขอ: เลียนแบบค่าเฉลี่ยโดยไม่ข้ามเส้น
แม้จะมี token bucket และพูลพร็อกซีที่สมบูรณ์แบบ อัตราคำขอคงที่ที่ 2.0 ครั้งต่อวินาทีก็ไม่เป็นธรรมชาติ ผู้ใช้จริงหยุดชั่วคราว ทำเป็นชุด และเปลี่ยนช่วงเวลาระหว่างคำขอ เพิ่มความล่าช้าแบบสุ่มเล็กน้อย (เช่น time.sleep(random.uniform(0.1, 0.5))) ก่อนการเรียก acquire() แต่ละครั้ง และเปลี่ยนอัตราของ token bucket ±10% ทุกสองสามนาที อย่างไรก็ตาม อย่าเพิ่มความล่าช้าที่เกินกรอบเวลาของ rate limit ที่เอกสาร API ระบุไว้ — เพราะจะทำให้ไร้ประโยชน์ เป้าหมายคือการอยู่ภายในขีดจำกัดในขณะที่ดูเหมือนกลุ่มไคลเอนต์ที่ถูกต้อง การปรับจังหวะมากเกินไป (เช่น การแทรกความล่าช้าแบบการพิมพ์ของมนุษย์) เป็นความพยายามที่สูญเปล่า API สนใจความถี่ของคำขอ ไม่ใช่จังหวะระหว่างการกดแป้น
การบรรเทาผลกระทบอย่างมีจริยธรรม: รู้ว่าเส้นอยู่ตรงไหน
ทุกเทคนิคที่อธิบายไว้ที่นี่เป็นการปรับให้เหมาะสมที่ถูกต้องตามกฎหมาย — จนกว่าจะละเมิดข้อกำหนดในการให้บริการ (ToS) ของ API ToS ส่วนใหญ่ห้าม "การหลีกเลี่ยง rate limit" หรือ "การใช้วิธีการอัตโนมัติเพื่อเข้าถึงบริการ" อย่างชัดเจน การอ่าน ToS ไม่ใช่ทางเลือก หาก API อนุญาต 100 คำขอต่อนาทีต่อคีย์ การใช้พร็อกซี 50 ตัวที่ส่งคำขอ 2 ครั้งต่อนาทีต่อตัวถือว่าปฏิบัติตามข้อกำหนด การใช้พร็อกซี 100 ตัวที่ส่งคำขอ 100 ครั้งต่อนาทีต่อตัวไม่ใช่ — นั่นคือการละเมิด ไม่ว่าคุณจะใช้ jitter ฉลาดแค่ไหนก็ตาม ความแตกต่างอยู่ที่เจตนาและปริมาณ token bucket ที่วางซ้อนบนพูลพร็อกซีเป็นเครื่องมือ เครื่องมือเดียวกับที่ใช้ขูดข้อมูลไดเรกทอรีสาธารณะอย่างมีจริยธรรมก็สามารถใช้โจมตี DDoS API เล็กๆ ได้เช่นกัน จงบันทึกเป้าหมาย rate limit ของคุณ ตรวจสอบบันทึกสำหรับ 429 และอย่าเกินเพดานต่อคีย์ที่ระบุไว้ หากคุณต้องการปริมาณงานมากขึ้น ให้ขอขีดจำกัดที่สูงขึ้นหรือจ่ายสำหรับแผนเฉพาะ