并行运行请求
你将学到什么
如何通过限制保持打开的调用数量,并在遇到拒绝时做出相应处理而非盲目重试,来并发运行大批量 FourA request 并避免触发 429 错误。
前置条件
- 一个 FourA API key(在此获取)
- Python 3.9+ 及
requests,或 Node.js 18+
你需要应对的限制
你的套餐对每个 endpoint 设有两个上限:可同时运行的 request 数量,以及每分钟可发起的 request 数量(Browser 采用按日配额而非按分钟配额)。Single、Proxy 和 Browser 各有独立配额,Usage & Limits 的 Limits & Features 标签页列出了这些数值。
超出并发上限的 request 会返回 HTTP 429,并在 body 中包含 X-FourA-Limit: plan_limit_concurrency 及对应上限值:
{
"error": "Concurrency limit reached: your plan allows 50 simultaneous single request(s). Retry when an in-flight request finishes.",
"reason": "plan_limit_concurrency",
"documentation": "https://foura.ai/prices",
"limit": 50,
"in_flight": 51,
"retry_after_seconds": 1
}
超出每分钟上限的请求会返回 X-FourA-Limit: plan_limit_rate,并且 retry_after_seconds 会指明还需等待多久才能再进入一个请求(1 到 120 秒)。
两种拒绝都是即时的。FourA 不会对调用进行排队并在稍后返回,因此不会产生任何消耗,也不会计费。被拒绝的调用不计入滑动分钟窗口,因此请求量略微超出上限的账户仍可获得完整的上限配额。重试风暴有单独的处理方式:当被拒绝的调用持续以超过每分钟上限两倍的速率到达时,该 endpoint 上的所有调用都会被拒绝,并返回 cooldown: true 和 retry_after_seconds: 30,因此请暂停这 30 秒。完整字段参考:Rate Limits。
人们经常忽略的两点:
- 一个
POST /api/auto/调用本身不占用槽位,但它为你发起的每个 Single、Proxy 和 Browser 子调用都会占用。一个 auto 调用在其梯级策略运行时可能会占用多个槽位。(在控制面板中,auto 调用是 request,其子调用是 attempts。) - Browser 请求占用槽位的时间取决于页面渲染所需的时间,这远长于 Single 请求。一批 browser 调用比一批 single 调用更快达到上限。
第 1 步:限制自身的并发量
选择一个低于所调用 endpoint 上限的数值并保持它。Worker pool 只需一行代码即可实现:
import os
import concurrent.futures
import requests
API = "https://eu.api.foura.ai/api/single/"
KEY = os.environ["FOURA_API_KEY"]
HEADERS = {"X-API-Key": KEY, "Content-Type": "application/json"}
# Below your plan's Single concurrency, so a slow request never pushes the batch over it.
MAX_IN_FLIGHT = 30
def fetch_one(url):
resp = requests.post(API, headers=HEADERS, json={"method": "GET", "url": url}, timeout=60)
return url, resp.status_code, resp.json()
def fetch_all(urls):
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_IN_FLIGHT) as pool:
for outcome in concurrent.futures.as_completed(pool.submit(fetch_one, u) for u in urls):
results.append(outcome.result())
return results
max_workers 就是全部机制。连接池中的并发调用数绝不会超过该值,因此即便批处理包含 100 万个 URL,也不会超出限制。
预留余量。如果您这边的两个进程使用同一个账户,即使使用不同的 API key,它们也会共享同一个上限,因此请为每个进程分配一半。如果目标站点响应较快,使连接池在 1 分钟内完成的请求数过高,请调整 worker 速率以确保不超出每分钟上限。
步骤 2:退避而非立即重发
如果收到拒绝响应,立即重发该批次是错误的做法。其中的每次调用都会再次被拒,重试请求还会叠加在已运行的请求之上。
先等待。等待时长可通过 Retry-After header 以及响应体中的 retry_after_seconds 获取:
import time
# Plan limits that no short wait will clear.
STOP_ON = {"plan_limit_browser_daily", "plan_limit_credits", "plan_limit_bandwidth", "plan_limit_feature", "plan_limit_premium"}
def fetch_one(url, attempts=4):
for attempt in range(attempts):
resp = requests.post(API, headers=HEADERS, json={"method": "GET", "url": url}, timeout=60)
limit = resp.headers.get("X-FourA-Limit")
if limit in STOP_ON:
raise RuntimeError(f"stopped by {limit}")
if resp.status_code not in (429, 503):
return url, resp.status_code, resp.json()
header = resp.headers.get("Retry-After")
if header and header.isdigit():
wait = int(header)
else:
body = resp.json()
wait = body.get("retry_after_seconds") or body.get("retryAfter") or 2 ** attempt
time.sleep(wait)
return url, 429, {"error": "still refused after retries"}
运行多个 worker 时请添加抖动(jitter)。如果不加,同一秒内被拒绝的每个 worker 都会在同一秒被唤醒,并再次一同被拒绝。
import random
time.sleep(wait + random.uniform(0, 0.5))
步骤 3:在 Node 中实现相同逻辑
const API = 'https://eu.api.foura.ai/api/single/';
const HEADERS = {
'X-API-Key': process.env.FOURA_API_KEY,
'Content-Type': 'application/json',
};
const MAX_IN_FLIGHT = 30;
async function fetchOne(url) {
const resp = await fetch(API, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ method: 'GET', url }),
});
return { url, status: resp.status, body: await resp.json() };
}
async function fetchAll(urls) {
const queue = [...urls];
const results = [];
async function worker() {
while (queue.length) {
results.push(await fetchOne(queue.pop()));
}
}
await Promise.all(
Array.from({ length: Math.min(MAX_IN_FLIGHT, urls.length) }, worker)
);
return results;
}
固定数量的 worker 从单个队列中拉取任务,无论批次大小如何,都能保证并发调用数严格受控。
步骤 4:监控实际用量
在 控制台中打开用量与限制。上限旁的实时计数器会动态显示并发数、每分钟速率以及每日浏览器 request 数,方便你根据实际运行情况而非猜测来调整 MAX_IN_FLIGHT。
活动日志也会记录拒绝情况。如果一次运行最终处理了正确数量的行,但零星出现了 rate_limit 结果,说明它触发了自身限流。
常见错误
- 一次性触发整个列表。 对 5,000 个 URL 执行
asyncio.gather,或对无界数组执行Promise.all,会发起 5,000 个调用。应当限制并发池大小,而不是限制列表。 - 并行重试。 在请求被拒绝的瞬间立即重发,会再次引发导致拒绝的突发流量。请等待
Retry-After,并加入随机抖动。 - 将每日或周期配额当作临时等待。
plan_limit_browser_daily会在 UTC 午夜清零,plan_limit_credits和plan_limit_bandwidth会在计费周期结束时重置。应停止运行,并在响应体存在时读取resets_at。 - 将 auto 调用计为单个槽位。
/api/auto/内部的阶梯机制会产生真实的子调用,上限统计的是这些子调用。 - 跨进程共享密钥却未分配预算。 配额上限基于账户计算,而非基于进程。
- 为所有 endpoint 配置统一大小的池。 Single、Proxy 和 Browser 各自拥有独立的并发配额。针对 Single 配置的池大小可能会超出较小的 Browser 上限。
相关内容
- Rate Limits:所有拒绝形式及各个字段的含义
- Response Headers:
X-FourA-Limit与Retry-After - Request Outcomes:为什么
rate_limit结果绝不会计费 - 用量与限制:实时计数器与套餐额度所在位置
- Smart Fetch (Auto):单个 auto 调用如何转化为多个子调用