A. Jesse Jiryu Davis是纽约MongoDB的工程师。他撰写了Motor,即异步MongoDB Python驱动程序,他是MongoDB C驱动程序的主要开发者,也是PyMongo团队的成员。他贡献于asyncio和龙卷风。他写在http://emptysqua.re。
Guido van Rossum是Python的创建者,它是网络和网络上的主要编程语言之一。
Python社区将他称为BDFL(Benevolent Dictator For Life),一个直接来自Monty Python短片的标题。
Guido在网上的家是http://www.python.org/~guido/。
介绍
古典计算机科学强调有效的算法,尽可能快地完成计算。但是许多网络程序花费时间不用计算,但是保持开放的许多连接速度很慢,或者是偶然的事件。
这些程序提出了一个非常不同的挑战:有效地等待大量的网络事件。这个问题的现代方法是异步I / O或“异步”。
本章介绍一个简单的网络爬虫。爬虫是一个原型的异步应用程序,因为它等待许多响应,但几乎没有计算。
一次可以获取的页面越多,越快完成。如果它将线程用于每个正在运行的请求,那么随着并发请求数量的增加,它将在内存不足或其他与线程相关的资源耗尽内存之前耗尽。它通过使用异步I / O避免了线程的需要。
我们将这个例子分为三个阶段。首先,我们显示一个异步事件循环,并绘制一个使用事件循环与回调的爬网程序:它是非常有效的,但将其扩展到更复杂的问题将导致无法管理的意大利面条代码。第二,因此,我们展示了Python协同程序是高效和可扩展的。我们使用生成器函数在Python中实现简单的协同程序。
在第三阶段,我们使用Python标准“asyncio”库(http://aosabook.org/en/500L/a-web-crawler-with-asyncio-coroutines.html#fn1)的全功能协同程序,并使用异步队列进行协调。
任务
网页抓取工具查找并下载网站上的所有页面,也许可以归档或索引。从根URL开始,它会获取每个页面,解析它以查看页面的链接,并将其添加到队列中。它在获取没有未看到的链接的页面并且队列为空时停止。
我们可以通过同时下载多个页面来加速这个过程。当抓取工具找到新的链接时,它会在单独的插槽上启动对新页面的同时提取操作。它在到达时解析响应,向队列添加新链接。可能会出现一些递减的回报,其中太多的并发降低性能,因此我们将并发请求的数量上限,并将其余链接留在队列中,直到某些正在运行的请求完成。
传统方法
我们如何使爬虫并发?传统上我们将创建一个线程池。每个线程将通过套接字一次下载一页。例如,要下载一个页面xkcd.com
def fetch(url):
sock = socket.socket()
sock.connect(('xkcd.com', 80))
request = 'GET {} HTTP/1.0\r\nHost: xkcd.com\r\n\r\n'.format(url)
sock.send(request.encode('ascii'))
response = b''
chunk = sock.recv(4096)
while chunk:
response += chunk
chunk = sock.recv(4096)
# Page is now downloaded.
links = parse_links(response)
q.add(links)
默认情况下,插座操作阻塞:当线程调用等的方法connect或者recv,它会暂停,直到操作完成。2因此,一次下载多个页面,我们需要许多线程。复杂的应用程序通过在线程池中保留空闲线程来摊销线程创建的成本,然后检查它们以便重新使用以用于后续任务; 它与连接池中的套接字相同。
然而,线程是昂贵的,并且操作系统对进程,用户或机器可能具有的线程数量强制执行各种硬上限。
在Jesse的系统上,Python线程花费大约50k的内存,并启动数以万计的线程会导致失败。
如果我们在并发套接字上同时执行数万个并发操作,那么在我们用完了套接字之前,我们用完了线程。
线程上的线程开销或系统限制是瓶颈。在他有影响力的文章“C10K问题” 3中,Dan Kegel概述了I / O并发多线程的局限性。他开始,现在是网络服务器同时处理万个客户的时候了,你不觉得吗?毕竟网络是一个很大的地方。
凯格尔在1999年创造了“C10K”一词。现在有一万个连接听起来很精致,但问题只有在尺寸上才有变化,而不是实物。那么,当使用C10K的每个连接的线程是不切实际的。现在的帽子数量级更高。事实上,我们的玩具网络抓取工具可以正常工作。然而,对于非常大规模的应用程序,拥有成千上万的连接,仍然保留上限:有一个限制,大多数系统仍然可以创建套接字,但已经用完了线程。我们如何克服这个?
异步
异步I / O框架使用非阻塞套接字在单个线程上执行并发操作。在我们的异步抓取工具中,我们设置套接字非阻塞,然后我们开始连接到服务器:
sock = socket.socket()
sock.setblocking(False)
try:
sock.connect(('xkcd.com', 80))
except BlockingIOError:
pass
刺激性地,非阻塞套接字connect即使正常工作也会引起异常。此异常复制下面的C函数,它设置的刺激性行为errno来EINPROGRESS告诉你它已经开始。现在,我们的抓取工具需要一种方法来知道连接何时建立,因此可以发送HTTP请求。我们可以简单地继续努力:
request = 'GET {} HTTP/1.0\r\nHost: xkcd.com\r\n\r\n'.format(url)
encoded = request.encode('ascii')
while True:
try:
sock.send(encoded)
break # Done.
except OSError as e:
pass
print('sent')