踩过100+坑后,我终于搞懂了Redis+Scrapy分布式爬虫的核心原理
做爬虫这行快8年了,从最开始的单线程requests,到后来的多线程threading,再到现在的Scrapy框架,我算是把爬虫的各种坑都踩了个遍。
前阵子接了个需求,要爬取某电商平台1000万+商品数据,要求7天内完成。一开始我用单机Scrapy跑,速度慢得像蜗牛,一天才爬20万条,照这速度得跑50天。客户那边催得紧,我只能硬着头皮上分布式。
结果这一搞,又踩了无数坑。Redis连接超时、去重失效、任务重复执行、数据丢失、节点负载不均… 整整折腾了一周,才把整个系统调通。今天就把这些血泪经验分享出来,让大家少走点弯路。
为什么要做分布式爬虫?
很多人觉得单机爬虫够用了,没必要搞分布式。这话没错,但仅限于小数据量场景。当你需要爬取百万级甚至千万级数据时,单机的瓶颈就非常明显了。
我给大家算笔账:假设一个请求从发送到接收响应需要1秒,单机Scrapy开100个并发,一天能爬多少条?
100个并发 × 3600秒 × 24小时 = 8,640,000条
看起来不少,但实际情况远没有这么理想。网络延迟、反爬限制、页面解析时间、数据库写入速度… 这些都会严重影响实际爬取速度。我之前那个电商项目,单机实际速度只有理论值的1/40,一天才20万条。
而且单机爬虫还有个致命问题:单点故障。一旦机器宕机,所有任务都得从头开始。我就遇到过一次,爬了3天的数据,因为服务器断电全没了,当时想死的心都有。
分布式爬虫就完美解决了这些问题:
- 水平扩展:加机器就能线性提升爬取速度
- 高可用:一个节点挂了,其他节点继续工作
- 负载均衡:任务自动分配到各个节点
- 断点续爬:任务状态持久化到Redis,随时可以暂停和恢复
Redis+Scrapy分布式架构详解
Scrapy本身是不支持分布式的,它的调度器和去重器都是基于内存的。要实现分布式,我们需要把这两个核心组件替换成基于Redis的实现。
这是我最终落地的分布式爬虫架构图:
整个架构非常清晰,核心就是Redis作为中间件,负责存储任务队列和去重集合。所有爬虫节点都从同一个Redis队列中获取任务,爬取完成后将数据写入统一的数据存储。
核心组件说明
- Redis任务队列:存储待爬取的URL,使用List数据结构,实现先进先出的队列。
- Redis去重集合:存储已经爬取过的URL指纹,使用Set数据结构,保证O(1)时间复杂度的去重。
- 爬虫节点:运行Scrapy的服务器,可以是物理机、虚拟机或容器,数量可以根据需求动态调整。
- 数据管道:统一处理爬取到的数据,进行清洗、验证和存储。
- 监控面板:实时监控Redis队列长度、去重数量、各节点爬取速度等指标。
从零开始搭建分布式爬虫
环境准备
首先,你需要安装以下软件:
- Python 3.8+
- Scrapy 2.5+
- Redis 6.0+
- scrapy-redis 0.7.2+
安装命令很简单:
pipinstallscrapy scrapy-redis redis这里要特别注意版本问题。我之前踩过一个大坑,scrapy-redis 0.6.x版本和Scrapy 2.5+不兼容,会出现各种奇怪的错误。一定要用0.7.2及以上版本。
第一步:创建Scrapy项目
scrapy startproject distributed_spidercddistributed_spider scrapy genspider example example.com第二步:修改settings.py配置
这是最关键的一步,很多人分布式爬虫跑不起来,都是因为配置错了。
# 启用Redis调度器SCHEDULER="scrapy_redis.scheduler.Scheduler"# 启用Redis去重器DUPEFILTER_CLASS="scrapy_redis.dupefilter.RFPDupeFilter"# 允许暂停和恢复爬取SCHEDULER_PERSIST=True# Redis连接配置REDIS_URL='redis://:your_password@127.0.0.1:6379/0'# 每个请求的优先级SCHEDULER_QUEUE_CLASS='scrapy_redis.queue.PriorityQueue'# 最大空闲时间,防止爬虫退出SCHEDULER_IDLE_BEFORE_CLOSE=10# 并发请求数CONCURRENT_REQUESTS=100# 下载延迟DOWNLOAD_DELAY=0.5# 禁用cookiesCOOKIES_ENABLED=False# 管道配置ITEM_PIPELINES={'scrapy_redis.pipelines.RedisPipeline':300,'distributed_spider.pipelines.MySQLPipeline':400,}这里有几个配置我要重点说一下:
SCHEDULER_PERSIST = True:这个一定要开,否则爬虫停止后,Redis中的任务队列和去重集合会被清空。我第一次没开,爬了一半重启爬虫,结果所有任务都没了。SCHEDULER_QUEUE_CLASS:默认是PriorityQueue,根据请求的优先级排序。如果你不需要优先级,可以用FifoQueue,性能更好。SCHEDULER_IDLE_BEFORE_CLOSE:当Redis队列为空时,爬虫会等待多少秒后退出。建议设置一个比较大的值,防止因为网络延迟导致队列暂时为空而退出。
第三步:修改Spider代码
原来的Spider需要继承自RedisSpider,而不是原来的scrapy.Spider。
importscrapyfromscrapy_redis.spidersimportRedisSpiderclassExampleSpider(RedisSpider):name='example'allowed_domains=['example.com']# 注释掉start_urls,改为从Redis获取# start_urls = ['http://example.com/']# Redis键,用于存储种子URLredis_key='example:start_urls'defparse(self,response):# 解析页面内容title=response.xpath('//title/text()').get()yield{'title':title,'url':response.url,}# 提取下一页链接next_page=response.xpath('//a[@class="next"]/@href').get()ifnext_page:yieldresponse.follow(next_page,self.parse)就是这么简单!原来的解析逻辑完全不用改,只需要把父类换成RedisSpider,然后注释掉start_urls,添加redis_key即可。
第四步:启动Redis服务
redis-server /etc/redis/redis.conf一定要给Redis设置密码,并且只允许本地访问,否则你的Redis会被黑客攻击。我就见过有人把Redis暴露在公网上,结果被人挖矿了。
第五步:启动爬虫节点
在每台服务器上都执行:
scrapy crawl example这时候你会看到爬虫启动了,但什么都没做,一直在等待任务。
第六步:推送种子URL到Redis
打开另一个终端,连接Redis:
redis-cli-ayour_password然后推送种子URL:
lpush example:start_urls"http://example.com/"这时候你会看到所有爬虫节点都开始工作了!
踩过的那些坑和解决方案
坑1:Redis连接超时
这是最常见的问题,尤其是在高并发场景下。
错误信息:
redis.exceptions.ConnectionError: Error 110 connecting to redis:6379. Connection timed out.解决方案:
- 增加Redis连接池大小:
REDIS_PARAMS={'socket_timeout':30,'socket_connect_timeout':30,'retry_on_timeout':True,'max_connections':1000,}- 优化Redis配置:
# redis.conf maxclients 10000 tcp-keepalive 300 timeout 0- 使用Redis集群:如果单台Redis性能不够,可以考虑使用Redis集群。
坑2:去重失效,任务重复执行
这个问题非常隐蔽,很多人都遇到过,但不知道为什么。
原因:scrapy-redis默认使用请求的URL、method、body和headers来生成指纹。但如果你的请求中有随机参数或者时间戳,每次生成的指纹都不一样,就会导致去重失效。
解决方案:自定义去重规则,只使用URL的核心部分生成指纹。
fromscrapy_redis.dupefilterimportRFPDupeFilterimporthashlibclassCustomDupeFilter(RFPDupeFilter):defrequest_fingerprint(self,request):# 只使用URL的scheme、netloc和path生成指纹url_parts=request.url.split('?')[0]fp=hashlib.sha1(url_parts.encode('utf-8')).hexdigest()returnfp然后在settings.py中配置:
DUPEFILTER_CLASS='distributed_spider.dupefilters.CustomDupeFilter'坑3:数据丢失
当爬虫节点突然宕机时,正在处理的任务会丢失。
原因:scrapy-redis默认使用的是List结构,当一个节点从List中pop出一个任务后,这个任务就从Redis中删除了。如果节点在处理这个任务的过程中宕机,这个任务就永远丢失了。
解决方案:使用SortedSet实现可靠队列。
scrapy-redis提供了一个SortedSetQueue,它会把正在处理的任务放到一个临时集合中,只有当任务处理完成后才会从临时集合中删除。如果节点宕机,临时集合中的任务会被其他节点重新处理。
SCHEDULER_QUEUE_CLASS='scrapy_redis.queue.SortedSetQueue'不过这个队列的性能比List差一些,你需要在可靠性和性能之间做权衡。
坑4:节点负载不均
有时候会出现一个节点非常忙,其他节点很闲的情况。
原因:scrapy-redis默认使用的是抢占式任务分配方式,哪个节点处理得快,哪个节点就会抢到更多的任务。如果某个节点的性能特别好,它就会抢到大部分任务。
解决方案:
- 限制每个节点的并发数:
CONCURRENT_REQUESTS=50- 使用轮询式任务分配:
你可以自己实现一个调度器,采用轮询的方式把任务分配给各个节点。不过这个比较复杂,一般情况下限制并发数就够了。
坑5:Redis内存不足
当爬取的数据量非常大时,Redis的内存会很快被占满。
解决方案:
- 定期清理Redis中的数据:
# 只保留最近7天的去重数据DUPEFILTER_EXPIRE=60*60*24*7- 使用Redis的RDB和AOF持久化:
# redis.conf save 900 1 save 300 10 save 60 10000 appendonly yes appendfsync everysec- 把数据尽快从Redis转移到数据库:
不要让数据在Redis中停留太久,爬取完成后立即写入MySQL或MongoDB。
性能优化技巧
1. 优化Scrapy配置
# 增加并发数CONCURRENT_REQUESTS=200# 减少下载延迟DOWNLOAD_DELAY=0.1# 禁用重定向REDIRECT_ENABLED=False# 禁用重试RETRY_ENABLED=False# 增加DNS缓存DNSCACHE_ENABLED=TrueDNSCACHE_SIZE=100002. 使用异步数据库驱动
不要使用同步的数据库驱动,否则数据库写入会成为瓶颈。
- MySQL:使用aiomysql
- PostgreSQL:使用asyncpg
- MongoDB:使用motor
3. 开启HTTP连接池
DOWNLOADER_HTTPCLIENTFACTORY='scrapy.core.downloader.webclient.ScrapyHTTPClientFactory'DOWNLOADER_CLIENTCONTEXTFACTORY='scrapy.core.downloader.contextfactory.ScrapyClientContextFactory'4. 使用代理池
分布式爬虫很容易被封IP,一定要使用代理池。我推荐使用scrapy-proxies或者自己搭建一个代理池。
DOWNLOADER_MIDDLEWARES={'scrapy_proxies.RandomProxy':100,'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware':110,}PROXY_LIST='/path/to/proxy/list.txt'PROXY_MODE=0监控与运维
一个好的分布式爬虫系统,必须要有完善的监控和运维机制。
我自己写了一个简单的监控脚本,可以实时查看Redis队列长度、去重数量、爬取速度等指标:
importredisimporttime r=redis.Redis(host='127.0.0.1',port=6379,password='your_password',db=0)defmonitor():whileTrue:queue_len=r.llen('example:requests')dupe_count=r.scard('example:dupefilter')item_count=r.llen('example:items')print(f"队列长度:{queue_len}")print(f"去重数量:{dupe_count}")print(f"已爬取数量:{item_count}")print("-"*50)time.sleep(5)if__name__=='__main__':monitor()如果需要更专业的监控,可以使用Prometheus+Grafana。
总结
Redis+Scrapy是目前最流行的分布式爬虫解决方案,它简单易用,性能强大,能够满足绝大多数爬虫需求。
但要真正用好它,你需要深入理解它的原理,并且踩过足够多的坑。我这篇文章里提到的问题,都是我在实际项目中遇到过的,每一个解决方案都是用无数个通宵换来的。
最后给大家一个建议:不要一开始就追求完美的分布式架构。先从单机开始,当单机性能不够时,再逐步升级到分布式。很多时候,优化单机爬虫的性能,比盲目上分布式更有效。
