我见过不少团队的做法:任务还没跑起来,先花两周搭了一套 Redis 集群 + 消息队列 + 独立调度服务 + 监控大盘的代理架构。结果日均请求量只有几千——架构的运维成本比采集本身还高。
反过来也有团队一直用单机脚本跑,日均请求量已经上了十万、成功率掉到 60%,才开始想"是不是该换架构了"——这时候积累的技术债务已经很重,改造成本远高于从合适的时机做增量演进。
合理的做法是:按请求量级和业务复杂度分阶段演进,每个阶段只解决当前瓶颈,不提前过度设计。
典型场景: 一个 Python 脚本 + requests 库 + 一个代理 IP 列表,跑一个采集任务,目标站 1-2 个。
代理架构: 最简单的形态——代理列表存在本地文件或配置里,脚本启动时加载,按轮询或随机方式使用。
import random
import requests
proxies_list = [
"http://ip1:port1",
"http://ip2:port2",
# ...
]
def get_with_proxy(url):
proxy = random.choice(proxies_list)
return requests.get(url, proxies={"http": proxy, "https": proxy}, timeout=10)这个阶段的核心问题:
根本没有 IP 健康管理。某个 IP 挂了,你不知道——它继续被选中,继续失败,拉低整体成功率。
没有失败反馈。脚本里的错误处理通常是 try/except 然后 continue,不会把失败信息反馈给代理选择逻辑。
什么时候该演进: 当你发现成功率不稳定、需要频繁手动删除失效 IP、或者开始有第二个采集任务需要共用代理时。
演进方向: 加一个最简单的代理管理层——不需要 Redis,不需要独立服务,在脚本内部加一个带评分的代理选择器就够了。
class SimpleProxyPool:
def __init__(self, proxies):
self.proxies = {p: 100.0 for p in proxies} # ip: score
def get(self):
# 按评分加权随机
total = sum(self.proxies.values())
if total == 0:
return None
r = random.uniform(0, total)
cumulative = 0
for proxy, score in self.proxies.items():
cumulative += score
if r <= cumulative:
return proxy
return list(self.proxies.keys())[0]
def feedback(self, proxy, success):
if proxy not in self.proxies:
return
if success:
self.proxies[proxy] = min(self.proxies[proxy] + 2, 100)
else:
self.proxies[proxy] *= 0.7
if self.proxies[proxy] < 5:
del self.proxies[proxy] # 淘汰这个改动只需要几十行代码,但效果显著:坏 IP 会被自动降权和淘汰,好 IP 会被更多地使用。
典型场景: 1-3 个采集任务并行跑,可能用了 Scrapy 或 asyncio,单机并发 20-50。代理 IP 数量从几十个增长到几百个。
演进触发条件:
多个采集任务需要共享代理池——脚本 A 和脚本 B 同时跑,如果各自维护独立的代理列表,IP 使用状态不一致。
代理 IP 从上游接口动态获取——不再是固定列表,需要定期刷新,新 IP 要验证后再入池。
开始需要按目标站隔离代理(脚本 A 打目标站 X、脚本 B 打目标站 Y,不想互相干扰)。
代理架构: Redis 成为代理池的中心。Sorted Set 存 IP 和评分,Hash 存元数据,Set 存黑名单。所有采集脚本连同一个 Redis 实例。
这个阶段要做的关键改造:
把代理池从脚本内存搬到 Redis。 所有脚本共享同一个 Redis 代理池,IP 的评分、使用记录、黑名单都在 Redis 里——任何一个脚本发现某个 IP 失效,其他脚本立刻能看到。
加一个独立的健康检测进程。 不要让每个采集脚本自己做健康检测(会重复检测、浪费资源、产生竞态)。单独跑一个进程,每 5-10 分钟扫描 Redis 里所有 IP,更新评分,淘汰坏 IP。
加水位监控。 一个简单的 cron 任务,每分钟检查 Redis 里 ZCARD 的值,低于阈值时触发 IP 补充(调用上游接口获取新 IP、走入池验证流程)。
这个阶段的典型问题:
单机并发上限。Python 的 GIL 限制了 CPU 密集型任务的并发能力。如果你的采集逻辑包含大量 HTML 解析,单进程的并发瓶颈在 30-50 左右。解决方案是用多进程(multiprocessing)或改用 asyncio + aiohttp。但这还是单机范畴。
Redis 单点故障。这个阶段 Redis 通常是单实例,挂了整个代理池不可用。如果业务允许短暂中断(几分钟),Redis 重启后重新跑一次 Fetch + 验证就能恢复。如果不允许中断,加 Redis Sentinel 做主从自动切换。
什么时候该演进: 当单机的 CPU、内存、出口带宽中任何一个成为瓶颈,或者你需要从多个不同地域的出口发请求时。
典型场景: 3-10 台机器组成 Worker 集群,共享 Redis 代理池和请求队列。可能已经用了 Scrapy-Redis 或自建的分布式任务调度。
演进触发条件:
单机出口带宽不够——一台机器的千兆网卡跑满了也只有 ~120MB/s 的下载能力。
需要多地域出口——比如采集任务要求从不同城市或国家发请求,单机只能从一个出口发。
单机故障影响全局——一台机器挂了,所有采集任务停。
代理架构的关键变化:
代理调度要加锁。 多个 Worker 同时从 Redis 代理池取 IP,需要防止同一个 IP 被多个 Worker 同时用在同一个目标站上。用 Redis 的 SET NX EX 做分布式锁,锁的粒度是 ip + domain。
代理评分要聚合。 Worker A 报告某个 IP 成功,Worker B 报告同一个 IP 失败——评分更新不能各自为政。两种方案:一是所有 Worker 直接写 Redis(用 ZINCRBY 原子操作,天然支持并发),二是让 Worker 把反馈发到消息队列,由一个独立的评分聚合服务统一更新。方案一简单够用,方案二在 Worker 数量超过 20 台时更可控。
健康检测要防重复。 阶段二的独立检测进程在这里还是跑一个就够——它扫描 Redis 里所有 IP 做检测,和 Worker 数量无关。但要注意:如果你部署了多个检测进程副本(为了高可用),需要用分布式锁保证同一时间只有一个在跑。
def run_health_check_with_lock(rdb, pool_key):
lock = rdb.lock("health_check_lock", timeout=600, blocking_timeout=5)
if lock.acquire(blocking=False):
try:
do_health_check(rdb, pool_key)
finally:
lock.release()
else:
print("Another checker is running, skip.")这个阶段的典型问题:
Worker 之间的请求分配不均。如果用 Scrapy-Redis,请求分配是"谁先 POP 谁拿",快的 Worker 多干、慢的少干。如果某些 Worker 因为网络环境差导致成功率低,它们会消耗大量代理 IP 但产出少。解决方案:监控每个 Worker 的成功率,成功率低于阈值的 Worker 自动降低请求获取频率。
跨地域部署的时钟同步。如果 Worker 分布在不同地域,Redis 的 TTL 和分布式锁的超时依赖系统时钟。时钟不同步可能导致锁提前释放或过期判断出错。确保所有机器都配置了 NTP。
典型场景: 大规模数据采集团队,10+ 台 Worker,多个业务线共用代理基础设施,需要精细的流量控制、成本统计、权限管理。
演进触发条件:
需要统一的代理接入层——不同业务线的采集脚本使用不同的语言和框架(有用 Python 的、有用 Go 的、有用 Node.js 的),不可能每种语言都实现一套 Redis 代理调度逻辑。
需要成本核算——按业务线统计代理 IP 的消耗量和成本,谁用了多少、花了多少。
需要限流和权限——业务线 A 每天最多用 50 万个请求的代理配额,超了就排队。
代理架构: 在 Worker 和 Redis 代理池之间加一层 代理网关(Proxy Gateway)——一个独立的 HTTP 服务,Worker 的所有请求先发到网关,网关负责:选 IP、加速率限制、记录使用日志、返回代理。
Worker 侧的改造非常简单:把代理地址从具体的 IP:PORT 改成网关地址。Worker 不再直接操作 Redis,不需要知道代理池的内部逻辑。
# Worker 只需要知道网关地址
proxy = "http://proxy-gateway.internal:8080"
requests.get(target_url, proxies={"http": proxy, "https": proxy})网关内部的核心模块:
请求接入 → 鉴权(API Key / 业务线标识)
→ 限流(令牌桶,按业务线独立计数)
→ 选IP(从Redis池取,带评分、隔离、冷却逻辑)
→ 转发请求
→ 接收响应
→ 更新IP评分
→ 记录日志(业务线、目标站、IP、状态码、延迟、成本)
→ 返回响应给Worker网关可以用 Go 或 OpenResty(Nginx + Lua)实现,性能要求是能扛住所有 Worker 的并发——日均百万请求大约是每秒 12-15 QPS(均摊),峰值可能到 50-100 QPS,Go 的 net/http 或 OpenResty 都轻松撑住。
这个阶段要特别注意的事:
网关本身不能成为单点。至少部署 2 个实例,前面挂负载均衡。
日志量会很大。每天百万条请求日志,存数据库太贵,建议写到文件 + 定期归档,或者写到 ClickHouse 这类列式数据库。
成本统计口径要提前定义。按请求数计费、按 GB 计费、按成功请求计费——不同口径差异很大,提前和业务方对齐。
维度 | 阶段一:单机脚本 | 阶段二:单机+Redis | 阶段三:多机Worker | 阶段四:代理网关 |
|---|---|---|---|---|
日均请求量 | < 5万 | 5-30万 | 30-100万 | > 100万 |
IP 存储 | 本地列表/配置 | Redis Sorted Set | Redis(可 Sentinel) | Redis Cluster |
健康检测 | 脚本内部 | 独立进程 | 独立进程+分布式锁 | 网关内置+独立巡检 |
调度方式 | 轮询/随机 | 评分加权 | 评分+分布式锁 | 网关统一调度 |
业务隔离 | 无 | 按目标站分池 | 按目标站分池+域名冷却 | 按业务线+目标站 |
监控 | print/日志 | Redis ZCARD + 简单脚本 | 成功率+水位+锁竞争 | 全链路日志+成本统计 |
演进信号 | 成功率不稳定 | 单机瓶颈 | 需要统一接入层 | —— |
错误一:跳级。 日均 3 万请求就上代理网关。网关的开发和运维成本远高于直接在脚本里加个 SimpleProxyPool 类。每个阶段的方案都是为对应量级优化的,跳级只会增加不必要的复杂度。
错误二:不做水位监控就上多 Worker。 多 Worker 消耗 IP 的速度是单机的 N 倍。如果没有水位监控和自动补充机制,池子会在高峰期迅速被掏空,所有 Worker 同时取不到 IP,采集全部停转。
错误三:只看成功率,不看单位成本。 阶段三以后,成功率 95% 但每个成功请求的代理成本是 0.1 元,和成功率 85% 但单位成本 0.03 元——哪个更好取决于你的业务。演进架构时要同步建立成本统计口径,不能只盯着成功率。
Q:从阶段二到阶段三,是不是必须用 Scrapy-Redis?
不是。Scrapy-Redis 是一种方案,但不是唯一的。如果你不用 Scrapy(比如用 asyncio + aiohttp,或者用 Go 写采集器),可以自己实现分布式任务分发——用 Redis List 做队列、用 Pub/Sub 或消息队列做通知。核心是把"任务分发"和"代理调度"两个问题分开解决,不要耦合在一起。
Q:代理网关用 Go 写还是用 OpenResty?
如果团队熟悉 Go,用 Go 的 net/http + httputil.ReverseProxy,开发快、调试方便。如果团队有 Nginx 运维经验,OpenResty 的 Lua 脚本可以热更新,不需要重启网关就能改调度逻辑。日均百万级的流量两种方案都扛得住,选团队更熟悉的。
Q:Redis 在哪个阶段需要上集群?
阶段三通常不需要——Redis 单实例的吞吐(10 万+ QPS)远超代理调度的需求。阶段四如果代理池规模超过 10 万个 IP,或者你把所有业务线的日志也存在 Redis 里,可能需要上 Redis Cluster。但更常见的做法是日志不走 Redis(写 ClickHouse 或文件),Redis 只存代理池状态,这样单实例基本够用到日均千万级。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。