搜索 API 延迟优化:并发数、超时与超时的真实代价

搜索 API 延迟优化:并发数、超时与超时的真实代价

同一个 API key,有人跑 1000 个词要一个半小时,有人二十分钟跑完------差别往往不在代码,在三个参数的配合:并发数、超时值和重试策略。配错了不只是慢,还会多扣 credits:官网明确"Executed failures may consume credits; an unknown execution or timeout is not automatically refunded"------超时的请求可能照扣费。

这篇讲清三件事:并发数和 QPS 的关系、超时值怎么定、以及为什么"超时后立即重试"是最贵的操作。字段与计费口径以 SerpBase 官方文档的端点与计费说明 为准;search/news/videos 每次 1 credit。

并发数的上限不是你的线程数

错误码 1029 的描述是"QPS or concurrency limit exceeded"------QPS 和并发是两个独立维度,哪个超了都报同一个码。价格页按套餐给 QPS 档位(Starter 2 QPS、Growth 5、Pro/Business 15、Enterprise 50),并发上限文档没给具体数字,但可以确定:它不是无限大。

这意味着"开 32 个线程并发跑"不一定更快------超出限制的部分都在收 1029,重试还继续占额度。真正的最优并发是:

有效吞吐 = min(套餐 QPS, 并发上限) × 单请求成功率

调优的第一步不是加线程,是测出自己 key 的实际档位:固定间隔探测,看有没有 1029。没超就缩短间隔再测,直到出现 1029,那个间隔就是你当前的安全线。

超时值:太长和太短都花钱

超时值设多少,是个双向成本问题:

超时设置 问题
太短(如 3 秒) 正常慢请求被误杀,重试一次 = 多一次请求,而且被杀的请求可能已经扣费
太长(如 120 秒) 真故障时线程全卡死,批量任务尾延迟被拖长,后面全排队

合理起点:先探到 p50 和 p95 基线,超时设 p95 的 2~3 倍。比如实测 p50 400ms、p95 1.2s,超时设 3~4 秒比较稳------既容得下偶发慢请求,又不会让线程卡太久。

还有一个容易忽略的点:重试的超时应该比首次短。首次给 4 秒,重试给 2 秒------第一次慢可能是冷启动,重试还慢大概率是真有问题,别把线程继续押上。

带预算感知的调优客户端

python 复制代码
import random, threading, time
from concurrent.futures import ThreadPoolExecutor

import requests

API = "https://api.serpbase.dev/google/search"
KEY = "你的 API Key"

# 基于探测结果的配置:示例按 5 QPS 档设计
QPS = 5.0
CONCURRENCY = 4
TIMEOUT_FIRST = 4.0
TIMEOUT_RETRY = 2.0
MAX_ATTEMPTS = 3

_gate = threading.Semaphore(CONCURRENCY)
_pacer = threading.Lock()
_next_slot = [0.0]

def acquire_slot() -> None:
    """全局匀速放行,把并发请求摊到 QPS 预算内。"""
    with _pacer:
        now = time.monotonic()
        start = max(now, _next_slot[0])
        _next_slot[0] = start + 1.0 / QPS
    delay = start - now
    if delay > 0:
        time.sleep(delay)

def search_once(q: str, timeout: float) -> dict:
    resp = requests.post(API,
        headers={"X-API-Key": KEY, "Content-Type": "application/json"},
        json={"q": q, "page": 1}, timeout=timeout)
    return resp.json()

def search(q: str) -> dict:
    with _gate:
        for attempt in range(1, MAX_ATTEMPTS + 1):
            acquire_slot()                      # 先抢 QPS 配额再发请求
            timeout = TIMEOUT_FIRST if attempt == 1 else TIMEOUT_RETRY
            try:
                data = search_once(q, timeout)
            except requests.Timeout:
                # 超时最贵:服务端可能已执行并计费,重试前先退避
                time.sleep((2 ** attempt) + random.random())
                continue
            code = data.get("status")
            if code == 0:
                return data
            if code == 1029:                     # 被限流:退避后重试
                time.sleep((2 ** attempt) + random.random())
                continue
            if code in (1000, 1001, 1020):       # 不可重试
                raise RuntimeError(f"{code}: {data.get('error')}")
            if code in (1500, 1502, 1503, 1504):
                time.sleep((2 ** attempt) + random.random())
                continue
            if code == 1004:
                return {"status": 1004, "empty": True}
        return {"status": -1, "error": "exhausted"}

