300个站同时推百度API,3天就被封了接口,换成令牌桶中间件用两个月零封号,限流策略比单纯控制QPS管用得多
2024年底帮一个做外贸站群的朋友排查收录问题,发现他300多个站已经整整两周没有新页面被收录了。查了一圈,既不是内容质量问题,也不是网站被降权,百度站长平台里的推送记录全部显示"推送失败:接口调用次数超限"。他的做法是把所有站点的URL收集起来,写了个cron脚本每隔5分钟就往百度API推一批,300个站共用同一个token,三天就把接口推废了。
做站群的、跑AI内容管线的、对接多个第三方API的,迟早都会撞上这个问题。不是某个工具不好用,是批量API调用这件事如果不做限流,不管换什么工具都会翻车。这篇文章不讲单个API的对接方式,专门讲怎么在批量调用场景下把限流做好——从算法原理到落地工具,从单机方案到站群级统一限流。
四个关键认知,先讲清楚再往下看
| 1 | 被封不是因为"请求太多",而是"请求太快"——同样是300个站推3000条URL,分散到24小时推和集中在1小时推,结果是天壤之别 |
| 2 | 每个API的限流逻辑不一样——百度API是按token+IP的QPS限制,IndexNow有每小时上限,AI接口按token消耗+并发数双重限流 |
| 3 | 简单sleep()只能解决一时的问题,站群级场景必须上令牌桶或漏桶算法,配合队列做削峰填谷 |
| 4 | 最容易被忽略的是多站点共用一个API key时的"隐性竞争"——A站狂推把配额耗光,B站C站跟着陪葬,这个问题不解决,限流做再多也是白费 |
一、sleep()为什么不够用:从最简单的限流方式说起
写爬虫和做API对接的人,第一反应基本都是加一句sleep(1)。单机单任务场景下,这确实管用。但站群场景有三个问题sleep()搞不定。

