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

