10个0.125乘以30个2等于多少



通常,很容易将性能理解错。对于Scrapy,几乎一定会把它的性能理解错,因为这里有许多反直觉的地方。除非你对Scrapy的结构有清楚的了解,你会发现努力提升Scrapy的性能却收效甚微。这就是处理高性能、低延迟、高并发环境的复杂之处。对于优化瓶颈, Amdahl定律仍然适用,但除非找到真正的瓶颈,吞吐量并不会增加。要想学习更多,可以看Dr.Goldratt的《目标》这本书,其中用比喻讲到了更多关于瓶延迟、吞吐量的知识。本章就是来帮你确认Scrapy配置的瓶颈所在,让你避免明显的错误。

请记住,本章相对较难,涉及到许多数学。但计算还算比较简单,并且有图表示意。如果你不喜欢数学,可以直接忽略公式,这样仍然可以搞明白Scrapy的性能是怎么回事。

并行系统看起来就像管道系统。在计算机科学中,我们使用队列符表示队列并处理元素(见图1的左边)。队列系统的基本定律是Little定律,它指明平衡状态下,队列系统中的总元素个数(N)等于吞吐量(T)乘以总排队/处理时间(S),即N=T*S。另外两种形式,T=N/S和S=N/T也十分有用。

图1 Little定律、队列系统、管道

管道(图1的右边)在几何学上也有一个相似的定律。管道的体积(V)等于长度(L)乘以横截面积(A),即V=L*A。

如果假设L代表处理时间S(L≈S),体积代表总元素个数(V≈N),横截面积啊代表吞吐量(A≈T),Little定律和体积公式就是相同的。

提示:这个类比合理吗?答案是基本合理。如果我们想象小液滴在管道中以匀速流过,那么L≈S 就完全合理,因为管道越长,液滴流过的时间也越长。V≈N 也是合理的,因为管道越大,它能容下的液滴越多。但是,我们可以通过增大压力的方法,压入更多的液滴。A≈T是真正的类比。在管道中,吞吐量是每秒流进/流出的液滴总数,被称为体积流速,在正常的情况下,它与A^2成正比。这是因为更宽的管道不仅意味更多的液体流出,还具有更快的速度,因为管壁之间的空间变大了。但对于这一章,我们可以忽略这一点,假设压力和速度是不变的,吞吐量只与横截面积成正比。

Little定律与体积公式十分相似,所以管道模型直观上是正确的。再看看图1中的右半部。假设管道代表Scrapy的下载器。第一个十分细的管道,它的总体积/并发等级(N)=8个并发请求。长度/延迟(S)对于一个高速网站,假设为S=250ms。现在可以计算横街面积/吞吐量T=N/S=8/0.25=32请求/秒。

可以看到,延迟率是手远程服务器和网络延迟的影响,不受我们控制。我们可以控制的是下载器的并发等级(N),将8提升到16或32,如图1所示。对于固定的管道长度(这也不受我们控制),我们只能增加横截面积来增加体积,即增加吞吐量。用Little定律来讲,并发如果是16个请求,就有T=N/S=16/0.25=64请求/秒,并发32个请求,就有T=N/S=32/0.25=128请求/秒。貌似如果并发数无限大,吞吐量也就无限大。在得出这个结论之前,我们还得考虑一下串联排队系统。

当你将横截面积/吞吐量不同的管道连接起来时,直观上,人们会认为总系统会受限于最窄的管道(最小的吞吐量T),见图2。

图2 不同的串联排队系统

你还可以看到最窄的管道(即瓶颈)放在不同的地方,可以影响其他管道的填充程度。如果将填充程度类比为系统内存需求,瓶颈的摆放就十分重要了。最好能将填充程度达到最高,这样单位工作的花费最小。在Scrapy中,单位工作(抓取一个网页)大体包括下载器之前的一条URL(几个字节)和下载器之后的URL和服务器响应。

