并行系统和管道系统很类似,在计算机科学中,我们用队列符号来代表队列和正被处理的元素(图1的左边部分)。排队系统的一个基础的定律就是Little定律,即系统达到平衡时,系统中元素的个数(N)等于系统的吞吐量乘以总的排队时间(S),N = T · S。
对于管道的几何问题也有一个相似的规律(图1的右边部分)。管道的容量(V)等于管道的长度(L)乘以它的截面积(A),V = L · A。
如果用长度代表服务时间(即排队时间)(L ~ S),容量代表系统中的元素数目(V ~ N),截面积代表吞吐量(A ~ N),那么Little定律就和容积公式是一样的了。
这样的类比有意义吗?答案是肯定的。如果把工作的单元看做是在管道中以匀速移动的小液滴,那么L ~ S就是有意义的,因为管道越长,液滴通过花费的时间也越多。V ~ N也是有意义的,因为管道越大,它就能容纳越多的液体。A ~ T的类比有些迁强了。在管道中,真正的吞吐量,即每秒进出管道的液滴数量,叫做”体积流量”,并且除非一些特殊的情况(比如说管道上有孔),否则它是与A^2,而不是A成正比的。这是因为一个宽的管道不仅意味着更多的液体可以出去,还意味着液体的流动更快了,因为有了更多的空间。不过在这里我们可以忽略这些细节,假设压力和速度都是常量,吞吐量只与截面积成正比。
Little定律和简单的容积公式是很相似的,这就是为什么管道模型这么直观这么有力的原因。仔细地分析一下图1右边的部分,假设这个管道系统代表了Scrapy的下载器。第一个下载器很“细”,可能总共有8个并发请求的容量/并发水平(N)。对于一个比较快的网站,它的长度/延时(S)假定是250ms。给定了N和S,我们就能计算出吞吐量T = N/S = 8/0.25 = 32个请求/秒。
你可能会注意到,延时是不受我们所控制的,因为它主要取决于远程服务器的性能和网络的延时。我们比较容易控制的是下载器的并发水平(N),正如在图1中所看到的,可以增加到16或者32都行。在长度(即排队时间)固定的情况下,我们只能通过增加截面积(系统的容量)来增加吞吐量。根据Little定律,如果并发的有16个Request
,那么每秒就有T = N/S = 16/0.25 = 62个请求,如果有32个的话,那么每秒就有128个请求。似乎可能通过增加并发来使得一个系统具有无限快的速度,在得出这样的结论之前,我们也应该考虑一下级联排队系统的影响。
级联的排队系统
当你把数个不同截面积/吞吐量的管道连接在一起时,你就会理解这个级联系统的流量是受最窄(最小吞吐量)的管道所限制的。
从上图中也可以看到,最窄的管道所放置的位置也决定了其余管道的容量有多满(即负载有多大)。如果把这些类比成对计算机的内存要求,你就会认识到瓶颈摆放的位置是十分重要的,所以最好能把最满的管道的位置摆在单位工作量最少的地方。在Scrapy中,一个单位的工作(抓取一个网页)由一个URL(只有少量字节)——下载器之前——和这个URL以及服务器的响应(很多字节)——下载器之后。
这就是为什么在Scrapy系统中把下载器设计为瓶颈的原因。
找出瓶颈
上节中我们关于管道系统的比喻已经把识别瓶颈的过程可视化地表现出来了,如果观察一下图2,你会注意到“瓶颈”之前的管道都很满,而“瓶颈”之后的不是很满。
好消息是,在大多数排队系统中,你可以使用它们的测量工具相对方便地监视这个系统多“满”。通过仔细地视察Scrapy的队列,我们可以知道瓶颈在哪,如果不在下载器那里的话,我们可以通过设置使它转移到下载器。任何没有改进瓶颈的改进都不会对提高吞吐量有任何的好处。如果对系统的其他部分有所修改,只会让情况变得更加糟糕,甚至也可能把瓶颈转移到了其他地方。在你修改代码或者设置之前,必须遵寻一些系统的方法,找出瓶颈在哪,并且知道怎么下手。在一起很多情况下,瓶颈并不是在我们本来期望它在的位置。
Scrapy的性能模型
现在回到Scrapy并仔细地看一下它的性能模型(图3)。
Scrapy由下面几部分组成:
- 调度器:所有的
Request
对象都在这里排队,直到下载器已经准备好了 来处理它。Reuqest
主要由URL组成,所以比较紧凑,也就是说即使有很多Request
也不会导致性能问题,并且还可以使下载器一直处于满负荷工作状态。 - 节流器(throttler):这是一个安全阀门,它从scraper获得反馈,如果正在处理的响应加起来超过了5MB,它就会阻止
Request
对象进入到下载器中。这可能会导致性能波动。 - 下载器:对于性能方面,这是Scrapy最值得关注的一个组件。它对于可以同时并发处理的
Request
对象的数目有着复杂的限制。它的延迟(亦即管道的长度)等于远程服务器响应的时间加上网络/操作系统和Python/Twisted的延迟。我们可以调整并发的Request
对象的数目,但是没法控制延迟,所以下载器的容量由CONCURRENT_REQUESTS*
设置项来控制。 - 爬虫:这是scraper的一部分,它从返回的响应中提取
Item
和接下来的Request
对象。一般情况下,只要按照规则来写这部分的代码,爬虫就不会成为性能瓶颈。 - Item pipelines:这是scraper的第二个部分。爬虫对应于每个
Request
会产生许多个Item
,不过只有CONCURRENT_ITEMS
个会同时进行并发处理。这点是很重要的,因为,例如,你在pipeline中进行数据库操作,或许就会无意识地对你的数据库进行了洪泛攻击,因为默认值(100)就已经太高了 。
爬虫和pipeline都有异步的代码,并且会导致大部分的延迟,但是即使这样,它们也不应该被当做瓶颈。极少数情况下,爬虫和pipeline会做一些复杂的处理,此时的瓶颈会是我们服务器的CPU。