我已经实现了一个爬虫系统。这个爬虫系统能够定时地爬取 Bilibili 的视频以及作者,并保存历史数据。

这个爬虫运行得十分良好,但是还有很多不足的地方。比方说,这个系统的生产者(产生待爬取链接的服务)以及消费者(获得爬取链接去爬取内容的爬虫)是紧密地耦合在一起的,非常不利于系统的扩展。此外,由于爬取对象的 API 调用速率限制,单机所有爬虫对单个 API 的爬取速率不能太快,不能在短时间内爆发大量的请求。此外对临时的请求,我的系统不能很快地做出反应。因为原先的爬虫系统是定时爬取的,只有在某些固定时刻开启爬虫。

需求

引入消息队列是一种非常自然的解决方案。

我的需求主要有两个:

解耦

我需要对生产者和消费者进行解耦,实际上爬虫服务和链接生成服务是可以独立的。事实上可以理解为链接生成应用程序发布了爬取任务,然后爬虫应用程序消化这些任务,这就是所谓的发布-订阅模式。通过消息队列可以很轻松地实现这种模式。

由此的好处是链接生成和爬虫不再互相依赖了。链接生成模块不需要去在意爬虫是怎么爬取的,爬虫也不需要知道链接是如何生成的。生产者只需要将链接放入消息队列中,而消费者只需要从中取出链接。

如果需要扩展服务,比如添加一个新的爬虫,生成者的代码可以完全不用改变。同理,添加一种新的产生链接的规则不必再去考虑爬虫如何爬取。

削峰

其实这里的削峰比较奇葩,削峰是为了减轻短时间内大量请求打炸服务器。我这里主要用作防止爬虫爬取过快被封。

原先的设计里,我如果设计了多个爬虫,对同一个 api 进行爬取,每一个爬虫的限速都是自身的。如果有 10 个爬虫,每秒爬一次,那么对于这台服务器而言,每秒发出了 10 个请求。如果被爬的 API 限速就是每秒 10 个请求,那么我如果要添加一个爬虫对相同的 API 进行爬取,就需要修改爬虫间隔时间。这种修改还很不稳定,因为很可能导致一下子发了十个请求,过了一秒再发十个这样的操作,不是我们想要的情形。而如果使用消息队列,结构就变成了多个链接生成器往一个队列里装填链接,而一个爬虫服务以每秒 10 个链接的速度消耗请求,从而保障了峰值不会太高。

异步

既然使用了消息队列,那么传递消息并消费消息就是一个异步的工程了。

一个繁忙的消息队列不会立即消费掉爬取请求。实际上爬取信息也是一个相对比较慢的工序。

现在有一个需求是立即刷新数据,即需要立即放出爬虫进行爬取。针对这一操作,用户发出请求后,应该是立即往消息队列中放置了一条待爬取链接,然后直接返回添加成功的信息。至于真正的爬取工作是异步执行的。这样用户会有一种处理很迅速的错觉。