提示:这就是为什么,Scrapy把瓶颈放在下载器。

用管道系统的比喻,可以直观的确认瓶颈所在。查看图2,你可以看到瓶颈之前都是满的,瓶颈之后就不是满的。

对于大多数系统,可以用系统的性能指标监测排队系统是否拥挤。通过检测Scrapy的队列,我们可以确定出瓶颈的所在,如果瓶颈不是在下载器的话,我们可以通过调整设置使下载器成为瓶颈。瓶颈没有得到优化,吞吐量就不会有优化。调整其它部分只会使系统变得更糟,很可能将瓶颈移到别处。所以在修改代码和配置之前,你必须找到瓶颈。你会发现在大多数情况下,包括本书中的例子,瓶颈的位置都和预想的不同。

让我们回到Scrapy,详细查看它的性能模型,见图3。

Scrapy包括以下部分:

  • 调度器:大量的Request在这里排队,直到下载器处理它们。其中大部分是URL,因此体积不大,也就是说即便有大量请求存在,也可以被下载器及时处理。
  • 阻塞器:这是抓取器由后向前进行反馈的一个安全阀,如果进程中的响应大于5MB,阻塞器就会暂停更多的请求进入下载器。这可能会造成性能的波动。
  • 下载器:这是对Scrapy的性能最重要的组件。它用复杂的机制限制了并发数。它的延迟(管道长度)等于远程服务器的响应时间,加上网络/操作系统、Python/Twisted的延迟。我们可以调节并发请求数,但是对其它延迟无能为力。下载器的能力受限于CONCURRENT_REQUESTS*设置。
  • 爬虫:这是抓取器将Response变为Item和其它Request的组件。只要我们遵循规则来写爬虫,通常它不是瓶颈。
  • Item Pipelines:这是抓取器的第二部分。我们的爬虫对每个Request可能产生几百个Items,只有CONCURRENT_ITEMS会被并行处理。这一点很重要,因为,如果你用pipelines连接数据库,你可能无意地向数据库导入数据,pipelines的默认值(100)就会看起来很少。

爬虫和pipelines的代码是异步的,会包含必要的延迟,但二者不会是瓶颈。爬虫和pipelines很少会做繁重的处理工作。如果是的话,服务器的CPU则是瓶颈。

为了理解Requests/Items是如何在管道中流动的,我们现在还不能真正的测量流动。然而,我们可以检测在Scrapy的每个阶段,有多少个Requests/Responses/Items。

通过Scrapy运行远程登录,我们就可以得到性能信息。我们可以在6023端口运行远程登录命令。然后,会在Scrapy中出现一个Python控制台。注意,如果在这里进行中断操作,比如time.sleep(),就会暂停爬虫。通过内建的est()函数,可以查看一些有趣的信息。其中一些或是非常专业的,或是可以从核心数据推导出来。本章后面会展示后者。下面运行一个例子。当我们运行一个爬虫时,我们在开发机打开第二台终端,在端口6023远程登录,然后运行est()。

提示:本章代码位于目录ch10。这个例子位于ch10/speed。

在第一台终端,运行如下命令:

现在先不关注scrapy crawl speed和它的参数的意义,后面会详解。在第二台终端,运行如下代码:

然后在第二台终端按Ctrl+D退出远程登录,返回第一台终端按Ctrl+C停止抓取。

提示:我们现在忽略dqs。如果你通过设置JOBDIR打开了持久支持,你会得到非零的dqs(len(engine.slot.scheduler.dqs)),你应该将它添加到mqs的大小中。

(engine.scraper.slot.active_size)告诉我们这些响应的大小是115kb。除了响应,105个Items正在pipelines(engine.scraper.slot.itemproc_size)中处理,这说明还有10个在爬虫中。经过总结,我们看到瓶颈是下载器,在下载器之前有很长的任务队列(mqs),下载器在满负荷运转;下载器之后,工作量较高并有一定波动。

