jspider的一个bug

  最近解决一个蜘蛛爬虫的问题,需求是这样的,每发布一个网站,就要用爬虫去爬该网站所有的链接,爬虫找的是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监控配置学习了一下,收获还是不小的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值