单机爬虫挂代理,写个DownloaderMiddleware,在process_request里给每个请求分配一个IP,跑得好好的。一上多机分布式,同一套代码立刻翻车。
翻车的方式很明显:每台Worker各自维护自己的代理列表,列表又来自同一个接口,所以所有Worker拿到的是同一批IP。同一个IP可能同时被5台机器打向同一个目标站,目标站看到的并发量是你预期的5倍。Worker A发现某个IP失效,从自己的列表里删掉,可B、C、D还在用,继续产生失败请求。你算的是每台机器每分钟用某个IP发10个请求,合起来50个,但目标站的频率控制是按IP计的,不是按Worker计的。
三个问题根子是同一个:代理状态没有中心化。分布式爬虫的代理方案,核心不是把单机代码复制到多台机器,而是把代理池从Worker本地搬到Redis里,所有Worker共享同一个池,状态统一同步。

中心化代理池:Worker只管借和还
分布式代理池的架构,拆成三层看最清楚。
Redis代理池层是所有代理IP的唯一存储位置,IP地址、评分、上次使用时间、当前借出状态都在这。调度用SortedSet,Score就是评分;元数据用Hash,黑名单用Set。集群环境还要多一个字段:last_used_by,记录是哪个Worker在用。
中间那层是代理调度服务,可选,但我建议加。在Redis和Worker之间放一个轻量服务,负责分配IP、接收使用反馈、更新评分、触发健康检测。不想加独立服务,让Worker直接操作Redis也行,代价是并发安全得自己保证。
Worker层就不用维护本地代理列表了。每个Scrapy Worker的Middleware每次需要代理,向Redis借一个,用完还回去,附上使用结果。
借-用-还这套模型是整个方案的骨架。单机代理池和集群代理池的本质区别,不在IP数量,在IP的状态有没有被全局看见、全局管住。
数据结构:在单机代理池上多把锁
单机代理池用SortedSet加Hash加Set就够了,集群环境得多解决一个问题:同一个IP不能同时借给两个Worker用在同一个目标站上。
办法是给每个IP加一把借出锁,用Redis的SET key value EX seconds NX:
Key: proxy:lock:{ip:port}:{domain}
Value: worker_id
EX: 30(秒,超时自动释放)
NX: 只在key不存在时设置
Worker借IP的时候,先从SortedSet取一个高分IP,再对IP加目标域名加锁。锁加上了才算借到,加不上说明别的Worker正在用这个IP访问同一个域名,换一个IP重试。
锁的粒度是ip+domain,不是只锁IP。原因很直接:同一个IP同时用在不同目标站上没毛病,目标站A的频率控制,不会因为你在访问目标站B就出问题。这个细节,决定了集群代理池是能用还是好用。
生产级场景里,Redis的结构还要分出四个区域:可用池、占用池、临时黑名单、永久黑名单。临时黑名单用ZSet存解禁时间,到期自动释放;永久黑名单存那些连续失败、批量被封的IP,永不复用。这么做的目的,是把IP复用混乱和误封禁这两个坑一起填掉。
IP调度:评分决定谁先被借
从池子里取哪个IP,不能随机抽。权重由三个指标合成:成功率占60%,延迟占30%,剩余可用时间占10%。优质的IP优先被拿去用。
评分不是死的。每次Worker还IP时附的使用结果——成功还是失败、延迟多少、有没有触发验证码——都写回Redis,更新这个IP的评分。连续失败到阈值的,从可用池移进黑名单;评分一路走高的,在SortedSet里排到前面。
还有一条约束很容易被漏掉:单站点单IP,60秒内只允许5次请求。这个限制得在调度层强制执行,不能指望每个Worker自觉。全局频率控制是分布式代理池存在的核心价值之一,没它,多机部署只是把单机的封禁问题按Worker数量放大了一遍。
故障切换:心跳和租约,机器挂了任务不丢
代理池管好了,还有个多机环境特有的问题:Worker自己挂了怎么办。
高可用任务队列的核心是状态可恢复、故障可转移。方案是心跳加租约。Worker启动时在Redis注册一个心跳key,TTL设30秒,每5秒续一次;拿到任务后给任务也设一个30秒租约,心跳续期的时候同步刷新租约。Worker要是挂了,心跳停下来,30秒后租约过期,任务被自动回收给别的Worker。
这套机制和代理池的借出锁是同一个逻辑:用带过期时间的锁表达占用,而不是用删除表达占用。删除是不可恢复的,过期是可回收的。分布式系统里,任何占用状态都必须带超时,不然一次意外宕机就能把资源永久卡死。
代理层面的故障切换还有个额外要求:重试时必须先换IP,再发请求。Scrapy默认的RetryMiddleware会沿用原meta,代理字段不变,一个已经失效的IP被重试三次,三次全打在死节点上。正确做法是在重试逻辑里显式走一遍标记当前IP失败、从池里借下一个IP,让重试和换IP绑定,不是和同一个IP绑定。
免费代理在这个架构里放哪
分布式架构下,免费代理的角色得说清楚:它是代理池的补充数据源,不是主力。
谷德免费代理在免费渠道里的数据是头部水平——HTTP初始可用率62.3%、平均延迟387ms、高匿占比94%——可以拿它在开发和测试阶段把借-用-还这条链路验一遍:鉴权方式对不对、评分更新有没有生效、故障切换会不会误杀可用节点。页面上存活时间那一列入池时也用得上,优先把存活超过2小时的IP写进待验证队列,能省掉大量无效验证请求。
但24小时存活率8.2%这个数字,决定了它撑不起7×24小时的多机持续采集。分布式架构要的是稳定的基础供给,付费的隧道代理或动态住宅代理提供持续可用的IP流,免费代理只做临时扩容或测试。架构解决的是IP怎么管,资源解决的是有没有IP可管,两件事互相替代不了。
一条能直接落地的顺序
先做中心化,再做调度,最后加故障转移。把代理池搬进Redis,加ip+domain借出锁,跑通借-用-还闭环——这一步做完,多机撞车和状态不同步基本就解决了。然后加评分和全局频率控制,让优质IP优先、单站点请求受限。最后用心跳和租约补上Worker故障时的任务回收。
分布式爬虫的代理问题,本质上不是IP不够多,是IP的状态没有被所有节点看见。中心化代理池解决可见性,评分和锁解决可控性。这两件事做到位了,横向扩容就是加机器的事,不是加bug的事。