第一个问题是"并发穿透"。如果你开了5个goroutine或者5个worker进程同时推百度API,每个worker里都写了sleep(1),实际效果是每秒发出5个请求而不是1个。sleep()是线程级别的,管不了跨线程的总速率。
第二个问题是"尖刺效应"。sleep()的逻辑是"发一个请求→等N秒→发下一个",但如果API响应本身就慢(比如百度推送有时候要1-2秒才返回),实际请求间隔就变成了sleep时间+响应时间,速率不可控。更糟的是,如果某个请求超时重试,打乱了节奏,后续请求可能扎堆发出。
第三个问题是"队列堆积"。站群场景下,可能同时有几百个站的内容需要推送,用sleep()慢慢推,后面积压的URL越来越多,等排到的时候文章可能已经过了最佳推送窗口。
sleep()适用的场景:单机、单站点、偶尔推送几篇新文章、API调用量远低于配额上限。超出这个范围,就得换方案了。
二、令牌桶 vs 漏桶:两种算法,场景不同
做API限流绕不开两个核心算法:令牌桶(Token Bucket)和漏桶(Leaky Bucket)。它们解决的问题类似,但适用场景差别很大。
令牌桶的思路是:系统以固定速率往桶里放令牌(比如每秒放2个),每个请求必须从桶里取一个令牌才能发出去,桶有最大容量(比如10个)。如果桶里没令牌了,请求就得等。桶的容量就是"突发容量"——如果之前没请求,桶里攒了10个令牌,可以一口气发10个请求。
漏桶的思路反过来:请求先进入一个队列(桶),桶以固定速率往外"漏"请求去调用API。不管请求来得多猛,出去的速率是恒定的。桶满了就拒绝新请求。
对SEO批量推送来说,令牌桶更合适。百度API、IndexNow这些接口本身允许一定的突发——比如QPS限制是2,但你可以一次性发10条,只要平均下来不超过2 QPS就行。令牌桶的"桶容量"正好匹配这种"允许短期突发、平均速率受限"的场景。漏桶强制匀速输出,反而浪费了API的突发容忍度。
| 对比维度 | 令牌桶 Token Bucket | 漏桶 Leaky Bucket |
|---|---|---|
| 核心逻辑 | 固定速率生产令牌,请求消费令牌 | 请求进入队列,固定速率漏出执行 |
| 突发支持 | 支持,桶容量就是突发上限 | 不支持,强制匀速输出 |
| 队列特性 | 无队列,令牌不够就等待 | 有队列,队列满了就丢弃 |
| 适合API类型 | 允许短期突发的API(百度推送、IndexNow) | 严格匀速限制的API(支付接口、金融交易) |
| SEO推送推荐度 | 五颗星 | 三颗星 |
三、落地工具:5个可以直接用的限流方案
理论讲完了,下面是直接能用的方案。按部署复杂度从低到高排列,根据当前技术栈和站点规模选。
1. PHP:bandaid/rate-limit
GitHub开源,支持令牌桶和漏桶两种算法,存储后端支持Redis、Memcached、APCu、文件。WP站群可以直接composer引入,配合Redis做跨进程令牌管理。每秒生成2个令牌,桶容量10个。
2. Python:aiolimiter
基于asyncio的异步令牌桶实现,天然支持并发场景。适合Python写的AI内容管线——一边调AI生成接口,一边控制百度推送速率。pip install aiolimiter,3行代码接入。
3. Node.js:bottleneck
npm周下载量超过700万,功能最全的Node.js限流库。支持minTime、reservoir(令牌桶)、优先级队列、集群模式(Redis)。做Node.js站群管理的首选。
4. Redis + Lua脚本
最灵活的方案:用Redis+Lua实现分布式令牌桶。Redis的原子性保证多进程共享同一个令牌桶,Lua脚本保证取令牌操作原子性。Redis官方文档有完整示例。
5. RabbitMQ + 消费者限速
用消息队列做削峰填谷:所有API请求先写入RabbitMQ队列,消费者端设置prefetch=1配合消费后sleep控制速率。完全解耦,不挑语言。
重点说一下Redis+Lua方案,因为它是站群场景下最实用的。令牌桶状态存在Redis里,所有worker共享同一个key。取令牌的Lua脚本逻辑如下:
-- Redis Lua: 分布式令牌桶取令牌local key = KEYS[1]local rate = tonumber(ARGV[1])local capacity = tonumber(ARGV[2])local now = tonumber(ARGV[3])local requested = tonumber(ARGV[4])local last_time = redis.call('get', key .. ':last')local tokens = redis.call('get', key .. ':tokens')if not last_time thenredis.call('set', key .. ':last', now)redis.call('set', key .. ':tokens', capacity)tokens = capacitylast_time = nowendlocal elapsed = now - tonumber(last_time)local new_tokens = math.min(capacity, tonumber(tokens) + elapsed * rate)if new_tokens >= requested thenredis.call('set', key .. ':last', now)redis.call('set', key .. ':tokens', new_tokens - requested)return 1elseredis.call('set', key .. ':tokens', new_tokens)return 0end核心优势是原子性——计算补充令牌、判断是否足够、扣减令牌,三步在Redis里一次完成,不会出现A进程读到旧值、B进程也读到旧值、两个都扣了的情况。300个站共享一个令牌桶,速率由rate参数统一控制。
四、四种常见API的限流策略,分别怎么配
限流方案选好了,具体到每个API的配置参数才是决定生死的关键。下面是四种SEO站群最常用的API,各自的限制规则和推荐配置。