另一个可以查看信息的地方是stats对象,抓取之后打印的内容。我们可以以dict的形式访问它,只需通过via stats.get_stats()远程登录,用p()函数打印:

我为本章写了一个简单的评分系统,它可以让我们评估在不同场景下的性能。它的代码有些复杂,你可以在speed/spiders/speed.py找到,但我们不会深入讲解它。

  • 服务器上...的句柄(handlers)。我们可以控制这个假网站的结构(见图4),通过调节URL参数/Scrapy设置,控制网页加载的速度。不用在意细节,我们接下来会看许多例子。现在,先看一下和的不同。第一个网页在半秒内加载完毕,每页只含有一个item,第二个网页加载用了五秒,每页有三个items。我们还可以在网页上添加垃圾信息,降低加载速度。例如,查看。默认条件下(见speed/),页面渲染用时SPEED_T_RESPONSE

图4 评分服务器创建了一个结构可变的假网站

  • settings.py中的一组高性能设置。关闭任何可能使系统降速的项。因为只在本地服务器运行,我们还关闭了每个域的请求限制。
  • 一个可以记录数据的扩展,和第8章中的类似。它每隔一段时间,就打印出核心数据。

在上一个例子,我们已经用过了这个系统,让我们重新做一次模拟,并使用Linux的计时器测量总共的执行时间。核心数据打印如下:

结果这样显示出来效果很好。调度器中初始有5000条URL,结束时done的列也有5000条。下载器全负荷下并发数是16,与设置相同。抓取器主要是爬虫,因为pipeline是空的,它没有满负荷运转。它用46秒抓取了5000个Items,并发数是16,即每个请求的处理时间是46*16/ms,而不是预想的125ms,满足要求。

当Scrapy正常运行且下载器为瓶颈时,就是Scrapy的标准性能模型。此时,调度器有一定数量的请求,下载器满负荷运行。抓取器负荷不满,并且加载的响应不会持续增加。

图5 标准性能模型和一些试验结果

也可以限制活跃请求数。如果你设置了CONCURRENT_REQUESTS_PER_IP,CONCURRENT_REQUESTS_PER_DOMAIN就会被忽略,活跃请求数就是每个IP的请求数量。对于共享站点,比如,多个域名指向一个服务器,这可以帮助你降低服务器的载荷。

我们希望吞吐量取决于下载网页的平均时间,包括远程服务器和我们系统(Linux、Twisted/Python)的延迟,tdownload=tresponse+toverhead。还可以加上启动和关闭的时间。这包括从取得响应到Items离开pipeline的时间,和取得第一个响应的时间,还有空缓存的内部损耗。

总之,如果你要完成N个请求,在爬虫正常的情况下,需要花费的时间是:

所幸的是,我们只需控制一部分参数就可以了。我们可以用一台更高效的服务器控制toverhead,和tstart/stop,但是后者并不值得,因为每次运行只影响一次。除此之外,最值得关注的就是CONCURRENT_REQUESTS,它取决于我们如何使用服务器。如果将其设置成一个很大的值,在某一时刻就会使服务器或我们电脑的CPU满负荷,这样响应就会不及时,tresponse会急剧升高,因为网站会阻塞、屏蔽进一步的访问,或者服务器会崩溃。

在我的电脑上,我完成2000个请求的时间如下:

Excel函数)和前面的数据,可以计算出toverhead=6ms,tstart/stop=3.1s。toverhead可以忽略,但是开始时间相对较长,最好是在数千条URL时长时间运行。因此,可以估算出吞吐量公式是:

处理N个请求,我们可以估算tjob,然后可以直接求出T。

现在我们已经明白如何使Scrapy的性能最大化,让我们来看看如何解决实际问题。我们会通过探究症状、运行错误、讨论原因、修复问题,讨论几个实例。呈现的顺序是从系统性的问题到Scrapy的小技术问题,也就是说,更为常见的问题可能会排在后面。请阅读全部章节,再开始处理你自己的问题。

