最近解决一个蜘蛛爬虫的问题,需求是这样的,每发布一个网站,就要用爬虫去爬该网站所有的链接,爬虫找的是jspider,碰到的第一个问题是,如果同时发布很多网站,每个发布动作都会起一个爬虫实例
SpiderContext context = SpiderContextFactory.createContext(url);
SpiderNest nest = new SpiderNest();
Spider spider = nest.breedSpider(context);
spider.crawl(context);
这样导致爬虫服务器直接被压死,虽然加了负载均衡,用4台jboss分担压力,到发布高峰的时候,仍然会被压死
后来采取的方案是加入一个线程池,把爬取网站任务放到队列里,第一次用的线程池是ScheduledThreadPool
this.ES = Executors.newScheduledThreadPool(this.threadPoolCount);
for (int i = 0; i < this.threadCount; ++i)
this.ES.scheduleWithFixedDelay(new TrackThread(this), 200L, 5L, TimeUnit.MILLISECONDS);
}
升级后经过一段时间后来发现,工作的5个线程都不在继续工作,但是一直处于等待状态,经过简单分析,认为可能爬虫任务进入假死状态,始终不完成任务,导致我的工作线程一直在等待,变成只能接受任务,没人干活的状态了,由于时间紧迫,采取了一个临时解决方案,即等待队列数目大于100时,就认为线程池里线程已经废掉了,就重新创造一个线程池从队列中取任务
虽说能解决一定问题,但是在极端情况下,后来的线程池即使创建了,但是没完成队列里的任务后,又都over了,又要等待到达一定数目重新创建,这样会导致创造的线程永远干不完活,并且“僵尸线程”到达一定数量后,jvm最终也会挂掉
后来经过一个周末,决定换一个线程池,这次换成了ThreadPoolExecutor
//创建
queue = new ArrayBlockingQueue<Runnable>(queueSize);
threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, consuskSleepTime,TimeUnit.SECONDS, queue,
new ThreadPoolExecutor.DiscardOldestPolicy());
//将任务加入队列中
public void addUrlToArray(URL url) {
threadPool.execute(new TrackThread(url));
}
悲剧再一次重演,过了一段时间后,线程又不干活了,又都处于等待状态,这次要动真格的了,打开jconsole,观察线程运行情况
发现叫Spider的线程全部处于wait状态,终于确认,这不是我线程池的问题,其实是jspider自己的问题,漫长痛苦的读源码阶段开始了,经过观察,发现每次Spider出问题之前都会报错
14:07:59,897 ERROR [STDERR] Exception in thread "Spider 3"
14:07:59,897 ERROR [STDERR] java.lang.NullPointerException
14:07:59,897 ERROR [STDERR] at net.javacoding.jspider.core.impl.SpiderContextImpl.throttle(SpiderContextImpl.java:154)
14:07:59,897 ERROR [STDERR] at net.javacoding.jspider.core.task.work.SpiderHttpURLTask.prepare(SpiderHttpURLTask.java:38)
14:07:59,898 ERROR [STDERR] at net.javacoding.jspider.core.threading.WorkerThread.run(WorkerThread.java:130)
打开这里的源码,发现是这样的
/**
* Thread's overridden run method.
*/
public synchronized void run() {
running = true;
Log log = LogFactory.getLog(WorkerThread.class);
log.debug("Worker thread (" + this.getName() + ") born");
synchronized (stp) {
stp.notify();
}
while (running) {
if (assigned) {
state = WORKERTHREAD_BLOCKED;
task.prepare();
state = WORKERTHREAD_BUSY;
try {
task.execute();
task.tearDown();
} catch (Exception e) {
log.fatal("PANIC! Task " + task + " threw an excpetion!", e);
System.exit(1);
}
synchronized (stp) {
assigned = false;
task = null;
state = WORKERTHREAD_IDLE;
stp.notify();
this.notify(); // if some thread is blocked in stopRunning();
}
}
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/* notify the thread pool that we died. */
log.debug("Worker thread (" + this.getName() + ") dying");
}
问题就在这里,由于这里代码带过于局限片面,无从下手,开始找jspider的资料,经过一段时间分析,jspider的工作原理大致是这样的,有两个线程池,里面有两种线程,大概做两种事,第一个线程名字都叫Thinker xx,(见上面的图片),他负责发现某网站的url,完后第二个线程池里面的程序负责分析这些url,第二个线程就是报错的叫Spider xx的,当某个Spider线程完成一个任务后,就会通知“我干完活了”,并且当前线程进入wait()状态,等待下一个任务,当Thinker发现的所有url都爬取完后,当前任务就完成
而问题在于,如果某个蜘蛛线程抛出异常,就导致该线程不会执行后面的操作27~32.而直接进入wait状态,最终导致该thinker下的所有Spider都进入wait状态,进入“假死”。经过看更多源码,发现当时作者的意思是如果spider异常,直接系统退出,而这显然不符合我的需求,我需要某Spider抛出异常,不影响整个进程,大不了这个url不爬就是了。进一步分析,每个Spider完成任务后,要做两件事,一个是将当前线程置为可用状态,并且通知整个context该任务完成。
由于jspider太过于复杂,中间省去1w字,最终方案是,只要该任务异常,也要强行通知“我干完活了”就ok了,修改过的代码如下 (12~25行部分)
public synchronized void run() {
running = true;
Log log = LogFactory.getLog(WorkerThread.class);
log.debug("Worker thread (" + this.getName() + ") born");
synchronized (stp) {
stp.notify();
}
while (running) {
if (assigned) {
state = WORKERTHREAD_BLOCKED;
try {
if(null!=task){
task.prepare();
state = WORKERTHREAD_BUSY;
task.execute();
}
} catch (Exception e) {
log.fatal("PANIC! Task " + task + " threw an excpetion!", e);
System.out.println("WorkerThread error================"+e);
// System.exit(0);
}finally{//解决某个spider线程异常后,导致整个Thinker线程无法结束
task.tearDown();
}
synchronized (stp) {
assigned = false;
task = null;
state = WORKERTHREAD_IDLE;
stp.notify();
this.notify(); // if some thread is blocked in stopRunning();
}
}
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/* notify the thread pool that we died. */
log.debug("Worker thread (" + this.getName() + ") dying");
}
经过模拟抛出异常测试,终于该问题解决,结论发现,老外也有范低级错误的时候~其实修改代码不过10行,但是前前后后折腾了一个星期,把线程知识又重新温习了一遍,并且把jmeter和jconsole以及jboss的jvm监控配置学习了一下,收获还是不小的