tornado 网络抓取比线程实现慢

tornado web scraping is slower than threads impl

我使用 tornado 实现了一个简单的网络抓取,主要思想是将所有 url 插入队列 q 并生成多个 worker,ping url 并检查它的状态(大多数 url 不存在,即超时)

所有响应都插入到另一个队列q2,但这无关紧要,因为在所有工作人员完成后才处理此队列

我还使用与 concurrency 相同数量的线程实现了相同的方法,尽管线程在等待网络响应期间处于空闲状态,但线程实现速度要快得多,而龙卷风 IOLoop 应该是最佳选择行为类型

我错过了什么?感谢进阶

from tornado import httpclient, gen, ioloop, queues

concurrency = 100

@gen.coroutine
def get_response(url):
    response = yield httpclient.AsyncHTTPClient().fetch(url, raise_error=False)
    return response


@gen.coroutine
def main():
    q = queues.Queue()
    q2 = queues.Queue()

    @gen.coroutine
    def fetch_url():
        url = yield q.get()
        try:
            response = yield get_response(url)
            q2.put((url, response.code))
        finally:
            q.task_done()

    @gen.coroutine
    def worker():
        while True:
            yield fetch_url()

    for url in urls:
        q.put(url)

    print("all tasks were sent...")

    # Start workers, then wait for the work queue to be empty.
    for _ in range(concurrency):
        worker()

    print("workers spwaned")

    yield q.join()
    
    print("done")


if __name__ == '__main__':
    io_loop = ioloop.IOLoop.current()
    io_loop.run_sync(main)

线程实现很简单(没有多处理)并使用以下代码

for i in range(concurrency):
  t = threading.Thread(target=worker, args=())
  t.setDaemon(True)
  t.start()

这可能会变慢有几个原因:

  1. 异步编程的目标不是速度,而是可扩展性。异步实现应该在高并发水平下表现更好(特别是,它将使用更少的内存),但在低并发水平下可能没有差异,或者线程可能更快。

  2. Tornado 的默认 HTTP 客户端是用纯 python 编写的,缺少一些对性能很重要的功能。特别是它无法重用连接。如果 HTTP 客户端请求的性能对您很重要,请改用基于 libcurl 的客户端:

    tornado.httpclient.AsyncHTTPClient.configure('tornado.curl_httpclient.CurlAsyncHTTPClient')
    
  3. 有时即使在异步 HTTP 客户端中 DNS 解析也会阻塞,这会限制有效并发。在 Tornado 5.0 之前,Tornado 的默认 HTTP 客户端都是如此。对于基于 curl 的客户端,这取决于 libcurl 的构建方式。您需要使用 c-ares 库构建的 libcurl 版本。上次我查看大多数 linux 发行版时默认情况下并没有这样做。