症状:当你提高并发数时,性能并没有提高。当你降低并发数,一切工作正常。下载器没有问题,但是每个请求花费时间太长。用Unix/Linux命令ps或Windows的任务管理器查看CPU的情况,CPU的占用率非常高。

案例:假设你运行如下命令:

求得抓取5000条URL的时间。预计时间是用之前推导的公式求出的,CPU是用命令查看得到的(可以在另一台终端运行查看命令):

图6 当并发数超出一定值时,性能变化趋缓。

在我们的试验中,我们没有进行任何处理工作,所以并发数可以很高。在实际中,很快就可以看到性能趋缓的情况发生。

讨论:Scrapy使用的是单线程,当并发数很高时,CPU可能会成为瓶颈。假设没有使用线程池,CPU的使用率建议是80-90%。可能你还会碰到其他系统性问题,比如带宽、内存、硬盘吞吐量,但是发生这些状况的可能性比较小,并且不属于系统管理,所以就不赘述了。

解决:假设你的代码已经是高效的。你可以通过在一台服务器上运行多个爬虫,使累积并发数超过CONCURRENT_REQUESTS。这可以充分利用CPU的性能。如果还想提高并发数,你可以使用多台服务器(见11章),这样就可以使用更多的内存、带宽和硬盘吞吐量。检查CPU的使用情况是你的首要关切。

症状:系统的运行得十分奇怪。比起预期的速度,系统运行的十分缓慢。改变并发数,也没有效果。下载器几乎是空的(远小于并发数),抓取器的响应数很少。

图7 阻塞代码使并发数无效化

无论阻塞代码位于pipelines还是爬虫,你都会看到抓取器满负荷,它之前和之后的部分都是空的。看起来这违背了我们之前讲的,但是由于我们并没有一个并行系统,pipeline的规则此处并不适用。这个错误很容易犯(例如,使用了阻塞APIs),然后就会出现之前的状况。相似的讨论也适用于计算复杂的代码。应该为每个代码使用多线程,如第9章所示,或在Scrapy的外部批次运行,第11章会看到例子。

解决:假设代码是继承而来的,你并不知道阻塞代码位于何处。没有pipelines系统也能运行的话,使pipeline无效,看系统能否正常运行。如果是的话,说明阻塞代码位于pipelines。如果不是的话,逐一恢复pipelines,看问题何时发生。如果必须所有组件都在运行,整个系统才能运行的话,给每个pipeline阶段添加日志消息(或者插入可以打印时间戳的伪pipelines),就可以发现哪一步花费的时间最多。如果你想要一个长期可重复使用的解决方案,你可以用在每个meta字段添加时间戳的伪pipelines追踪请求。最后,连接item_scraped信号,打印出时间戳。一旦找到阻塞代码,将其转化为Twisted/异步,或使用Twisted的线程池。要查看转化的效果,将SPEED_PIPELINE_BLOCKING_DELAY替换为SPEED_PIPELINE_ASYNC_DELAY,然后再次运行。可以看到性能改进很大。

症状:吞吐量比预期的低。下载器的请求数貌似比并发数多。

案例:模拟下载1000个网页,每个响应时间是0.25秒。当并发数是16时,根据公式,整个过程大概需要19秒。我们使用一个pipeline,它使用crawler.engine.download()向一个响应时间小于一秒的伪装API做另一个HTTP请求,。你可以在尝试。下面运行爬虫:

很奇怪,不仅时间多花了三倍,并发数也比设置的数值16要大。下载器明显是瓶颈,因为它已经过载了。让我们重新运行爬虫,在另一台终端,远程登录Scrapy。然后就可以查看下载器中运行的Requests是哪个:

貌似下载器主要是在做APIs请求,而不是下载网页。

