欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

一、关于爬虫的一点想法

程序员文章站 2022-07-14 08:33:23
...
                    
                      
关于爬虫的一点想法(一)


小弟毕业后参加工作的过程中在iteye汲取了很多营养,一直想写点什么回报这种无私共享的精神,之前策划了一篇文章《论工作的那些事》由于比较长,还需等一段时间.

关于爬虫的一点想法:
爬虫的大致业务流程是:下载、解析、存储,不断重复(任务调度),那么想提高爬虫的运行效率是不是可以从以下几个方面着手.这个系统的上下文大概是这样的,网络、服务器(cpu、cache、disk)、爬虫程序
1、爬虫对网络带宽要求较高,在有限的资源下,我们能做的就是提高带宽的利用率,那么这里的话我们就可以尽量增加同一时刻执行下载线程的数量(不是一味的多就好),但是由于linux系统的特性,所有对其的调用都是文件的操作(大概是这意思,之前在一本书上看到的,非专业),linux对文件打开数量有限制,即使可以修改参数,所以对于一个大规模的爬虫增加服务器是必要的也是必然的,这样就涉及分布式,涉及分布式就涉及任务的调度,实现一个无延迟的任务调度需要思考,在数据结构上可能disruptor比较适合,另外jdk的blockqueue(需要理解内部机制)也不错。DNS解析耗费大量不必要的时间,可以在本地增加DNS缓冲池,之前测过一般我们要访问一台www服务器一般要经过2~3个路由器,更远的可能3~5个。linux下可以使用tracert命令进行路由的跟踪。
2、提高cache的工作效率。Java不同于C可以为任务指定cpu执行,这里有个非常关键的问题,就是CPU伪共享问题,现在pc都有了三级缓存,但大多树都是cpu共享的,这样就产生了伪共享问题,例如cpu1访问cache段C1的A字节,同时CPU2访问cache段C1中的B字节,实际上A和B毫无关系,但是由于CPU寻址的问题,他每次都是加载16字节,所以就造成CPU2需要重新去内存加载B的内容,而B没有变更,造成了无形的效率问题,redis中有这个的解决方案,就是在自己的变量前后各添加16字节的byte数组。
3、disk问题。爬虫服务器没必要弄个ssd,基本都是机械硬盘,机械硬盘的特点是顺序读写比较好,整个数据操作的过程中磁头寻道的时间是主导,所以有效减少磁盘寻道时间会比较有效,这里有两种解决方案:A,优化程序尽量按磁盘块大小进行操作,当然jdk已经考虑到这个问题,buffer大小默认好像是4K ,这个和磁盘块大小是一致的,我也写过简单程序测试过。
B,添加raid卡,不过这个成本好像比较高。添加了raid卡的一个磁盘阵列,具有多个磁头,理论上相当于磁盘效率正比例提高了,因为同一时刻可以多个磁头同时工作。
C,尽量顺序操作。虽然现在linux对磁盘的随机读写有优化,例如,预读策略,但这应该是最后一个关卡,我们可以通过程序尽量避免随机操作。
4、cpu+数据总线+磁盘的工作方式,这个之前在《大话处理器》上看到的,没见过类似的具体应用。cpu读取数据的过程cpu是不参与的,而是通过数据总线,在数据从磁盘加载到内存中间的操作是由数据总线完成的,这段时间cpu是空闲的,是否有某些方法可以充分利用这段时间。这里就跟操作系统的cpu调度策略有关了。
5、多线程的合理运用,不是开的越多越好。线程主要解决是阻塞问题,不论是锁还是资源等待都会造成这个问题,下面从这两个方面写。
A,资源等待。在爬虫系统中大量运行时间都花在网络资源获取上,这个可能是由于带宽问题或服务器超出了打开最大文件数限制引起(httpclient运用不好就会造成这个bug),之前看过一篇文章,大概意思是系统要运行一个任务T,T运行时间=T资源等待时间+T处理时间,程序为这个任务开的任务数同CPU有个比例函数关系。大概是线程数=(T处理时间/T等待时间+1)*CPU内核数(可能不准确)。
B,锁问题。锁是影响程序执行性能的一大问题,能不用就不用,jdk也自带了current包。举个例子,disruptor是一个无锁的高效队列,非常适合生产者/消费者问题,并且支持消费者层级关系,即消费者A处理完由消费者B处理。数据结构模型也不复杂。
6、对象缓存运用,apache有开源实现。java是个面向对象的语言,现在开发程序更是无所不用其及,想法用面向对象的思想,一切都是对象,有利有避,开发/维护简单了,运行效率就不好说了,大量的GC不但会影响程序性能,同时其自身也有个bug----GC回收不及时,这个平常可能很少见,但是对于淘宝那样的高并发,他们就曾经遇到过这个问题,最后是由他们的jvm团队修改jvm代码解决的。apache这个开源的实现包不大,使用也比较简单,简单的说有一个对象池,每次对特定对象的操作都需要找这个池去要,使用完归还,途径这个池时可以进行对对象的处理,例如初始化、挂起、激活、销毁等动作。对于爬虫这样大量对象的操作我认为对象缓存是必要的。我们的微薄采集程序用了,我们是采用api调用的方式处理的,在每次通过api获取数据时都可以指定页数,例如我要获取JERRY的新浪微薄数据,因为jerry刷微薄都是在一个很短的时间段完成的,大部分时间没有更新,所以每次只请求很少的记录条数,减少网络传输的数据,我们将api的请求参数封装成一个对象Query,请求是调用toString()即可,这个方法用反射将query对象的成员变量和值组成url形式返回,并且进行了缓存,通过去重机制判断是否有下一页,如果有下一页就对这个query进行一个深拷贝,形成一个子任务返回给调度器,并且设置启动时间为立即。
7、NIO的运用,最新的httpclient包已经支持NIO。
8、JVM参数配置。我们一般都将jvm设置成server模式,虽然启动会慢,但有一个显著的性能提升,尤其明显的是对方法的调用提高很多很多,当然jvm自身也会判断该工作在client模式还是server模式。jvm还有很多参数,一般我们都关心内存,对于jvm的内存模型,也许有必要深入研究,尤其是几个区的分配,避免自动扩增,之前也看了一遍《深入理解JVM》,但基本没应用到工作也没记住多少。
9、JDK版本的选择,如果你对内存的大小没太大要求,还是用32位的比较好,如果需要大量内存那只能用64位的了,对于JDK的32位和64位有过这么一个测试,前者运行效率全面超过后者。JDK64位有个自动补全机制。
10、nosql的运用。heritrix是个特别好的单机开源爬虫框架,内部有很多好的思想,其内部运用了BDB做为其内存数据库。现在key-value类型的nosql比较多,redis比较好用,之前看memlink正在研发,是基于redis的,官方说实现了完全的读写分离。


非常愿意和大家一起讨论,谢谢!!!!