——一次真实的爬虫系统架构设计评审记录
评审主题:高并发数据采集系统设计
核心争议:当前采集失败率高,是代码质量问题,还是系统架构问题?
评审结论:这是一个典型的架构失配问题,而非代码层缺陷。
一、业务背景说明
当前采集系统的目标非常明确:
- 日采集任务量不低于一万
- 目标站点具备一定反爬策略
- 必须使用代理 IP
- 允许部分失败,但不允许系统性雪崩
- 成本可控,可长期运行
在采集规模维持在一千以内时,系统运行基本稳定;
当任务量提升到五千至一万后,问题开始集中出现:
- 请求成功率明显下降
- 大量代理 IP 被快速封禁
- 线程阻塞,请求队列堆积
- CPU 与内存利用率异常
评审会议的第一个核心问题随之出现:
问题究竟出在代码层,还是系统结构本身?
二、现有系统结构回顾
当前系统属于典型的“脚本增强型爬虫”:
- 多线程或协程并发
- 每个请求独立获取代理 IP
- 请求失败立即重试
- 请求、调度、异常处理集中在同一进程
从代码质量角度评审:
- 请求逻辑清晰
- 异常处理完整
- 日志较为齐全
- 单次请求成功率尚可
代码评审并未发现明显缺陷,这意味着问题很可能不在实现细节。
三、方案一:继续深度优化代码(评审否决)
方案描述
方案提出方认为,可以通过进一步代码优化解决问题,例如:
- 精简请求与解析逻辑
- 调整超时时间
- 提升并发执行效率
- 减少不必要的数据处理
评审结论
该方案被否决,理由如下:
第一,代码优化只能改善单次请求质量,无法解决系统级资源竞争问题。
第二,高并发下代理 IP 被无序滥用,失败重试会放大请求压力。
第三,在架构不变的前提下,性能优化反而可能加速系统崩溃。
评审结论非常明确:
当采集规模达到上万时,继续在代码层“打磨细节”,并不能改变系统失稳的根本原因。
四、方案二:引入架构拆分与代理池(评审通过)
核心设计思想
将“请求执行”和“资源调度”从逻辑上彻底分离,让系统具备规模意识。
系统被拆分为四个核心模块:
- 任务调度层
负责控制整体并发规模,避免瞬时流量洪峰。 - 代理 IP 池
统一管理代理资源,控制单个 IP 的使用频率和生命周期。 - Worker 执行层
只负责执行请求,不关心代理来源和并发策略。 - 失败与降级策略层
对失败请求进行延迟重试、限流或降级处理。
五、关键模块实现说明
以下为评审通过后的核心实现示例,使用 Python 进行说明。
1. 代理统一配置入口
# 16YUN代理配置
PROXY_HOST = "proxy.16yun.cn"
PROXY_PORT = 9020
PROXY_USER = "你的用户名"
PROXY_PASS = "你的密码"
def get_proxy():
"""
返回 requests 可用的代理配置
"""
proxy_auth = f"{PROXY_USER}:{PROXY_PASS}"
proxy_url = f"http://{proxy_auth}@{PROXY_HOST}:{PROXY_PORT}"
return {
"http": proxy_url,
"https": proxy_url
}
在评审中明确要求:
代理配置必须集中管理,禁止在各个业务逻辑中随意拼接和分散使用。
2. 代理使用限速封装
import time
import threading
class ProxyLimiter:
"""
简单的代理使用限速器
"""
def __init__(self, interval=1):
self.lock = threading.Lock()
self.last_used = 0
self.interval = interval
def wait(self):
with self.lock:
now = time.time()
wait_time = self.interval - (now - self.last_used)
if wait_time > 0:
time.sleep(wait_time)
self.last_used = time.time()
proxy_limiter = ProxyLimiter(interval=0.8)
评审共识是:
代理 IP 是受限资源,而不是并发加速器。
3. Worker 执行层实现
import requests
def fetch(url):
"""
单个任务的执行逻辑
"""
proxy_limiter.wait()
proxies = get_proxy()
try:
response = requests.get(
url,
proxies=proxies,
timeout=10
)
response.raise_for_status()
return response.text
except Exception as e:
print(f"请求失败: {e}")
return None
Worker 层遵循单一职责原则:
只负责执行请求,不承担调度、限流或代理管理职责。
4. 简化版任务调度示例
from concurrent.futures import ThreadPoolExecutor, as_completed
def run(urls):
"""
控制整体并发规模
"""
results = []
with ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(fetch, url) for url in urls]
for future in as_completed(futures):
results.append(future.result())
return results
在评审结论中明确指出:
并发上限是系统的安全边界,而不是性能指标。
六、风险点与预案
- 代理被封:动态切换代理并降低请求频率
- 请求失败激增:引入失败队列与延迟重试
- 突发任务洪峰:调度层统一限流
- 单机性能瓶颈:Worker 模块支持横向扩展
七、最终评审结论
当采集规模达到上万级别时,继续纠结代码是否足够“优雅”已经失去意义。
真正决定系统稳定性的,是架构是否具备资源调度、限流和隔离能力。
一句话总结本次评审结论:
代码没有犯错,错的是让一个脚本级结构去承担系统级规模。