分布式爬虫实战:基于Scrapy-Redis构建千万级数据采集系统
摘要:当单机爬虫遭遇性能瓶颈,如何平滑过渡到分布式架构?本文不讲空洞理论,从真实项目出发,完整复盘一套日均采集2000万条数据、支撑50+节点的Scrapy-Redis分布式系统。重点解决调度去重失效、节点负载不均、数据一致性难保障三大生产痛点,附可落地的架构设计与核心代码。
声明:本文仅用于技术交流与合规数据采集,请严格遵守目标网站robots.txt及相关法律法规。
一、 为什么你的分布式爬虫“分布”不起来?
很多团队在引入scrapy-redis后,以为改个配置就实现了分布式,结果发现:
- 请求重复率高达30%:默认去重策略在节点重启/网络抖动时失效;
- 部分节点闲置、部分过载:调度器无感知分配,热点域名挤爆单节点;
- 数据丢失或重复入库:Pipeline无幂等设计,Redis与数据库状态不一致;
- 监控盲区:不知道哪个节点卡死、哪个队列积压,出问题靠猜。
根本原因:scrapy-redis只是一个基础组件,不是开箱即用的生产系统。它解决了“共享队列”的问题,但没解决“可靠调度”和“数据闭环”的问题。
二、 生产级分布式爬虫架构全景
我们在原生scrapy-redis基础上,增加了四层关键能力:
这套架构的核心思想是:调度智能化、传输缓冲化、处理幂等化、运维可视化。
三、 核心痛点解决方案
3.1 调度去重:从“内存Set”到“持久化布隆过滤器”
原生scrapy-redis使用Redis Set做去重,百万级URL占用数GB内存,且节点重启后需全量重载,期间大量重复请求涌入。
优化方案:RedisBloom + 增量同步
# settings.py 自定义去重组件DUPEFILTER_CLASS="myproject.dupefilter.PersistentBloomFilter"# dupefilter.pyimportredisfromscrapy_redis.dupefilterimportRFPDupeFilterclassPersistentBloomFilter(RFPDupeFilter):""" 基于RedisBloom的持久化去重 - 内存占用降低90%(1亿URL仅需~120MB) - 重启零恢复时间 - 支持误判率可调(默认0.01%) """def__init__(self,server,key,debug=False):super().__init__(server,key,debug)self.bf_key=f"{key}:bloom"# 初始化布隆过滤器(仅首次创建)ifnotself.server.exists(self.bf_key):self.server.execute_command("BF.RESERVE",self.bf_key,0.00001,# 误判率0.001%100_000_000,# 预期容量1亿"EXPANSION",2)defrequest_seen(self,request):fp=self.request_fingerprint(request)# BF.ADD 返回1表示新元素,0表示已存在is_new=self.server.execute_command("BF.ADD",self.bf_key,fp)returnnotbool(is_new)关键细节:
- 布隆过滤器一旦创建不可删除,需预留足够容量;
- 对于需要精确去重的场景(如订单号),保留小容量Set作为二级校验;
- 定期导出布隆过滤器快照,防止Redis故障导致全量重建。
3.2 智能调度:让每个节点“忙得均匀”
默认FIFO/LIFO调度对域名无感知,导致热门域名请求集中在少数节点,触发风控;冷门域名节点空转。
自研域名感知调度器:
# scheduler.pyclassDomainAwareScheduler(Scheduler):""" 按域名分桶 + 动态权重调度 """defnext_request(self):# 1. 获取当前节点IP哈希,确定负责的域名桶node_id=get_node_hash()assigned_domains=self._get_assigned_domains(node_id)# 2. 优先从本节点负责的域名队列取请求fordomaininassigned_domains:req=self._pop_from_domain_queue(domain)ifreq:returnreq# 3. 本桶空闲时,从全局溢出池取(避免浪费)returnself._pop_from_overflow_pool()defenqueue_request(self,request):domain=extract_domain(request.url)# 4. 根据域名热度动态分配桶bucket=self._get_domain_bucket(domain)self._push_to_domain_queue(bucket,domain,request)配套措施:
- 域名热度实时统计:用Redis HyperLogLog估算各域名QPS,每5分钟重平衡一次;
- 背压机制:当某域名队列长度超过阈值,自动降低该域名的入队优先级;
- 故障转移:节点心跳超时30秒,其负责域名自动迁移至健康节点。
3.3 数据可靠性:Kafka缓冲+幂等写入
直接写数据库的Pipeline在分布式环境下极易丢数据或重复写入。我们引入Kafka作为缓冲层:
幂等Consumer核心逻辑:
# consumer.pydefprocess_item(item):idem_key=item["idempotency_key"]# 如 MD5(url+timestamp)# 原子性检查+写入withdb.transaction():exists=db.query("SELECT 1 FROM processed_keys WHERE key = %s FOR UPDATE",idem_key).fetchone()ifnotexists:db.insert("target_table",item)db.insert("processed_keys",{"key":idem_key})metrics.increment("item.new")else:metrics.increment("item.duplicate")# 无论新旧都提交offset,避免重复消费阻塞returnTrue为什么不用Redis去重代替?
Redis去重在采集端防重复请求,Kafka幂等在存储端防重复写入。两者互补:前者减少无效采集,后者保证数据最终一致。
3.4 全链路监控:让分布式系统“可见”
没有监控的分布式爬虫就是黑盒。我们定义了四个黄金指标:
| 指标维度 | 具体指标 | 告警阈值 | 业务含义 |
|---|---|---|---|
| 调度健康度 | 队列积压深度 | >10万持续5分钟 | 消费能力不足 |
| 节点活性 | 心跳间隔 / 请求产出率 | >60s无心跳 | 节点假死 |
| 数据质量 | 重复率 / 字段缺失率 | 重复率>1% | 去重或解析异常 |
| 资源效率 | CPU/内存利用率 / 带宽占用 | CPU>85%持续10分钟 | 需扩容或限流 |
Grafana看板示例结构:
┌─────────────────┬─────────────────┐ │ 实时QPS趋势 │ 各节点负载热力图 │ ├─────────────────┼─────────────────┤ │ 队列积压Top10域名│ 数据重复率曲线 │ ├─────────────────┴─────────────────┤ │ 最近1小时错误日志聚合 │ └───────────────────────────────────┘四、 部署与运维最佳实践
4.1 节点配置差异化
并非所有节点都需要相同配置:
- 调度节点:高内存(32G+)、低CPU,专注Redis操作;
- 采集节点:均衡配置(8C16G),SSD磁盘(缓存响应);
- 消费节点:高CPU、大连接池,专注数据库写入。
4.2 优雅启停协议
# 停止节点(非kill -9!)scrapy crawl myspider --stop-after=100# 完成当前批次后退出# 或通过信号量kill-SIGUSR1<pid># 触发自定义graceful_shutdown钩子关键点:节点退出前必须将未处理请求重新入队,并更新心跳状态,否则会造成请求永久丢失。
4.3 配置热更新
通过Consul/Nacos实现运行时参数调整,无需重启节点:
- 动态调整并发数(应对目标站限速变化);
- 开关特定解析规则(紧急修复解析错误);
- 切换代理IP池(原池被封时秒级切换)。
五、 性能调优实录
在某电商价格监控项目中,我们通过以下优化将吞吐量提升4倍:
- 响应压缩:启用
HTTP_ACCEPT_ENCODING=gzip,br,带宽占用降60%; - DNS缓存:本地dnsmasq缓存TTL=300s,DNS查询耗时从50ms→2ms;
- 连接复用:
CONCURRENT_REQUESTS_PER_DOMAIN=16+DOWNLOAD_TIMEOUT=10,避免连接池耗尽; - 解析异步化:CPU密集型解析任务放入进程池,不阻塞IO线程。
六、 避坑指南
- 不要过度分布式:单日<100万请求,单机Scrapy+SSD足够。分布式带来复杂度指数级上升,按需演进。
- Redis不是万能数据库:仅用于调度和临时状态,业务数据务必落库。Redis宕机不应导致历史数据丢失。
- 尊重目标站点:设置合理的
DOWNLOAD_DELAY和AUTOTHROTTLE,分布式不等于可以无限加速。我们曾因速率过高被某平台法务函警告,此后所有项目强制接入速率审批流程。 - 测试环境先行:分布式问题在单机无法复现。搭建最小化集群(3节点)验证后再上生产。
七、 总结
分布式爬虫的本质是用工程手段对抗不确定性。调度要容忍节点失效,传输要容忍网络抖动,存储要容忍重复投递。当你把每个环节都设计成“可失败、可恢复、可验证”时,系统才真正具备生产级韧性。
希望这篇实战复盘能帮你跨越从“能跑”到“稳跑”的鸿沟。如果你的系统正面临具体瓶颈,欢迎在评论区描述场景——分布式的问题,往往藏在细节里。
