“爬虫该用线程还是 asyncio”几乎是必争的话题。这篇不讲道理,在本机用 0.1 秒延迟的服务器测:并发量逐步加大,总耗时离理论下限有多远、纯客户端开销差多少、内存怎么涨。
理论下限 = 1000 / 并发 × 0.1 秒
并发 理论下限 线程池 requests asyncio aiohttp
1 4000 ms 4258 ms 4172 ms (请求数 40)
10 2000 ms 2168 ms 2149 ms (请求数 200)
50 2000 ms 2179 ms 2164 ms (请求数 1000)
100 1000 ms 1171 ms 1125 ms (请求数 1000)
200 500 ms 698 ms 615 ms (请求数 1000)
300 333 ms失败:ConnectionError 530 ms (请求数 1000)
说明:macOS 监听队列上限 128(kern.ipc.somaxconn),并发连接一次涌入太多时测试服务器会重置连接,所以并发更高的数据不可靠,没有测服务器每个请求停 0.1 秒,并发 10 到 100 时,线程池和 asyncio 的总耗时都只比理论下限多一点点,互相之间差不到 5%。I/O 等待是主要成本时,并发模型不是瓶颈。并发 200 时才看出 83 ms 的差距;并发 300 时线程池一侧遇到了我的测试服务器的连接重置(macOS 监听队列上限 128),不能说明线程池撑不住 300。
线程池 requests:每个请求平均 0.331 ms(总耗时 993 ms) asyncio aiohttp:每个请求平均 0.109 ms(总耗时 328 ms)
服务器不延迟时,请求本身没有等待,剩下的是客户端 CPU 开销:线程池 requests 每个请求 0.331 ms,asyncio aiohttp 0.109 ms。这里换掉的是整套库,不只是线程对协程;要单独比较并发模型,需要在同一个库(比如 curl_cffi 的同步和异步版本)上做,我没有做。
并发 线程池 requests 峰值内存 asyncio aiohttp 峰值内存 (起点:导入库之后的内存)
10 49.4 MiB 47.1 MiB (起点 46.4 / 46.5 MiB;比起点多出 3.0 / 0.5 MiB)
100 56.5 MiB 48.5 MiB (起点 46.5 / 46.6 MiB;比起点多出 10.0 / 1.9 MiB)
200 65.3 MiB 49.7 MiB (起点 46.5 / 46.4 MiB;比起点多出 18.8 / 3.3 MiB)线程池里每多一个并发就多一个线程,峰值内存随并发线性增长:并发 200 时多出 18.8 MiB(约每线程 0.09 MiB)。asyncio 同样的并发只多出 3.3 MiB。这里的内存是 ru_maxrss(峰值常驻内存),且请求体很小;爬真实页面时解析结果占的内存会盖过这一项。