背景:
前些天接手了上一位同事的爬虫,一个全网爬虫,用的是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会用自己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查看它的代码,可看到下面的代码:
de 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去重
以上,优化的思路和代码就是这样;以下将已有的七亿多的去重数据转成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左右。