def batch(keywords: list, workers: int = CONCURRENCY) -> list:
    results, stats = [], {"ok": 0, "empty": 0, "failed": 0}
    with ThreadPoolExecutor(max_workers=workers) as pool:
        for data in pool.map(search, keywords):
            if data.get("status") == 0:
                stats["ok"] += 1
                results.append({"q": data["query"], "organic": len(data.get("organic", []))})
            elif data.get("empty"):
                stats["empty"] += 1
            else:
                stats["failed"] += 1
    print(stats)
    return results

if __name__ == "__main__":
    batch(["serp api", "rank tracker", "google maps api"] * 5)

这段代码的核心是 acquire_slot:用一个全局 pacing 把并发请求摊到 QPS 预算内------不是"并发发了再说",而是从源头就不超速。这比"超了再退避"高效得多,因为退避本身也是请求。

三条调优经验

  1. 超时的请求按已扣费记账。 官网说得很清楚:已执行的失败可能扣费,超时和无法判定的执行不自动退款。所以客户端超时后直接抛异常是最坏选择------本地认为失败了,服务端可能正在执行并且计费。至少要把 credits_charged 从异常路径里能拿到的信息记下来,批量结束后用 GET /account/credits 对账(0 credits,限流 60 次/分),差额就是"你以为没花其实花了"的部分。
  2. 并发别超过 QPS 档位太多。 5 QPS 的 key 开 4 个 worker 是合理的(留余量给重试),开 16 个就是自己制造 1029。判断方法:跑 100 个词,如果 stats 里 1029 重试占比超过 10%,说明并发超过承载了,降下来。
  3. 尾延迟比平均值重要。 批量任务的总时长由最慢的那个请求决定,不是平均值。如果 p50 300ms 但 p99 5 秒,你的 1000 词任务会被最后几十个词拖死------这时候该调的是超时和重试策略,不是并发。

FAQ

怎么测自己 key 的 QPS 档位? 固定间隔探测:从每秒 1 次开始,没遇到 1029 就提速到 2 次、2.5 次,遇到就退回上一档。价格页有套餐档位表(2/5/15/50),实测结果应该能对上你的套餐。

超时后重试,服务端会执行两次吗? 可能。这正是超时"贵"的原因------本地看到超时,但服务端的 worker 可能还在跑并且完成了请求。所以退避要够长,别让重试和原请求撞在一起。

并发和 QPS 哪个先超? 取决于配置。窄并发(2-4 worker)一般是 QPS 先超;宽并发(16+ worker)可能并发先超。两者都报 1029,只能靠降并发和降速率分别试出来。

超时值设多少合适? 没有普适值,先测基线:p50 的 2-3 倍作为起点,再根据批量任务的尾延迟表现调整。记住重试的超时要比首次短。

先用探测脚本测出你的实际 QPS 档位,再把客户端改成"抢配额再发请求"的模式------同样的 key,吞吐可能差一倍,而且少扣一堆冤枉 credits。

相关推荐
wangqiaowq1 小时前
PII 脱敏指的是:把个人身份信息(PII)中能识别到具体个人的敏感部分,用替换、遮蔽、变形等方式处理掉,使得数据在保留可用性的同时,不再直接暴露个人身份。
python
风早爽太1 小时前
Python 学习笔记:SQLModel 使用外键关联查询数据
python·fastapi·sqlmodel
专业程序开发源1 小时前
springboot社区养老系统44071-计算机课程设计、毕业设计
vue.js·spring boot·后端·python·django·php·课程设计
在世修行2 小时前
干货:左右分栏调试工具设计
python·调试设计
零基础1232 小时前
DeepSeek V4.1 Flash (Batch) 的性能测试与应用
经验分享·python·语言模型·vllm
Yyyyyy~2 小时前
【Anaconda】安装
人工智能·python
LOVE️YOU2 小时前
Python 函数名、函数对象与“把函数作为参数传递”
开发语言·python
2601_957883843 小时前
2026年9月:雷神笔记本售后相关资讯
python·电脑
天赐范式3 小时前
天赐范式第185天:让漂变开始定量——扫N看选择主导边界
python·信噪比·数字生命·天赐范式·动态运行时·种群大小·遗传漂变