bigywater.tech
爬虫工程 · 27

限速:令牌桶与固定间隔的实际请求节奏

“我加了 sleep,应该很温和了”——如果有 20 个 worker 各自 sleep,对方看到的是 143.7 个/秒。这篇用服务器记录的请求到达时间,把几种限速写法打出的真实节奏量出来:每个 worker 各自睡、全局固定间隔、令牌桶(不同桶容量),以及间隔从完成还是从开始算起。

143.7 个/秒
20 个 worker 各自 sleep(0.1) 时服务器实际收到的速率(目标是 10 个/秒)
21
令牌桶 burst=20、rate=10/秒时,任意 0.1 秒窗口内出现的最大请求数
1.7 个/秒
服务器耗时 0.5 秒时,“完成后再等 0.1 秒”的实际速率

每个 worker 各自 sleep

实测 · 本机执行输出
   1 个 worker:100 个请求用了 15.36 秒,平均 6.4 个/秒;相邻请求最小间隔 151.1 ms,中位 155.0 ms;任意 1 秒内最多 7 个,任意 0.1 秒内最多 1 个
   5 个 worker:100 个请求用了 2.93 秒,平均 33.8 个/秒;相邻请求最小间隔 0.0 ms,中位 1.1 ms;任意 1 秒内最多 35 个,任意 0.1 秒内最多 5 个
  20 个 worker:200 个请求用了 1.39 秒,平均 143.7 个/秒;相邻请求最小间隔 0.0 ms,中位 0.1 ms;任意 1 秒内最多 140 个,任意 0.1 秒内最多 20 个

在每个 worker 里请求之间 sleep(0.1),速率随 worker 数线性增长:5 个 worker 33.8 个/秒,20 个 143.7 个/秒(1 个 worker 只有 6.4 个/秒,因为还要加上请求耗时)。要限制对一个站点的总速率,limiter 必须是全局共享的。

全局固定间隔

实测 · 本机执行输出
   1 个 worker:60 个请求用了 5.90 秒,平均 10.0 个/秒;相邻请求最小间隔 98.9 ms,中位 100.0 ms;任意 1 秒内最多 11 个,任意 0.1 秒内最多 2 个
   5 个 worker:60 个请求用了 5.90 秒,平均 10.0 个/秒;相邻请求最小间隔 96.9 ms,中位 100.0 ms;任意 1 秒内最多 11 个,任意 0.1 秒内最多 2 个
  20 个 worker:60 个请求用了 5.90 秒,平均 10.0 个/秒;相邻请求最小间隔 98.9 ms,中位 100.0 ms;任意 1 秒内最多 11 个,任意 0.1 秒内最多 2 个

用一把锁让所有请求排队,任意两个请求的开始至少隔 0.1 秒,速率恒定 10.0 个/秒,和 worker 数无关,最小间隔约 100 ms。

令牌桶

实测 · 本机执行输出
  burst= 1:100 个请求用了 9.99 秒,平均 9.9 个/秒;相邻请求最小间隔 99.5 ms,中位 101.0 ms;任意 1 秒内最多 10 个,任意 0.1 秒内最多 2 个
  burst= 5:100 个请求用了 9.50 秒,平均 10.4 个/秒;相邻请求最小间隔 0.1 ms,中位 100.0 ms;任意 1 秒内最多 15 个,任意 0.1 秒内最多 6 个
  burst=20:100 个请求用了 8.00 秒,平均 12.4 个/秒;相邻请求最小间隔 0.0 ms,中位 99.8 ms;任意 1 秒内最多 30 个,任意 0.1 秒内最多 21 个

令牌桶限制的是平均速率,同时允许桶容量那么大的突发。burst=1 时和固定间隔几乎一样;burst=5 时 0.1 秒内最多 6 个;burst=20 时 0.1 秒内最多 21 个,整体平均速率也高于设定的 10 个/秒(12.4 个/秒,因为开头的突发)。对对方服务器友好,通常把桶设小。

空闲之后的突发

实测 · 本机执行输出
  第一批(桶是满的):前 0.5 秒内 15 个,前 0.1 秒内 11 个,30 个用了 2.00 秒
  第二批(空闲 2 秒之后桶又满了):前 0.5 秒内 15 个,前 0.1 秒内 11 个,30 个用了 1.99 秒

桶在空闲期间会装满:停 2 秒后再发,前 0.1 秒内又出现 11 个请求。“平均速率受限”不等于“任何时刻都平稳”。

从哪里算起

实测 · 本机执行输出
  服务器耗时 0.05 秒:完成后再等 0.1 秒 → 平均 6.4 个/秒(相邻开始间隔中位 156 ms);开始到开始隔 0.1 秒(允许重叠)→ 平均 10.0 个/秒(中位 100 ms)
  服务器耗时 0.5 秒:完成后再等 0.1 秒 → 平均 1.7 个/秒(相邻开始间隔中位 606 ms);开始到开始隔 0.1 秒(允许重叠)→ 平均 10.0 个/秒(中位 100 ms)

每个请求完成后再等一段时间,速率取决于对方的响应速度(0.05 秒时 6.4 个/秒,0.5 秒时 1.7 个/秒);按开始时间隔开,才能稳定地控制发出速率,并允许请求重叠。哪种更合适取决于你想保护的是对方的什么——这里只量了速率,没有量对方的负载。

分步演示

限速
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。客户端 aiohttp 3.14.3,worker 从队列里取任务;限速器:固定间隔(asyncio.Lock 加 next 时间)、令牌桶(asyncio.Lock 内循环补充与等待);服务器每个请求耗时 0.05 秒(最后一节也试了 0.5 秒);速率统计取服务器收到请求的时间戳(perf_counter);“任意 1 秒窗口”包含两端,所以恒定 10 个/秒的最大值是 11;每项各跑一次
    • 没有验证:多进程或多机器共用一个限速器(需要共享状态,后面有分布式的文章);对真实站点的影响(对方的限流策略);时钟漂移对长时间运行的限速的影响;按域名分别限速(下一篇之一);漏桶(leaky bucket)与滑动窗口计数器等其它算法;限速与重试叠加后的实际速率
    京ICP备2026067502号-1京公网安备11010502063487号