讨论:你可能希望没人使用crawler.engine.download(),因为它看起来很复杂,但在Scrapy的robots.txt中间件和媒体pipeline,它被使用了两次。因此,当人们需要处理网络APIs时,自然而然要使用它。使用它远比使用阻塞APIs要好,例如前面看过的流行的Python的requests包。比起理解Twisted和使用treq,它使用起来也更简单。这个错误很难调试,所以让我们转而查看下载器中的请求。如果看到有API或媒体URL不是直接抓取的,就说明pipelines使用了crawler.engine.download()进行了HTTP请求。我们的ONCURRENT_REQUESTS限制部队这些请求生效,所以下载器中的请求数总是超过设置的并发数。除非伪请求数小于CONCURRENT_REQUESTS,下载器不会从调度器取得新的网页请求。

图8 伪API请求决定了性能

因此,当原始请求持续1秒(API延迟)而不是0.25秒时(页面下载延迟),吞吐量自然会发生变化。这里容易让人迷惑的地方是,要是API的调用比网页请求还快,我们根本不会观察到性能的下降。

解决:我们可以使用treq而不是crawler.engine.download()解决这个问题,你可以看到抓取器的性能大幅提高,这对API可能不是个好消息。我先将CONCURRENT_REQUESTS设置的很低,然后逐步提高,以确保不让API服务器过载。

下面是使用treq的例子:

可以看到一个有趣的现象。pipeline (p/line)的items似乎比下载器(d/load)的还多。这并不是一个问题,弄清楚它是很有意思的。

和预期一样,下载器中有16条请求。这意味着系统的吞吐量是T = N/S = 16/0.25 = 64请求/秒。done这一列逐渐升高,可以确认这点。每条请求在下载器中耗时0.25秒,但它在pipelines中会耗时1秒,因为较慢的API请求。这意味着在pipeline中,平均的N = T * S = 64 * 1 = 64 Items。这完全合理。这是说pipelines是瓶颈吗?不是,因为pipelines没有同时处理响应数量的限制。只要这个数字不持续增加,就没有问题。接下来会进一步讨论。

症状:下载器几乎满负荷运转,一段时间后关闭。这种情况循环发生。抓取器的内存使用很高。

案例:设置和以前相同(使用treq),但响应很高,有大约120kB的HTML。可以看到,这次耗时31秒而不是20秒:

讨论:我们可能简单的认为延迟的原因是“需要更多的时间创建、传输、处理网页”,但这并不是真正的原因。对于响应的大小有一个强制性的限制,max_active_size = 5000000。每一个响应都和响应体的大小相同,至少为1kB。

图10 下载器中的请求数不规律变化,说明存在响应大小限制

这个限制可能是Scrapy最基本的机制,当存在慢爬虫和pipelines时,以保证性能。如果pipelines的吞吐量小于下载器的吞吐量,这个机制就会起作用。当pipelines的处理时间很长,即便是很小的响应也可能触发这个机制。下面是一个极端的例子,pipelines非常长,80秒后出现问题:

解决:对于这个问题,在底层结构上很难做什么。当你不再需要响应体的时候,可以立即清除它。这可能是在爬虫的后续清除响应体,但是这么做不会重置抓取器的计数器。你能做的是减少pipelines的处理时间,减少抓取器中的响应数量。用传统的优化方法就可以做到:检查交互中的APIs或数据库是否支持抓取器的吞吐量,估算下载器的能力,将pipelines进行后批次处理,或使用性能更强的服务器或分布式抓取。

症状:爬虫对每个响应产生多个Items。吞吐量比预期的小,和之前的实例相似,也呈现出间歇性。

讨论:只有每个响应产生多个Items时才出现这种情况。这个案例的人为性太强,因为吞吐量达到了每秒1300个Items。吞吐量这么高是因为稳定的低延迟、没进行处理、响应很小。这样的条件很少见。

第二个是图11中像一个浴缸的函数。部分原因是纵坐标轴造成的。在左侧,有非常高延迟,因为达到了内存极限。右侧,并发数太大,CPU使用率太高。取得最优化并不是那么重要,因为很容易向左或向右变动。

