住宅代理

使用住宅代理进行速率限制和请求节流

庞大的代理池并不能让你免受速率限制的约束,它只是改变了限制生效的位置。以下是如何按目标、按IP以及在整个代理群中控制请求节奏的方法。

Matt Brown

Matt Brown

2026年8月26日 · 2 分钟阅读

有一种颇具诱惑力的逻辑让不少团队栽了跟头:速率限制是按 IP 执行的,住宅代理池能给你许多 IP,因此速率限制不再适用。前两个前提都是对的,结论却是错的,而这两者之间的落差,正是大量爬虫被封的原因所在。

代理池改变的是限制作用在哪里,而不是限制是否存在。你仍然需要控制节奏,只是轴线和单 IP 方案不同了,而人们往往忘记的那个轴线,恰恰就是让他们被抓住的那个。下面来说说当你的请求分散在一个轮换池中时,限速实际是如何运作的。

三重限制,不是一重

当你通过代理池抓取数据时,至少有三种独立的上限同时存在,而你的吞吐量取决于你先撞上哪一个。

按 IP、按目标。 这是经典的一种。任何单个地址在被目标网站限速或封禁之前,只能向该网站发送有限次数的请求。轮换正是用来避开这一限制的手段,这也正是负载均衡一文中所描述的、将负载分散到代理池中的整体意义所在。

按目标、汇总计算。 这一点最容易让人意外。网站不仅按单个地址计数,它还能看到自己的总入站负载,并能识别出跨多个地址的协同模式,尤其是当这些请求在时间、路径或行为上存在共性时。你的五十个 IP 每秒各发两个请求,加起来就是每秒一百个请求打到同一个源站,无论怎么轮换地址都无法让它看起来像是正常的自然流量。反爬系统正越来越多地在这个层面上进行判断。

你自身的承载能力。 即你实际能维持的并发量:打开的连接数、工作线程数、内存占用,以及住宅代理的延迟高于直连这一事实,这意味着固定的线程数所能带来的每秒请求数会比你预期的更少。

由此带来的实际后果是,你的限速策略需要按目标来设定,而不是全局设定。单一的全局速率限制,要么会让本可以承受更高请求量的其他五十个站点被你限制得太狠,要么会对那个承受不住的站点造成冲击。

从目标出发,而不是从你的期望出发

在写任何限速代码之前,先弄清楚目标实际能容忍什么,因为凭空猜测只会带来两种结果:一个不必要地慢的任务,或者一个被封的任务。

留意目标网站给出的信号。429 明确表示你的速度太快了。Retry-After 响应头则是网站直接告诉你需要等待多久,遵守它不仅是正确的做法,也远比通过反复试错去摸索答案更省钱。有些 API 会在文档中公布限制,或者返回剩余配额的响应头。而如果网站的 robots.txt 中带有 crawl-delay 指令,那也是一种值得尊重的明确偏好。

如果没有任何公开信息,就通过实测来校准:从保守的速率开始,逐步提高,并关注经过验证的成功率,而不是仅看状态码,因为一个承受压力的网站往往会先出现服务降级,而不是直接拒绝服务,一个返回 200 状态码的验证页面,在天真的计数器看来就是成功的请求,这一点在检测被封或虚假内容一文中有详细说明。成功率开始下滑的那个点,才是你真正的上限,你应该让运行速度低于它,而不是恰好卡在它上面。

实现限速

针对每个目标使用一个令牌桶,是标准做法,而且从零实现也足够简单。令牌以你设定的速率补充,每个请求消耗一个令牌,当桶中没有令牌时请求就需要等待。这样能得到一个稳定的平均速率,同时具备可控的突发余量,比死板的“每 N 秒一个请求”式延迟更贴近真实流量的行为方式。

import time, threading

class TargetLimiter:
    """Token bucket, one instance per target host."""
    def __init__(self, rate_per_sec, burst=5):
        self.rate, self.capacity = rate_per_sec, burst
        self.tokens, self.updated = burst, time.monotonic()
        self.lock = threading.Lock()

    def acquire(self):
        while True:
            with self.lock:
                now = time.monotonic()
                self.tokens = min(self.capacity,
                                  self.tokens + (now - self.updated) * self.rate)
                self.updated = now
                if self.tokens >= 1:
                    self.tokens -= 1
                    return
                wait = (1 - self.tokens) / self.rate
            time.sleep(wait)               # sleep outside the lock

LIMITS = {                                  # per target, tuned per target
    "shop.example.com":  TargetLimiter(2.0),
    "search.example.com": TargetLimiter(0.5),
}

def fetch(url, host, proxies):
    LIMITS[host].acquire()
    return requests.get(url, proxies=proxies, timeout=20)

