背景:
前些天接手了上一位同事的爬虫,一个全网爬虫,用的是scrapy+redis分布式,任务调度用的scrapy_redis模块。
大家应该知道scrapy是默认开启了去重的,用了scrapy_redis后去重队列放在redis里面。我接手过来的时候爬虫已经有7亿多条URL的去重数据了,再加上一千多万条requests的种子,redis占用了一百六十多G的内存(服务器,Centos7),总共才一百七十五G好么。去重占用了大部分的内存,不优化还能跑?
一言不合就用Bloomfilter+Redis优化了一下,内存占用立马降回到了二十多G,保证漏失概率小于万分之一的情况下可以容纳50亿条URL的去重,效果还是很不错的!在此记录一下,最后附上Scrapy+Redis+Bloomfilter去重的Demo(可将去重队列和种子队列分开!),希望对使用scrapy框架的朋友有所帮助。
接下来还会对种子队列进行优化,详见:《scrapy_redis种子优化》。
记录:
我们要优化的是去重,首先剥丝抽茧查看框架内部是如何去重的。
- 因为scrapy_redis会用自己scheduler替代scrapy框架的scheduler进行任务调度,所以直接去scrapy_redis模块下查看scheduler.py源码即可。
- 在open()方法中有句
self.df = RFPDupeFilter(...)
,可见去重应该是用了RFPDupeFilter这个类;再看下面的enqueue_request()方法,里面有句if not request.dont_filter and self.df.request_seen(request):return
,看来self.df.request_seen()这就是用来去重的了。 - 按住Ctrl再左键点击request_seen查看它的代码,可看到下面的代码:
def request_seen(self, request):
fp = request_fingerprint(request)
added = self.server.sadd(self.key, fp)
return not added
- 可见scrapy_redis是利用set数据结构来去重的,去重的对象是request的fingerprint。至于这个fingerprint到底是什么,可以再深入去看request_fingerprint()方法的源码(其实就是用hashlib.sha1()对request对象的某些字段信息进行压缩)。我们用调试也可以看到,其实fp就是request对象加密压缩后的一个字符串(40个字符,0~f)。
是否可用Bloomfilter进行优化?
以上步骤可以看出,我们只要在这个request_seen()
方法上面动些手脚即可。由于现有的七亿多去重数据存的都是这个fingerprint,所有Bloomfilter去重的对象仍然是request对象的fingerprint。更改后的代码如下:
def request_seen(self, request):
fp = request_fingerprint(request)
if self.bf.isContains(fp): # 如果已经存在
return True
else:
self.bf.insert(fp)
return False
self.bf是类Bloomfilter()的实例化,关于这个Bloomfilter()类,详见《基于Redis的Bloomfilter去重(附Python代码)》。
以上,优化的思路和代码就是这样;以下将已有的七亿多的去重数据转成Bloomfilter去重。
- 内存将爆,动作稍微大点机器就能死掉,更别说Bloomfilter在上面申请内存了。当务之急肯定是将那七亿多个fingerprint导出到硬盘上,而且不能用本机导,并且先要将redis的自动持久化给关掉。
- 因为常用Mongo,所以习惯性首先想到Mongodb,从redis取出2000条再一次性插入Mongo,但速度还是不乐观,瓶颈在于MongoDB。(猜测是MongoDB对_id的去重导致的,也可能是物理硬件的限制)
- 后来想用SSDB,因为SSDB和Redis很相似,用list存肯定速度快很多。然而SSDB唯独不支持Centos7,其他版本的系统都可。。
- 最后才想起来用txt,这个最傻的方法,却是非常有效的方法。速度很快,只是为了防止读取时内存不足,每100万个fingerprint存在了一个txt,四台机器txt总共有七百个左右。
- fingerprint取出来后redis只剩下一千多万的Request种子,占用内存9G+。然后用Bloomfilter将txt中的fingerprint写回Redis,写完以后Redis占用内存25G,开启redis自动持久化后内存占用49G左右。
福利福利:
献上Demo一个,链接:使用Bloomfilter去重的scrapy_redis。
Demo功能:启动spider1(或spider2),start_urls中有10条URL,其中4条是重复的,可以看到 parse1() 只处理了去重后的6条URL。
Demo去重功能的迁移:
- 将BloomfilterOnRedis_Demo目录下的 scrapy_redis 文件夹拷贝到你项目中settings.py的同级目录,在settings.py中增加几个字段:
FILTER_URL = None
FILTER_HOST = 'localhost'
FILTER_PORT = 6379
FILTER_DB = 0
# REDIS_QUEUE_NAME = 'OneName' # 如果不设置或者设置为None,则使用默认的,每个spider使用不同的去重队列和种子队列。如果设置了,则不同spider共用去重队列和种子队列
""" 这是去重队列的Redis信息。 原先的REDIS_HOST、REDIS_PORT只负责种子队列;由此种子队列和去重队列可以分布在不同的机器上。 """
- 以上两个步骤即可实现BloomfilterOnRedis去重。(注意
import scrapy_redis
要改成import 项目名.scrapy_redis
,即导入这个新的scrapy_redis,不要导错了!) - 特别说明一下
REDIS_QUEUE_NAME
这个字段。刚才放在demo里面的有spider1和spider2,分别启动一下两个爬虫,可以看到两个爬虫的去重队列和种子队列的名字是不一样的,即不是共用一个去重队列和种子队列的。如果项目需要,不同spider也要使用同一个去重队列和种子队列,则将这个REDIS_QUEUE_NAME
设置成你想要的名字,此时同一个项目下的不同爬虫也会使用同一个去重队列和种子队列。 - 如果待去重的数据量比较大,需要修改scrapy_redis/dupefilter.py中第14行的blockNum值,默认blockNum=1。Bloomfilter算法是有漏失概率的(即不存在的会误判为存在),在保证漏失率小于万分之一的情况下,一个blockNum可满足7千万条数据的去重,一个blockNum占用256M内存(注意Linux如果开了自动持久化,redis占用内存会加倍)。
转载请注明出处,谢谢!(原文链接:http://blog.csdn.net/bone_ace/article/details/53099042)