API Engineering

Mengurangkan Had Kadar API Tanpa Mencetuskan Heuristik Anti-Penyalahgunaan

4 min read Published Updated 808 words

Kebanyakan pelaksanaan had kadar API mudah dielakkan dengan putaran proksi naif — tetapi putaran yang sama itu mencetuskan heuristik anti-penyalahgunaan dalam beberapa minit. Cabaran sebenar bukan sekadar kekal di bawah had, tetapi melakukannya tanpa kelihatan seperti bot. Ini memerlukan pemahaman tentang dua skop had kadar yang berbeza (per-IP dan per-kunci), menggunakan backoff dengan jitter, merawak putaran kolam, dan membentuk pemasaan permintaan untuk meniru trafik organik. Corak token bucket, yang dilapiskan pada kolam proksi, menyediakan asas yang kukuh.

Per-IP vs Per-Kunci: Dua Paksi Had Kadar

Had kadar beroperasi pada sekurang-kurangnya dua dimensi bebas: IP sumber dan kunci API (atau token). Satu kunci mungkin membenarkan 5,000 permintaan sejam (had terautentikasi GitHub), tetapi kunci yang sama dari satu IP mungkin dihadkan pada siling letusan yang lebih rendah. Permintaan tidak terautentikasi lebih terhad — biasanya 60 permintaan sejam per IP. Mengabaikan dimensi IP adalah cara terpantas untuk mencetuskan respons 429 Too Many Requests (RFC 6585). Kolam proksi mesti mengagihkan permintaan merentas berbilang IP untuk mengelakkan memenuhi mana-mana asal tunggal, tetapi setiap IP masih berkongsi kunci yang sama. Jika had global kunci ialah 5,000/jam dan anda mempunyai 50 proksi, setiap proksi hanya boleh membuat 100 permintaan sejam sebelum kaunter peringkat kunci diaktifkan. Petakan kedua-dua had sebelum menulis sebarang baris kod.

Backoff dengan Jitter: Perbezaan Antara Sopan dan Boleh Diramal

Backoff eksponen tanpa jitter adalah cap jari. Pelayan melihat permintaan tiba pada selang yang berganda sempurna dan menandakannya sebagai automatik. Penyelesaiannya ialah jitter penuh: sleep(random.uniform(0, min(cap, base * 2 ** attempt))). Ini menyebarkan percubaan semula merentas tetingkap masa, menjadikan corak tidak dapat dibezakan daripada letusan pengguna sebenar. Dokumentasi AWS sendiri mengenai backoff eksponen mengesyorkan jitter atas sebab ini. Coretan Python berikut melaksanakan token bucket yang mengisi semula pada kadar tetap, tidur dengan jitter apabila kosong, dan memutarkan proksi pada setiap perolehan yang berjaya:

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

Putaran Kolam Proksi: Rawak, Jangan Round-Robin

Round-robin berurutan melalui senarai proksi boleh dikesan. Pelayan yang melihat permintaan daripada 1.2.3.4, kemudian 5.6.7.8, kemudian 9.10.11.12 secara serentak akan mengaitkan corak merentas IP. Sebaliknya, pilih proksi secara rawak dengan penggantian, dan tambah tempoh sejuk per-proksi. Direktori proksi awam melaporkan kadar kegagalan 60–80% — proksi yang mengembalikan 429 atau ralat sambungan harus diturunkan ke baris gilir mati sekurang-kurangnya 60 saat. Laksanakan pemilihan rawak berwajaran di mana proksi yang baru berjaya lebih berkemungkinan dipilih. Token bucket di atas menggunakan rotate(1) mudah untuk kejelasan; dalam pengeluaran, gantikannya dengan random.choice() dan jejaki kiraan kegagalan per-proksi.

Pembentukan Permintaan: Tiru Purata Tanpa Melampaui Garis

Walaupun dengan token bucket dan kolam proksi yang sempurna, kadar permintaan tetap tepat 2.0 sesaat adalah tidak semula jadi. Pengguna sebenar berhenti seketika, mengumpulkan, dan mengubah selang antara permintaan. Tambah kelewatan rawak kecil (cth., time.sleep(random.uniform(0.1, 0.5))) sebelum setiap panggilan acquire(), dan ubah kadar token bucket sebanyak ±10% setiap beberapa minit. Walau bagaimanapun, jangan tambah kelewatan yang melebihi tetingkap had kadar yang didokumenkan API — itu menjejaskan tujuan. Matlamatnya adalah untuk kekal dalam had sambil kelihatan sebagai sekumpulan pelanggan yang sah. Pembentukan berlebihan (cth., memasukkan kelewatan menaip seperti manusia) adalah usaha yang sia-sia; API mengambil berat tentang kekerapan permintaan, bukan pemasaan antara ketukan kekunci.

Mitigasi Etika: Tahu Di Mana Garisnya

Setiap teknik yang diterangkan di sini adalah pengoptimuman yang sah — sehingga ia melanggar Terma Perkhidmatan API. Kebanyakan ToS secara jelas melarang "menghindari had kadar" atau "menggunakan cara automatik untuk mengakses perkhidmatan." Membaca ToS bukan pilihan. Jika API membenarkan 100 permintaan seminit per kunci, menggunakan 50 proksi setiap satu membuat 2 permintaan seminit adalah patuh. Menggunakan 100 proksi setiap satu membuat 100 permintaan seminit adalah tidak — ia adalah penyalahgunaan, tidak kira betapa pintarnya jitter anda. Perbezaannya adalah niat dan jumlah. Token bucket yang dilapiskan pada kolam proksi adalah alat; alat yang sama yang mengikis direktori awam secara etika juga boleh digunakan untuk DDoS API kecil. Dokumentasikan sasaran had kadar anda, audit log anda untuk 429, dan jangan sekali-kali melebihi siling per-kunci yang didokumenkan. Jika anda memerlukan lebih daya pemprosesan, minta had yang lebih tinggi atau bayar untuk pelan khusus.