基本架构
需求来源
爬取徽州建筑的图片,后期用于徽州建筑图片的分类处理。
调度机制
解析机器定时向调度器发送消息,告诉调度器自己当前处理了多少任务,调度器根据解析器处理的任务数,向解析器的队列发送对应量的url数据。整个大环境下,调度器与解析器形成生产者消费者队列;小环境下,解析器自己有生产者阻塞队列,消费者线程池。
好处:根据不同机器当前处理任务的能力去分发任务。
缺点:socket过程耗时。
为什么要用调度器?
主要是为了防止不同机器不同线程的高并发请求数据库带来的事务的碰撞,导致数据库处理速度变慢。通过调度器一个机器获取数据,分发给处理器的方式,可以使得数据操作变快。
线程池拒绝策略
线程池的实现中,当队列满时会调用构造时传入的RejectedExecutionHandler去拒绝任务。默认的实现是AbortPolicy,直接抛出一个RejectedExecutionException。
四种拒绝策略:
- AbortPolicy 丢弃任务,抛运行时异常
- DiscardOldestPolicy 工作队列头部的任务会被踢出队列,然后重试执行程序
- DiscardPolicy也是丢弃任务,只不过他不抛出异常。
- CallerRunsPolicy 执行任务,这个策略是不想放弃执行任务。但是由于池中已经没有任何资源了,那么就直接使用调用该execute的线程本身来执行
这里和我们的需求比较接近的是CallerRunsPolicy,这种策略会在队列满时,让提交任务的线程去执行任务,相当于让生产者临时去干了消费者干的活儿,这样生产者虽然没有被阻塞,但提交任务也会被暂停。
但这样也不能完成我们的需求,我们的需求是,当线程池任务队列满的时候,不再接受任务。最简单的做法,我们可以直接定义一个RejectedExecutionHandler,当队列满时改为调用BlockingQueue.put来实现生产者的阻塞:
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (!executor.isShutdown()) {
try {
executor.getQueue().put(r);
} catch (InterruptedException e) {
// should not be interrupted
}
}
}
};
这就无需再关心解析器任务队列和线程池的逻辑,只管往线程池提交任务就行了。
线程池核心线程数的设置
解析器处理器为i7四核,每个线程等待html的时间设置为1s,每个html的解析时间大约0.5秒,最大核心线程数=(0.5+1)/0.5*4=12。
设置每个解析器最大核心线程数为12个。
URL查重处理
通过将数据库中的url设置为hash索引,判断url是否已经爬取到。
这种速度很慢,但实现简单,速度上也不算很慢,但要是数据量再大,可以考虑使用hashset做查重过滤。数据量再大,到亿级数据,hashset会溢出内存时,可以考虑使用布隆过滤器。
网页解析
使用开源包JSOUP做页面解析。网页中有包含想要查询的关键字:如徽州,马头墙,房檐等,则认为该页面是有价值页面,将url放入数据库,并将对应的关键字一同保存,后期分类做标记数据使用。
爬虫入口
从搜索引擎中找到有关徽州建筑的前20个网页,从这些网页作为入口,从网页中往下一层一层爬取。
数据库
使用mysql集群,主从复制,读写分离,降低读写在同一个数据库中造成的不必要的事务的处理。
成果
两天,三台下载机器下载1T的徽州建筑有关的图片资源。中间解析调度器发生一次中断,实现代码中有断点重启的考虑,调度器重启之后程序照常运行。
问题解决
线程池在没有自定义拒绝策略时,无法用一个比较优秀的方法,使得线程池满的时候,停止往内部插入任务。不管是使用自带的fixedThreadPool还是cachedThreadPool,亦或者通过ThreadPoolExecutor直接创建,都不能很好的解决任务一直插入的问题。
最先想到的解决方案是等待线程池中任务运行结束(使用计数器判断运行情况),再向调度器请求数据(100个)。这样导致运行效率很低,通常需要一群线程等待一个线程运行结束,才可以进行下一步,而且会有队列中空期,这个期间没有任何任务在执行。后来对每个线程运行时间做设置,每个请求只等待1s,没有数据返回,直接抛弃url,这样速度稍有提升,但会误伤很多在等一下就可以获取的数据。
第二想到的方法,向调度器发生一个时间段内,任务执行完毕的数量,调度器通过该数量做调度。缺点是时间段不容易控制,时间段长导致很长一段时间内都没有任务在处理。太短会导致不断的任务请求。到解析器队列里的url任务,在线程池队列满时王队列里丢的数据只能通过execute执行任务(拒绝策略4)本身,使得解析器中的线程越来越多。
最后通过自定义任务拒绝策略完成最终版本,这个版本中解析器中一直有任务在处理,调度器会根据当前机器处理的快慢发布不同数量的url,达到初步的负载均衡。