| API | 限制规则 | 令牌桶配置建议 | 额外注意事项 |
|---|---|---|---|
| 百度URL推送 | 每日配额200-10万条不等,QPS约1-3,同一token共享 | rate=2, capacity=5 | 多站点共用token时必须用分布式令牌桶,否则一个站把配额吃完 |
| IndexNow | 无公开QPS限制,但单次最多提交10条URL,建议间隔1秒 | rate=1, capacity=10 | 一次API key提交,所有支持IndexNow的搜索引擎都会收到 |
| AI文本生成API (GPT/Claude/文心等) | 按token消耗限流(RPM/TPM),并发数限制3-10不等 | rate=3, capacity=3 | 除了请求数限流,还要监控token消耗量,别只看请求次数 |
| Google Indexing API | 每天200条配额,仅限JobPosting和BroadcastEvent类型,QPS极低 | rate=1, capacity=1 | 配额太少,只用于核心页面,不要全站推 |
最容易犯的错:百度API推送失败后立刻重试。推送返回错误码时不要秒级重试——很多失败是因为配额耗尽,重试只会让情况更糟。应该把失败的URL放回队列尾部,等令牌桶自然恢复后再推。如果是429限流错误,至少等60秒再重试,最好用指数退避(1分钟→2分钟→4分钟→8分钟)。
五、站群级统一限流:多站点共用一个API key怎么不互相伤害
单站点做限流不难,真正头疼的是多站点场景。300个站共用一套API key和令牌桶,怎么保证公平性?不能A站一天推了2000条,B站一条都推不出去。
方案是两层限流架构:
第一层:站点级限流(防单站滥用)
每个站点有自己独立的令牌桶,比如每个站每小时最多推50条URL。超过上限后该站的请求直接排队或丢弃,不进入总桶。这样即使某个站突然产生大量内容,也不会拖累其他站。
第二层:全局限流(防全站群超配额)
所有站点的请求通过第一层后,进入全局令牌桶。全局桶的rate设成百度API的总QPS限制(比如2 QPS),capacity设成5。所有站公平竞争全局令牌,谁先抢到谁先推。
两层之间用优先级队列连接。不是简单的FIFO,而是按站点权重和URL发布时间排序——新发布的文章优先级高于旧文章、核心站点优先级高于边缘站点。这样保证最重要的内容最先被推送。
落地工具方面,Redis Streams是实现这个架构的最佳选择。每个站点往自己的Stream写推送任务,消费者组从所有Stream统一消费,消费速率受全局令牌桶控制。Redis Streams自带消费者组、ACK机制、消息持久化,不怕进程挂了丢任务。
用UC建站系统做站群管理时,这套两层限流架构是内置在推送模块里的。每个站点独立部署、独立IP、独立API key,推送层面天然隔离,不存在"一个站吃光配额"的问题。多站看板统一监控每个站点的推送成功率、剩余配额、队列堆积量,哪个站推送异常一眼就能看到。双通道推送(百度API+IndexNow)的限流也做了统一调度,不会出现两个通道互相抢资源的情况。
六、监控和告警:限流做完了,怎么知道它真的在工作
限流系统上线后,最怕的是"以为在限流,其实没生效"。下面五个指标是必须监控的:
| 监控指标 | 正常范围 | 告警阈值 | 说明 |
|---|---|---|---|
| 令牌桶当前令牌数 | 在0到capacity之间波动 | 持续为0超过10分钟 | 说明请求速率持续超限,需要提高rate或减少请求量 |
| 队列长度 | 小于当日预计推送量的2倍 | 超过1000条且持续增长 | 队列堆积说明消费速度跟不上生产速度 |
| API返回429比例 | 0% | 任何非零值 | 出现429说明限流没生效或配置错误,需要立刻排查 |
| API调用成功率 | 95%以上 | 低于90% | 排除429后,失败可能是API key过期、网络问题等 |
| 各站点推送配额消耗率 | 均匀分布 | 单站消耗超过总量50% | 说明站点级限流失效,某个站在"抢跑" |
告警渠道方面,最简单的做法是用企业微信/钉钉机器人webhook,在推送脚本里加一段:当429出现或队列超过阈值时,发一条消息到工作群。不需要复杂的监控系统,十几行代码就能搞定。
七、三个被忽略的细节,才是真正导致封号的原因
限流算法和工具讲完了,但实际踩坑中,下面三个细节比算法本身更容易翻车。
细节一:User-Agent不能统一
300个站用同一个User-Agent推百度API,跟300个请求同一个IP一样危险。每个站点的推送脚本应该使用该站点的域名作为UA的一部分,或者轮换UA池。百度、Google的API网关会记录UA指纹。
细节二:Token不能跨站复用
百度站长平台的API token是按站点发放的,一个token只能推对应的站点。用A站的token推B站的URL,不仅推不进去,还可能触发风控标记。每个站必须有独立的token。
细节三:推送时间窗口很重要
不要集中在一个时间段把所有站的内容推完。分散到全天24小时均匀推送,模拟自然发布节奏。凌晨2-6点是API压力最小的时间窗口,这段时间推送成功率最高。
八、不同规模的站群,限流方案怎么选
站群规模不同,限流的复杂度差好几个量级。按规模对号入座,不用过度设计:
| 站群规模 | 推荐方案 | 关键组件 | 预估部署时间 |
|---|---|---|---|
| 1-10个站 | 单机令牌桶 + sleep | aiolimiter / bottleneck | 30分钟 |
| 10-50个站 | Redis分布式令牌桶 | Redis + Lua脚本 | 2小时 |
| 50-200个站 | 两层限流 + Redis Streams | Redis + Streams + 消费者组 | 半天 |
| 200个站以上 | UC建站系统内置限流 | 独立部署 + 独立token + 统一看板 | 系统自带,无需额外配置 |
最后说几句。API限流这件事,说起来原理不复杂,但落到站群实操里,最容易出问题的地方不是算法,而是"没想到"——没想到多站点共用token会互相挤占配额,没想到429之后秒级重试等于自杀,没想到User-Agent也能被当成风控信号。把这些细节补上,配合一个稳定的令牌桶中间件,300个站跑两个月零封号不是难事。最怕的是觉得"我就用sleep()应该没问题",然后某天早上起来发现所有站都推不动了。