解决:很容易检测出这个例子中的两个错误。如果CPU使用率太高,就降低并发数。如果达到了5MB的响应限制,pipelines就不能很好的衔接下载器的吞吐量,提高并发数就可以解决。如果不能解决问题,就查看一下前面的解决方案,并审视是否系统的其它部分可以支撑抓取器的吞吐量。

症状:提高了CONCURRENT_REQUESTS,但是下载器中的数量并没有提高,并且没有充分利用。调度器是空的。

案例:首先运行一个没有问题的例子。将响应时间设为1秒,这样可以简化计算,使下载器吞吐量T = N/S = N/1 = CONCURRENT_REQUESTS。然后运行如下代码:

讨论:d/load列可以确认下载器没有满负荷运行。这是因为没有足够的URL进入。抓取过程产生URL的速度慢于处理的速度。这时,每个索引页会产生20个URL+下一个索引页。吞吐量不可能超过每秒20个请求,因为产生URL的速度没有这么快。

解决:如果每个索引页有至少两个下一个索引页的链接,呢么我们就可以加快产生URL的速度。如果可以找到能产生更多URL(例如50)的索引页面则会更好。通过模拟观察变化:

图12 以每页能产生的链接数为参数的吞吐量函数

在图12中,我们可以看到吞吐量是如何随每页URL数和索引页链接数变化的。初始都是线性变化,直到到达系统限制。你可以改变爬虫的规则进行试验。如果使用LIFO(默认项)规则,即先发出索引页请求最后收回,可以看到性能有小幅提高。你也可以将索引页的优先级设置为最高。两种方法都不会有太大的提高,但是你可以通过分别设置SPEED_INDEX_RULE_LAST=1和SPEED_INDEX_HIGHER_PRIORITY=1,进行试验。请记住,这两种方法都会首先下载索引页(因为优先级高),因此会在调度器中产生大量URL,这会提高对内存的要求。在完成索引页之前,输出的结果很少。索引页不多时推荐这种做法,有大量索引时不推荐这么做。

另一个简单但高效的方法是分享首页。这需要你使用至少两个首页URL,并且它们之间距离最大。例如,如果首页有100页,你可以选择1和51作为起始。爬虫这样就可以将抓取下一页的速度提高一倍。相似的,对首页中的商品品牌或其他属性也可以这么做,将首页大致分为两个部分。你可以使用-s SPEED_INDEX_SHARDS设置进行模拟:

这次的结果比之前的方法要好,并且更加简洁 。

总结一下,Scrapy的设计初衷就是让下载器作为瓶颈。使CONCURRENT_REQUESTS从小开始,逐渐变大,直到发生以下的限制:

  • 抓取器的响应达到内存5Mb上限
  • 始终保持调度器(mqs/dqs)中有一定数量的请求,避免下载器是空的
  • 不使用阻塞代码或CPU密集型代码

图13 解决Scrapy性能问题的路线图

在本章中,我们通过案例展示了Scrapy的架构是如何影响性能的。细节可能会在未来的Scrapy版本中变动,但是本章阐述的原理在相当长一段时间内可以帮助你理解以Twisted、Netty Node.js等为基础的异步框架。

谈到具体的Scrapy性能,有三个确定的答案:我不知道也不关心、我不知道但会查出原因,和我知道。本章已多次指出,“更多的服务器/内存/带宽”不能提高Scrapy的性能。唯一的方法是找到瓶颈并解决它。

在最后一章中,我们会学习如何进一步提高性能,不是使用一台服务器,而是在多台服务器上分布多个爬虫。



9分之4的2分之1次方加上负5.6的0次方减去2又27分之10的负3分之2次方加上0.125的负3分之1次方等于多少

我要回帖

更多关于 32乘以25乘以125的简便方法 的文章

 

随机推荐