批量请求 X 接口:那个没人测过的 14 倍差距

14 min readSocialAPI 工程团队

批量请求 X 接口:那个没人测过的 14 倍差距

大多数读 X 数据的代码都是一次处理一条,因为问题本来就是这么描述的:取这条推文、取那个资料。循环是自然而然写出来的,它能跑,而且看起来没有任何不对。

★★和批量版本实测对比,那个循环为同样的数据多花了大约 14 倍流量和 14 倍墙钟时间。★★

这一页讲清楚批量到底省了什么、什么时候它帮不上忙,以及★一个没人提醒就会让你的结果悄悄虚高的返回结构行为★。


实测差距

取 5 条推文,逐条 vs 一次批量:

逐条 批量
流量 基准 ★省 14.3 倍★
墙钟耗时 基准 ★快 14.7 倍★
配额消耗 基准 ★省 5 倍★

差距为什么这么大: 每一个单独的请求都要完整偿还一次往返的成本 —— 连接、请求头、每个响应的信封 —— ★而换来的只有一条数据★。批量把这些全部摊薄到一百条上。

⚠️ ★注意配额那一列和流量那一列是不同的比例。★ 流量和延迟随往返次数变化,配额随服务商怎么给批量定价变化。两者是分开的节省,混为一谈会导致容量规划算错。

批量形态覆盖推文和资料页 —— 正是人们最常拿来做循环的那两样。


什么能批量

两种形态覆盖了大多数真实工作负载:

import requests

BASE = "https://api.socialapi.tech"
KEY  = "your_api_key"
HDRS = {"X-API-Key": KEY}

def posts(ids):
    """一次最多 100 个推文 ID。"""
    r = requests.get(f"{BASE}/v1/tweets/batch",
                     params={"ids": ",".join(ids)}, headers=HDRS, timeout=60)
    r.raise_for_status()
    return r.json()["data"]

def profiles(usernames):
    """一次取多个资料页。"""
    r = requests.get(f"{BASE}/v1/batch/user_info",
                     params={"usernames": ",".join(usernames)},
                     headers=HDRS, timeout=60)
    r.raise_for_status()
    return r.json()["data"]

# ★按批量上限切块 —— 不要一次塞 500 个 ID 然后祈祷★
def chunked(seq, n=100):
    for i in range(0, len(seq), n):
        yield seq[i:i+n]

# post_ids: 你想要的推文 ID 列表, 比如来自一次搜索或你自己的库
post_ids = ["1234567890123456789", "1234567890123456790"]
all_posts = [p for chunk in chunked(post_ids) for p in posts(chunk)]

★那个切块函数不是可选的。★ 批量端点有上限,超过它会失败或截断 —— ★而截断才是危险的那个,因为它长得像成功★。


★那个返回结构陷阱★

下面这个行为,不处理就会污染你的数据。

向 X 底层批量调用请求 50 个推文 ID,回来的可能是 59 条。

★多出来的是真实推文 —— 它们是你请求的那些推文所引用、所回复的帖子。★ 平台带上它们,是因为渲染上下文需要。

⚠️ 为什么这比听起来更糟:

  • 你的计数是错的 —— 预期 50 结果 59
  • 如果你按返回条数计费,你的计费是错的
  • ★你的分析里悄悄混进了你从没请求过的账号的帖子★

★修法是拿响应去和你请求的 ID 集合做过滤。★ 我们在服务端做掉了这件事,所以批量调用返回的恰好就是你要的那些,不多不少 —— 但如果你是直接调平台,这个过滤得你自己写。

requested = set(ids)
clean = [p for p in response if p["id"] in requested]   # ★永远别省这一步★

什么时候批量帮不上忙

批量不是永远更好,有三种情况值得知道:

1. 你只要一条。 ★一条的批量,只是一个语法更啰嗦的普通请求。★

2. 条目是顺序发现的。 如果 2 号 ID 依赖 1 号的结果,就根本组不成批量 —— 那是一条流水线,不是批量取数。

3. 延迟比吞吐更重要。 ★批量要等它最慢的成员返回。对一次交互式查询,一个小请求比一个要等 99 条的批量更快。★

规则: ★当你手上一开始就有完整清单、而且全都要时才批量。不要为了显得聪明而批量。★

批量与复合两种形态在文档里放在一起 —— 选哪个取决于你的访问模式。


另一种形态:复合调用

当你需要关于同一个对象的好几样不同东西时,适用另一种节省。

资料 + 最近推文 + 账号详情,朴素做法是三个调用。★复合端点把它们并发跑掉,返回一个响应★ —— 省的是延迟而不是配额,因为那些活儿仍然要干。

def whois(username):
    """资料 + 账号详情 + 最近推文,并发。"""
    r = requests.get(f"{BASE}/v1/user/whois",
                     params={"username": username, "tweet_limit": 10},
                     headers=HDRS, timeout=60)
    r.raise_for_status()
    return r.json()["data"]