生产环境中有两点改进值得注意。一是加入抖动,让请求不要落在精确的固定间隔上,因为过于规律的间隔本身就是一种机器特征,而且它还会让你的多个 worker 同步成一波波集中发出请求。二是要让限速器在多个 worker 之间共享,而不是每个进程各自持有一个,否则十个进程各自礼貌地每秒发两个请求,加起来就是每秒二十个请求。在分布式环境中,这意味着需要一个共享的计数器,比如放在 Redis 之类的存储里,这正是跨 Kubernetes 扩展抓取任务一文需要解决的同类问题。

并发是另外一半

速率和并发是两个不同的调节维度,二者都需要设限。速率控制的是你发起请求的频率;并发控制的是同时有多少个请求在途中。一个速率限制适度但并发不受限的任务,一旦目标源站变慢,仍然会瞬间对同一个源站打开数百个并发连接,因为响应变慢会导致请求不断堆积。

用一个信号量按目标限制并发数,并根据目标能容忍的程度来设定大小,而不是根据你的机器能开多少连接来设定。在代理这一侧,并发连接数通常不是你的瓶颈,这也正因此容易让人忘记,目标网站的容忍度才是真正的瓶颈。而这背后隐含的“需要多少个地址”的分布问题,在你到底需要多少代理 IP一文中有详细讨论。

自适应限速优于固定数值

一个静态速率是一种猜测,而猜测会过时。网站会改变自己的容忍度,在负载下变慢,或者在高峰时段收紧限制。更稳健的做法是根据观察结果动态调整,采用类似加法增大、乘法减小的方式:在状态健康时缓慢地提高速率,在出现压力的第一个迹象时则大幅削减。

触发降速的应该是一个综合信号,而不是单一状态码:429、验证页面占比上升、延迟攀升,或经过验证的成功率下降。恢复时也应逐步进行,而不是直接跳回原来的速率,因为被封之后立即恢复到全速,本身就是一种容易被识别的模式。

class AdaptiveRate:
    def __init__(self, start=2.0, floor=0.2, ceiling=10.0):
        self.rate, self.floor, self.ceiling = start, floor, ceiling

    def ok(self):                       # healthy response
        self.rate = min(self.ceiling, self.rate * 1.02)   # creep up

    def strained(self):                 # 429, challenge, timeout, latency spike
        self.rate = max(self.floor, self.rate * 0.5)      # back off hard

限速与轮换、重试的交互

有三处交互值得明确说出来,因为一旦处理错了,就会让限速形同虚设。

不要用轮换来回应速率信号。 429 意味着要放慢速度。为了维持同样的节奏而换成一个新地址,正是把一个被限速的地址变成整个池子都被烧毁的行为,而这恰恰是让流量看起来像是在刻意规避,而不只是热情过头的原因。当信号关乎速率时就等待;当信号关乎地址本身时才轮换。

重试必须遵守限速器。 重试本质上也是一次请求,必须像其他请求一样获取一个令牌,否则你的错误处理路径就会悄悄绕开你精心设计的节奏控制。重试风暴是一个被限速的任务演变成被封任务的最常见途径,这也是重试与退避一文的主题。

粘性会话会集中负载。 在一个多步骤流程中持续使用同一个地址,意味着这个地址要承担整个流程的所有请求,因此在粘性会话中,按 IP 控制节奏比在负载天然分散的轮换任务中更加重要。

讲礼貌其实是为了自身利益

很容易把以上这些都读成一种合规负担,但激励方向其实是一致的。控制节奏能保护你所依赖的地址的信誉,让你的成功率保持在高位,从而不至于把带宽费花在验证页面上,同时也避免了一种升级循环:激进的采集行为会招致更严的防御,这会让目标站点对所有人都变得更难抓取,包括你自己在下个季度。尊重一个网站已声明的限制、它的 robots 指令和它的服务条款,既是正确的做法,也是更省钱的做法。

结论

代理池不会消除速率限制,它只是把限制挪了位置。按目标而不是全局来限速,因为单一数值对你接触的每个站点来说都是错的。从每个目标自身的信号中找出它真实的容忍度,把 Retry-After429 当作指令而不是障碍来遵守,并根据经过验证的成功率而不是状态码来校准。用共享的令牌桶加抖动来实现,把并发和速率分开限制,并让限制具备自适应能力,做到快速降速、缓慢恢复。然后把各种交互处理清楚:永远不要用轮换地址来回应节奏信号,始终让重试经过限速器,并在粘性会话中把节奏控制得更严。做得好的话,限速能让代理池以其真实的承载能力运行,而不是被白白耗尽。

代理池本身,正是给你腾出施展空间的东西:住宅代理能把一个节奏得当的任务分散到大量真实的家庭级地址上,并支持国家和城市级定位,而按 GB 计费则意味着,一个节奏得当、只抓取所需内容的任务,比一个猛冲猛打、不断重试的任务花费更少。

准备好开始了吗?

试用 Shifter 住宅代理,205M+ 个 IP,195+ 个国家,低至 $0.75/GB。

立即开始