爬虫碰到的 DNS 问题往往不是“解析失败”,而是“慢”和“被重复做”。这篇在本机用一个包装过的 getaddrinfo 看清:requests 和 aiohttp 各自什么时候解析、缓存多久、并发解析会被什么限制,以及 localhost 为什么比 127.0.0.1 慢一点。
20 次 requests.get(每次新连接):getaddrinfo 调用 20 次,总耗时 2091 ms 一个 Session 发 20 次(复用 1 条连接):getaddrinfo 调用 1 次,总耗时 114 ms 一个 Session 发 20 次,每次 Connection: close:getaddrinfo 调用 20 次,总耗时 2094 ms
urllib3 每建一条新连接就调用一次 getaddrinfo,自己不缓存(本机操作系统可能有缓存,这里看不到)。所以 Session 的连接复用同时也省掉了 DNS;而每次 Connection: close 或每次 requests.get,都会重新解析。上面的 100 ms 延迟是我模拟的,真实的解析耗时取决于你的 DNS 服务器。
默认值:use_dns_cache=True ttl_dns_cache=10 默认(缓存开):20 个新连接,getaddrinfo 调用 1 次,请求本身累计耗时(不含主动等待)约 120 ms use_dns_cache=False:20 个新连接,getaddrinfo 调用 20 次,请求本身累计耗时(不含主动等待)约 2099 ms ttl_dns_cache=1,每次请求后等 0.4 秒:10 个新连接,getaddrinfo 调用 4 次,请求本身累计耗时(不含主动等待)约 433 ms
TCPConnector 默认缓存解析结果 10 秒(use_dns_cache=True,ttl_dns_cache=10)。同一个主机的 20 个新连接只解析了一次;关掉缓存就是 20 次;把有效期调成 1 秒,10 个相隔 0.4 秒的连接解析了 4 次。
本机 os.cpu_count() = 10,asyncio 默认线程池大小的文档公式 min(32, cpu+4) = 14 默认线程池:同时在解析的数量峰值 14,100 个请求全部完成 1625 ms 换成 100 个线程的线程池:同时在解析的数量峰值 100,100 个请求全部完成 232 ms
aiohttp 默认用 getaddrinfo,在 asyncio 的默认线程池里执行。实测同时在解析的数量峰值是 14,与 min(32, cpu+4) 的结果一致。解析慢(这里模拟 200 ms)又要抓大量不同域名时,100 个请求被分成约 8 批,用了 1625 ms;换成更大的线程池后 232 ms。另一种办法是使用异步 DNS 库,我没有安装和测试。
getaddrinfo("localhost") 的顺序:::1, 127.0.0.1
200 次新连接 × 5 轮,每次请求平均:localhost 0.92 ms,127.0.0.1 0.71 ms
拆开测:getaddrinfo("localhost") 每次 0.121 ms,getaddrinfo("127.0.0.1") 每次 0.002 ms;对 ::1 发起连接被拒绝(服务器没听 IPv6)每次 0.041 mslocalhost 解析出 ::1 和 127.0.0.1,::1 在前。服务器只听 IPv4 时,客户端先试 ::1 被拒绝,再连 IPv4。每次新连接比直接用 127.0.0.1 多约 0.21 ms,其中解析名字 0.121 ms,被拒绝的 ::1 尝试 0.041 ms(两项相加约 0.16 ms,剩下的差额没有再拆)。差距很小,但在本机压测时别把它算进服务器耗时。