You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Tornado实现Web Scraping为何比线程版慢?求技术解析

为什么Tornado异步版本的Web Scraping比线程版慢?

我刚用Tornado实现了一个简单的Web Scraping工具,核心逻辑是把所有URL放到队列q里,启动多个worker去请求URL并检查状态(大部分URL不存在,会触发超时),响应结果存入另一个队列q2(这个队列的处理会在所有worker完成后进行,不影响当前流程)。

同时我用相同的并发数实现了线程版本,尽管线程在等待网络响应时会闲置,但线程版的运行速度反而快得多。按道理Tornado的IOLoop应该更适合这类IO密集型场景,请问我忽略了什么?

Tornado核心代码

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()

这问题挺典型的,你已经把异步的架子搭对了,但Tornado的默认配置和一些细节没跟上,导致没发挥出异步IO的优势。我帮你拆解几个关键原因:

1. AsyncHTTPClient的默认连接数限制是最大坑

Tornado默认用的simple_httpclient,对每个域名的并发连接数限制是1——也就是说,哪怕你开了100个worker,只要这些URL属于同一域名,它们会被串行处理!而线程版每个线程都会建立独立连接,能同时发起多个请求到同一域名,自然速度快很多。

解决方法很简单,初始化客户端时放开全局连接数限制:

# 在代码开头配置全局客户端
httpclient.AsyncHTTPClient.configure(None, max_clients=100)

# 或者在get_response里显式指定
@gen.coroutine
def get_response(url):
    client = httpclient.AsyncHTTPClient(max_clients=100)
    response = yield client.fetch(url, raise_error=False)
    return response

2. 超时设置的隐性差异

你代码里没指定超时时间,Tornado的默认connect_timeout和request_timeout比很多线程库(比如requests)要长。当大量URL不存在时,更长的超时会让异步版本整体耗时被拉长。

建议显式设置合理的超时:

response = yield httpclient.AsyncHTTPClient().fetch(
    url, 
    raise_error=False,
    connect_timeout=5,  # 连接超时5秒
    request_timeout=10  # 请求总超时10秒
)

3. 异步队列的调度开销

Tornado的queues.Queue每次get()和put()都要做协程切换,在高并发场景下,这种调度的累计开销会被放大。而线程版用的queue.Queue是基于操作系统线程调度,在大量IO等待的场景下,调度效率反而可能更高——尤其是当worker数量和CPU核心数差距不大时。

你可以试试简化队列逻辑:比如把URL列表直接分成100份,每个worker处理自己的批次,减少队列的调度次数。

4. 底层HTTP客户端的性能差距

默认的simple_httpclient是纯Python实现的,性能不如线程版常用的基于libcurl的客户端(比如requests依赖的urllib3)。你可以切换Tornado的客户端为CurlAsyncHTTPClient,它基于libcurl,性能会有明显提升:

# 开头配置客户端
httpclient.AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")

注意需要先安装依赖:pip install pycurl

总结

先从调整max_clients和超时设置入手,这两个是最容易踩的坑。如果还不够,再尝试切换到curl客户端。另外可以对比下两边的HTTP库版本,有时候不同版本的实现差异也会影响性能。

内容的提问来源于stack exchange,提问作者shahaf

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:34:10