简介:这份基于Redis的代理池服务框架压缩包,面向网络爬虫、代理验证与负载均衡等场景的开发者,用于解决单点代理IP不稳定、访问效率低及调度管理困难等问题。资源共43个文件,整体约26KB,包含35个Python脚本、3个Shell脚本、2个Markdown文档以及配置文件,覆盖代理收集、验证、调度、Redis存储交互等核心模块,可帮助快速搭建一套可用的代理池管理服务。包内目录结构清晰,核心业务逻辑与工具脚本分离,并附有启动/停止脚本和框架说明文档,便于理解与二次开发。配置层支持设定Redis连接信息与运行参数,验证模块可测试HTTP、HTTPS、SOCKS等多种协议,调度算法会优先分配表现更优的代理IP,以提升整体成功率。目前已有44人学习下载,适合具备一定Python基础、希望优化代理管理与网络请求效率的开发者参考。
1. 代理池这个老问题,为什么偏要用 Redis 重新搭一遍框架
做爬虫的人,没有谁没被代理IP坑过。早晨测好的一批代理,到中午就只剩一半能用,贵的稳定池成本扛不住,免费的又像开盲盒——今天全通,明天全部超时。我见过不少团队把代理IP当消耗品随意采购,结果在封控严的业务数据采集场景里,一个接一个连接超时,最后排查出来根本不是目标网站的问题,是代理池压根没人管。这篇要拆的,就是「基于Redis的代理池服务框架」这一类方案的落地路径:用 Redis 做存储与调度中枢,让采集、校验、淘汰、对外暴露全部自动化。它解决的是「手搓脚本测代理」这种临时方案解决不了的事——失效自动剔除、质量可打分、多业务方共享同一份代理数据。适合正在做爬虫采集、接口轮换、或想把自己的代理管理从手工 Excel 升级成服务的工程师。
2. 盘活 Redis 数据类型来搭代理池:五种结构的职责边界
2.1 用 Hash 存代理详情,为什么比存 JSON 字符串稳
代理池里一条代理记录,不只是「IP:端口」两个字段。协议类型是 HTTP 还是 HTTPS、来源站点、上次校验时间、连续失败次数、匿名等级,这些东西都跟着一条代理走。很多第一次搭框架的人图省事,直接把整个对象序列化成 JSON 塞进 Redis 的 String 里,取出来再整体反序列化。数据量小的时候没问题,几百条代理随便折腾;一旦代理池过万,每次校验只更新「连续失败次数」一个字段,却要把整个 JSON 取出来解析再写回去,性能损耗和并发冲突就开始冒头。
用 Hash 的好处是字段独立更新。我会给每条代理分配一个唯一 key,比如 proxypool:detail:{md5},然后把它拆成 host、port、protocol、source、fail_count、score 这些 field 分别存储。校验器只需要 HINCRBY 更新失败次数、HSET 更新得分,不碰其他字段。另一个实在的好处是排查问题方便——RedisInsight 或 Another Redis Desktop Manager 里随便点开一条 Hash,所有字段平铺在眼前,不用靠脑补 JSON 里的嵌套结构。
还有一点容易被忽略:删除粒度。Hash 可以整体 DEL,也可以单独 HDEL 掉某个失效字段,而 String 只能整体覆盖。代理池里经常出现「IP 还能通但协议已经变了」的情况,例如一个代理昨天还支持 HTTPS,今天目标网站握手失败。这时候直接更新 protocol 这一个 field,比起改整个 JSON 再写回,代码更短、出错的路径更少。
2.2 用 Set 去重、ZSet 调度:代理池的数据组织核心
代理池和普通缓存最大的不同,是它需要「排序」和「范围获取」。Redis 的 ZSet 天生就是干这个的:member 存代理唯一标识,score 存代理质量分。校验器每次成功验证,就给这条代理加分;连续失败,就减分。代理池对外提供代理时,按分数从高到低取一条 ZREVRANGE,拿到的就是当前质量最好的一条。
但 ZSet 有一个天然的毛病:member 是唯一的,可它不提供「某个来源是否已经采集过」的判断。代理采集器每天从免费代理网站抓好几轮,同一个 IP 今天出现在 A 站、明天出现在 B 站,如果直接 ZADD,后面的操作会把前面的覆盖或者只是更新 score,导致「这个代理我到底有没有入库过」变得模糊。所以我一般会额外维护一个 Set 集合,专门做去重:SADD 之后如果返回 0,说明是重复代理,直接跳过后续流程。
ZSet 的 score 设计是整个代理池的灵魂。我见过把 score 直接存「连续成功次数」的,也见过直接存「Unix 时间戳」的——把最近使用时间当分数,这样取出来的永远是最新代理。这两种都有道理,但没法同时表达「质量好」和「新」。我会拆成两个维度:一个 ZSet 存质量分,另一个 Hash 存代理元数据里的 last_checked 字段。质量分负责选代理,时间戳负责淘汰旧数据,各管各的。
2.3 用 List 做校验任务队列,把分布式锁也带上
代理入库后不能直接对外暴露,因为还没验证过。框架里需要一条「待校验队列」:采集器拿到新代理,推入一个 List;校验器从 List 左侧弹出,逐个去目标网站验证连通性。为什么用 List 而不是直接轮询 ZSet?因为 List 天然支持阻塞弹出,BRPOP 可以让校验器空闲时挂起、有任务时立刻醒来,不用写定时轮询逻辑,Redis 连接也不会被无谓的循环查询打满。
多个业务方同时取代理的时候,还要考虑一个问题:别让 10 个业务方把同一条刚入库的新代理抢去验证,验证完又同时写回 ZSet,分数直接被覆盖成同一个值——这属于典型的并发写冲突。常见做法是给校验任务加一把 Redis 分布式锁,用 SETNX 或者 Lua 脚本抢锁,抢到锁的节点才执行这一轮的验证和打分更新。注意锁的粒度要细,最好锁到单条代理的 key,而不是锁整个队列,否则一个代理卡住验证,全队列都跟着堵车。
3. 代理池服务框架落地:从采集到验证再到暴露代理的最短路径
3.1 模块划分:采集器、校验器、API 服务怎么分工
一个能跑起来的代理池服务框架,最少需要三个进程角色。采集器负责从公开代理网站抓取代理,做基础格式清洗后写入 Redis;校验器负责消费待校验队列,用真实请求验证代理能否访问目标网站,更新 Hash 详情和 ZSet 分数;API 服务负责对外提供 HTTP 接口,业务方来要代理就给一条分数最高的,来报「这条代理失败了」就把分数扣掉。
三个模块之间不用通信协议,全部通过 Redis 解耦。采集器写队列,校验器读队列,API 读 ZSet——谁挂了都不影响另外两个继续工作,这是把 Redis 当中间件用最大的收益。部署上,三个模块可以跑在同一台机器上先验证效果,之后再把校验器单独拆到多台机器横向扩容。校验器是一个无状态消费者,水平扩展几乎零成本,只要保证所有实例连的是同一个 Redis 实例或集群就行。
3.2 代理入库的代码:Python + redis-py 怎么把数据写进 ZSet
这里给一个最小可用的入库逻辑,用 Python 的 redis-py 客户端。注意这段代码处理了「去重、详情存 Hash、调度进 ZSet、来源记录进 Set」四件事:
import hashlib import json import redis import time r = redis.Redis(host='127.0.0.1', port=6379, db=0, decode_responses=True) def gen_proxy_key(protocol: str, host: str, port: int) -> str: raw = f"{protocol}://{host}:{port}" return hashlib.md5(raw.encode("utf-8")).hexdigest() def add_proxy(proxy: dict, source_site: str) -> bool: protocol = proxy.get("protocol", "http").lower() host = proxy.get("host") port = int(proxy.get("port")) key = gen_proxy_key(protocol, host, port) # 1. 去重:Set 里已经存在就跳过 if r.sadd("proxypool:seen:global", key) == 0: r.hincrby(f"proxypool:detail:{key}", "duplicate_count", 1) return False # 2. 存详情:Hash 各字段独立,后续更新只动单字段 detail = { "protocol": protocol, "host": host, "port": port, "source": source_site, "added_at": int(time.time()), "last_checked": 0, "fail_count": 0, "success_count": 0, } r.hset(f"proxypool:detail:{key}", mapping=detail) # 3. 进待校验队列:初始分数 0,代表还没被验证过 r.zadd("proxypool:score", {key: 0}) r.rpush("proxypool:todo", key) return True这段代码的逻辑顺序是刻意的:先去重,再存详情,最后入 ZSet 和队列。如果先 ZADD 再去重,会出现 Set 里没记录、ZSet 里却已经有了脏数据的中间态。把 ZADD 放最后,即使中途崩溃,最多丢一条待校验任务,不会污染调度表。
有一个细节值得注意:我在 Hash 里单独存了 success_count 和 fail_count,而不是只存一个最终分数。因为分数是可以通过 Lua 脚本重算的,但「成功了几次、失败了几次」这些原始数据一旦丢,重算就没依据了。这也是为什么我坚持用 Hash 存过程数据、用 ZSet 只存派生数据的原因——Hash 相当于原始日志,ZSet 相当于视图。
3.3 校验任务消费:BRPOP 阻塞队列的完整循环
校验器这边用一个常驻脚本消费队列。这里不只用 BRPOP 取任务,关键在后半段——取出来的代理如果验证失败,要根据 fail_count 决定是「重新排队再试」还是「直接打回老家」:
import time import requests as req VALIDATE_URL = "https://httpbin.org/ip" VALIDATE_TIMEOUT = (3, 5) def consume_todo_queue(): while True: # 阻塞等待待校验队列,超时 30 秒后循环回来继续等 item = r.brpop("proxypool:todo", timeout=30) if item is None: continue proxy_key = item[1] detail = r.hgetall(f"proxypool:detail:{proxy_key}") if not detail: continue ok = False try: proxies = { detail["protocol"]: f"{detail['protocol']}://{detail['host']}:{detail['port']}" } resp = req.get(VALIDATE_URL, proxies=proxies, timeout=VALIDATE_TIMEOUT) ok = resp.status_code == 200 except Exception: ok = False if ok: # 验证成功:加分数、清失败计数、刷新最后验证时间 r.zincrby("proxypool:score", 10, proxy_key) r.hset(f"proxypool:detail:{proxy_key}", mapping={ "last_checked": int(time.time()), "fail_count": 0, }) else: # 验证失败:失败计数 +1,按失败次数决定是重试还是淘汰 fail_cnt = r.hincrby(f"proxypool:detail:{proxy_key}", "fail_count", 1) if fail_cnt >= 3: remove_proxy(proxy_key) else: # 重新排到队尾,但这里刻意加一个延迟,避免立刻再次验证 r.zadd("proxypool:todo_delay", {proxy_key: time.time() + 60})看到没,失败 3 次以上的代理我不会直接删 ZSet——因为代理价值不只取决于它是否存活,还要考虑它来自哪个采集源。如果某个来源站连续贡献了 10 个「验证失败」的代理,那我应该把整个采集源降权甚至拉黑,而不是逐个删代理。代理池的「治理」落到源头才是高效的。
校验超时参数这里我给的是 3 秒连接、5 秒读取,这是爬虫场景里比较平衡的配置。太短会把慢速但真正可用的代理误杀;太长会让校验器吞吐量暴跌,一个代理验证卡了 30 秒,整个消费循环就堵住了。如果你验证的代理数量大,建议把超时压到 2 秒,同时把队列消费者数量从 1 个扩到 3 个——Redis 的 BRPOP 天然支持多个消费者同时阻塞在同一个 List 上,不会重复消费,这也是 List 做任务队列相比 ZSet 的一个明显优势。
4. 调度打分与淘汰策略:让代理池自己长出一套优胜劣汰
4.1 打分规则:成功率、连续失败、来源可信度三个维度加权
代理池跑起来之后,你很快就会遇到下一个问题:分数只有「成功 +10、失败 0」两条规则,感觉不够用。比如一个来自免费站的代理,今天通了 30 次,明天突然连续失败 5 次,按上面的逻辑它要被删了,但它之前明明很稳定——直接删掉是不是太武断?
我会把打分拆成三个维度:成功率(最近 20 次校验里成功占比)、连续失败次数(代表当前的即时状态)、来源可信度(代表这个采集源长期的历史表现)。三者的权重可以按你的业务场景调:如果代理主要用于短时突发采集,来源可信度权重调低;如果代理是长期稳定跑量,来源可信度权重调高。分数公式不必搞太复杂,线性加权就够了:
score = success_rate * 100 + source_score * 20 - fail_count * 5
这个公式的意思很简单:成功率是主要质量指标,满分 100;来源可信度是一个 -10 到 10 的调整项,影响范围在 ±200;连续失败次数是惩罚项,每失败一次扣 5 分。当分数被扣到负数,对外取代理时 ZREVRANGE 自然取不到它——它还在池里,但已经「沉底」,等它下一次验证成功再被捞起来。
这套规则的好处是「可解释」。任何人问「这条代理为什么分数是 83」,你可以直接拆给他看:成功率 90% 得 90 分,来源可信度 5 得 100 分,有过 7 次连续失败扣 35 分,加在一起 155——等等,你说 155?那就说明公式里还有另一个维度在起作用,例如匿名等级加分。总之规则维度不要超过 4 个,超过 4 个之后你根本没法靠直觉判断一条代理的死活,调试成本成倍上涨。
4.2 原子调度:用一段 Lua 脚本同时完成取代理和降权
代理池对外的 API 如果只是单纯 ZREVRANGE 取一条分数最高的代理,业务方拿走后这条代理可能在下一秒就被另一个业务方取走——两个爬虫共用一个代理 IP,轻则目标网站限制,重则直接封掉这个代理。解决思路是把「取出即降权」做成原子操作,避免同一时刻并发取出同一条代理。
Redis 的 Lua 脚本是个好工具,它可以保证一个脚本内所有命令在执行期间不被其他命令插入,天然避开竞态。我的调度脚本核心逻辑是这样:
-- 参数:KEYS[1]=分数ZSet, KEYS[2]=待校验队列, ARGV[1]=取出的最低分数 local used_key = redis.call('zrevrange', KEYS[1], 0, 0, 'WITHSCORES') if not used_key[1] then return '' end -- 取出来之后马上降权,防止同一时刻被其他人再次取走 redis.call('zadd', KEYS[1], used_key[2] - 20, used_key[1]) return used_key[1]注意这段脚本的关键点:ZREVRANGE 拿到当前最高分的代理,然后立刻用 ZADD 把它的分数减 20。由于整个操作在 Lua 脚本里是原子的,两个并发请求不可能同时拿到同一条代理——第二个请求 ZREVRANGE 时,这条代理的分数已经被减掉 20,可能掉出顶部位置。这个「取出即降权」的手法,让同一条代理在单位时间内不会被不同业务方反复使用,同时又不至于直接把它踢出池子。
关于减多少分合适,我一般用「20」作为初始值。原因是它和成功加 10 分对应,意味着「每被取用两次,就需要一次成功验证来补回分数」。如果你的代理池里可用代理很多,想让每条代理被取用的频率更低,可以把降权值调到 30 或 40。注意如果降权值调太高,一条代理刚被取用就沉底了,下次业务方取代理时可能会取到质量更差的那一批——这是一个需要根据业务跑量节奏反复调参的平衡点。
4.3 过期与淘汰:让 Redis 的 TTL 和「缓存治理」思路帮代理池自动清垃圾
ZSet 会一直保留成员,除非你手动删除。这意味着即使我们通过降权让垃圾代理沉底,它们依然占着 ZSet 的内存,而且 ZREVRANGE 偶尔会把它捞出来给业务方造成一次「假死」体验。代理池里垃圾代理的清理,常见做法是给每条代理的 Hash 设置一个过期时间,到期自动消失,再配一个定时任务把已经消失代理对应的 ZSet 成员也清掉。
我给代理设置的 TTL 通常是 15 分钟。原因是大多数公开代理的存活周期在 10 到 30 分钟之间,设置 15 分钟可以保证「一个代理最后一次成功校验之后,如果 15 分钟内没有任何业务方调用,它就会被自动遗忘」。注意 TTL 必须在每次成功校验时刷新,否则一条好代理会被当成僵尸清掉:
SETEX proxypool:detail:{key} 900 value但这带来一个新问题:Hash 可以 TTL 自动过期,ZSet 的成员却不会跟着自动消失。每条代理在 ZSet 里还有一个分数成员,过期后依然残留在内存里。所以我每周跑一次清理脚本,用 ZSCAN 遍历 ZSet,对每个成员去查对应的 Hash 是否还存在,不存在的直接从 ZSet 里 ZREM 掉。ZSCAN 而不是 KEYS,是因为代理池上了万条之后 KEYS 命令会直接卡死 Redis 主线程,这在 Redis 缓存治理里是基本功,但代理池场景特别容易踩——因为 ZSet 天然适合存代理,而 ZSet 越大,你越容易写出性能灾难级的 KEYS 扫描。
清理频率不用太高,每天凌晨业务低峰期跑一次就够了。清理脚本本身也要注意心法:每次 ZSCAN 用 COUNT 控制一次遍历数量,比如 500 条,处理完之后 sleep 0.1 秒再继续,避免把 Redis 的 CPU 打满。这不是玄学,是代理池跑久了之后最常见的性能杀手。
5. 代理池避坑指南:五条踩出来的硬经验
5.1 现象:代理池跑一天后 ZSet 分数全乱,源头是并发写
代理池上线第一周,我遇到过分数全部变成 9999 的诡异故障。排查半天发现是多个校验器实例同时验证同一条代理,每个实例都执行 ZINCRBY 加分,同一时刻 5 个实例同时加 20,分数瞬间暴涨。后续所有业务方拿到的都是同一批高分代理,池子里的其他代理被活活饿死。
原因就是校验器扩容后没有做并发控制。校验任务在处理前没有确认「这条代理是否正在被其他实例校验」,两个实例从 BRPOP 里同时取到一条任务(BRPOP 本身不会重复交付,但校验器消费完队列之后,代理重新被 ZADD 到待校验队列时,可能被另一个实例再次取走),就会造成重复验证和重复加分。
解决方式是两层的:第一层,校验器拿到代理后先检查 Hash 里的 lock 字段,用 SET NX EX 设置一个 60 秒的锁,拿不到锁就跳过;第二层,如果同一条代理在两分钟内被验证了三次以上,就不再给它加分,只更新 last_checked 时间。这两个措施落地之后,分数乱跳的问题再没出现过。
5.2 现象:代理验证成功但业务方实际用不了,目标网站判定标准不一致
最经典的翻车现场。我用 httpbin.org 做验证目标,代理验证全部通过,结果业务方拿这些代理去请求目标网站,大批连接超时。原因在于 httpbin.org 对代理的判定极宽松,只要代理能建立 TCP 连接就算通;而真实业务目标网站有 TLS 指纹校验、有 cookie 会话校验、有频率限制,一条代理在 httpbin 上通了,在目标网站上可能连握手都过不去。
解决思路是分级验证:采集入库的代理用轻量目标做「存活验证」,只验证 TCP 连通;业务方实际使用前,再到目标网站做一次「业务验证」,通过的直接加分,不通过的直接淘汰。业务验证的频率不用太高,每个业务场景每 5 分钟验证一次就够了,不然目标网站的请求量会被代理池的验证流量拖垮。另外,验证目标和业务目标最好独立——如果只有唯一一个验证目标,它挂了你的整个代理池也会误判为「全军覆没」。
5.3 现象:代理获取接口越来越慢,Redis 连接被打满
代理池对外暴露的 HTTP API 每被调用一次,就要执行 ZREVRANGE 一次。本来这是很轻量的操作,但业务方把获取代理的接口写成了循环——每发一次请求都来要一条新代理,QPS 一高,Redis 的连接数直接飙到上千。Redis 默认连接数是 10000 不假,但每条连接都要建 TCP、都要占用内存,连接一多整个实例响应延迟跟着上涨。
这件事的坑不在 Redis 配置,而在业务方行为。我会在代理池的 API 层做一层本地缓存:同一个业务方在 5 秒内重复请求,直接返回上一条代理,不穿透到 Redis。同时在 redis-py 客户端里强制使用连接池,把最大连接数控制在 50 以内,超出后请求排队等待而不是新建连接。代理池本身是一个低频服务,正常业务方每 10 秒拿一条代理就够用了,接口响应慢十有八九是连接管理的问题,不是 Redis 本身的问题。
5.4 现象:重启后丢了一部分代理,Redis 持久化配置没跟上
我犯过一个很低级的错误。本地测试用默认的 Redis 配置跑得很欢,部署到生产时忘了改持久化策略,结果机器在凌晨因为内存不足被系统 OOM kill 后重启,代理池里的数据全没了——因为默认配置下 Redis 的 RDB 快照频率是「900 秒内至少 1 次写」,代理池这种写量不小的服务,很容易在两三个小时里都不满足快照条件,一重启就回到 N 小时前的状态。
现在我的标配是 AOF + everysec。AOF 每秒写一次日志,最多丢一秒的数据,重启后最多回退一秒,代价是磁盘写入会多些流量。代理池这种「数据量不大但写入频繁」的服务非常适合 everysec,它既能保证 TTL 过期、ZSet 分数、Hash 字段这些细节全部可恢复,又不会像 always 模式那样把磁盘 IO 拖垮。另外,生产环境一定不要用 Redis 的默认内存淘汰策略,代理池要设置 maxmemory-policy 为 noeviction,否则内存满了 Redis 会随机淘汰 key,你的代理数据被当成普通缓存给清掉。
5.5 现象:多实例部署后,采集器重复抓到同一个来源站
采集器如果有多台实例同时在跑,同一个免费代理网站会被多个实例重复抓取,每台实例都把同一批代理往 Redis 里塞。虽然 Set 去重能挡住一部分,但去重是在写入时执行的,采集过程本身重复耗费了大量网络请求流量和解析 CPU。
我在采集器这边也加了分布式锁,但锁的粒度是「采集源 + 时间窗口」。比如某个免费代理网站每 10 分钟更新一次列表,锁就设计成 source:free_ip_site_A + 5 分钟窗口,每 5 分钟内只允许一台实例抓取一次。抢锁用 SETNX + EXPIRE,不需要引入额外组件。这个坑很隐蔽,因为单实例模式下根本不会触发,只有扩容到多实例才暴露——程序员最容易忽略的「看着能跑,一扩就死」类型。
6. 代理池上线前最后一步:用模拟爬虫流量验证调度是否合理
框架搭好、代码上线,不代表它能扛住真实业务。我习惯在正式接入前跑一轮「模拟爬虫压测」:写一个脚本,模拟 20 个并发爬虫任务,每个任务每 5 秒从代理池取一条代理,用这条代理请求目标网站,记录成功率和单次平均响应时间。压测跑 30 分钟,重点看三个指标。
第一看代理的平均使用次数。如果一条代理在 30 分钟内被取用超过 30 次,说明调度降权力度不够,业务方容易集中在少数高质量代理上;如果平均使用次数低但成功率也低,说明降权太狠,好的代理被雪藏了。第二看分数分布的变化曲线——通过 ZRANGE 拿出全部代理分数,算一下分布区间,高质量代理是否集中在顶部、垃圾代理是否稳定沉底。第三看待校验队列的长度是否持续堆积,如果堆积超过 500 条,说明校验器消费速度跟不上采集速度,需要扩校验器实例。
这里有一个我后来才加上的技巧:在 ZSet 里预置一个「最低分保留位」。具体做法是每轮压测结束,手动把表现最差的 10% 代理分数统一压到一个负数区间,而不是直接删掉。这样做的意义是保留它们的 Hash 详情,方便之后复盘它们「为什么差」——是来源站质量普遍低,还是某个具体时间段的验证环境出过问题。代理池最珍贵的数据不是那些活着的代理,而是那些死掉的代理留下的历史记录,它们能反推出采集源的优化方向。
用 Redis Desktop Manager 这类可视化工具直接观察代理池的分数分布,是最直观的调参方式。我每次压测结束都会打开客户端,看一遍 ZSet 的分数列表,再对比压测日志。分数分布异常通常一眼就能看出来:要么全挤在 90 分往上,要么全部负数,这两种情况都说明打分规则需要重新调。最终我给代理池定了一个规矩:每个代理的分数必须能让人在三秒内看懂它「为什么是现在这个状态」,任何需要翻日志才能解释的分数模型,对一线排查的人来说都是负担。
如果你准备开始搭,建议先把第 2 章的数据结构设计看懂,再用第 3 章的代码跑通最小闭环,最后把第 4 章的调度脚本和 TTL 策略接进去。这个方向值得投入的时间大概在三到五天,做完以后你会发现自己再也不想回到「手工测代理、Excel 记录」的状态了。希望帮到你。
本文还有配套的精品资源,点击获取