★做单个账号的尽调用复合;对很多账号取同一个字段用批量。★ 它们解决的是不同问题,而且经常被搞混。


大家会问的问题

怎么批量请求推特接口? 在一个调用里发多个 ID 或用户名,而不是循环。★按端点上限切块。★

批量能省多少? 推文实测省 14.3 倍流量、14.7 倍时间,★外加另一笔 5 倍的配额节省★。

批量为什么快这么多? 往返开销只付一次而不是每条付一次。 ★数据本身从来就不是贵的那部分。★

一批能放多少条? 最多 100 个推文 ID。 ★超过就切块★ —— 不要指望服务端替你拆。

超过上限会怎样? 失败或截断。 ★截断是危险的那个,因为它长得像成功。★

为什么回来的推文比我要的多? ★被引用和被回复的帖子会作为上下文一起来。★ 拿你请求的 ID 集合去过滤。

多出来的数据要我付钱吗? 在我们这里不用 —— 我们在服务端过滤掉了。 ★直接调平台的话,你为不想要的东西付了钱。★

资料页能批量查吗? 能 —— 一个调用查多个用户名,★任何排行或对比都该这么建★。

粉丝列表能批量吗? 不能。 ★列表是分页的,不是批量的★ —— 不同的问题,不同的机制。

搜索能批量吗? 不能。 每个查询是它自己的一次搜索。★批量适用于取已知的条目。★

一次批量调用在限流上算一个请求吗? 算 —— 这正是配额节省与流量节省是两笔账的原因。

是不是应该永远批量? 不是。 ★单条不用、顺序发现的不用、延迟优先于吞吐时也不用。★

批量会改变数据吗? 不会 —— 同样的字段、同样的新鲜度。★变的只是传输方式。★

批量里有一个 ID 无效会怎样? 它在响应里缺席。 ★拿返回的 ID 和请求的 ID 做对比★,不要假设一一对应。

怎么知道哪些失败了? 请求集合与返回集合的差集。 ★已删除的推文和无效 ID 长得一模一样。★

能混着放推文 ID 和用户名吗? 不能 —— 那是两个不同端点、不同形态。

复合端点是什么? 一个调用并发跑好几种不同的查询,再一起返回。

什么时候用复合、什么时候用批量? ★复合是"一个对象的很多字段";批量是"很多对象的同一个字段"。★

复合能省配额吗? 主要省延迟。 ★底层的活儿照样要干★ —— 只是并行了。

批量更难排查问题吗? 略难 —— 一次失败影响整个调用。★把请求的 ID 集合记下来★,这样能精确重试。

失败的批量该整批重试吗? 通常该,配合退避。★失败就拆成单条会让批量失去意义★,除非你是在定位某个坏 ID。

批量对限流有帮助吗? 帮助很大 —— 触发限流的是请求次数,不是数据大小。

翻页时怎么高效批量? 翻页时收集 ID,之后再批量取详情。 ★两阶段优于交错进行。★

批量能并发吗? 能,适度。 ★并发会成倍放大请求速率★,所以要配合退避。

批量会保持顺序吗? 不要假设它会。 ★按 ID 索引响应★,不要按位置。

批量里有已删除的推文怎么办? 它就是缺席 —— 删除意味着什么。

每条的成本有差别吗? 批量的每条单价更低,这正是它的意义。

这对大规模导出影响大吗? 非常大 —— 导出模式正是批量收益最高的场景。

能跨账号批量吗? 资料页可以,时间线不行。 ★时间线是按账号分页的。★

批量最大的一个错误是什么? ★没有拿响应去和请求的内容做过滤★ —— 它会以一个看起来很合理的幅度让计数虚高。


简短版

★一条一条循环,比批量取同样的数据多花约 14 倍流量和时间,而配额节省是另外的 5 倍。★ 大多数代码之所以循环,是因为问题被一条一条地描述,而不是因为有人做过选择。

★★那个陷阱:请求 50 个推文 ID 可能回来 59 条 —— 被引用和被回复的帖子会作为上下文一起来。拿请求的 ID 集合去过滤,否则你的计数、你的计费、你的分析会一起以一个看起来很合理的幅度漂移。★★

我们的 API 支持推文与资料页批量、把响应过滤成恰好你要的那些,并在你需要同一账号的多种信息时提供复合调用。只读、固定按次价格。

批量做不到什么: 把一个分页列表变成一个请求,或者批量做搜索。★那是不同的机制,而把它们当成可批量的,正是大多数性能优化走错的地方。★

延伸阅读:什么才真正触发限流 · 用得上批量的导出模式 · 为什么数字 ID 才是对的键 · 翻页取粉丝列表。