aiohttp默认用同步阻塞的socket.getaddrinfo解析DNS,导致事件循环卡顿;需同时配置ttl_dns_cache=300、aiodns.AsyncResolver()和limit_per_host=10才能实现真正异步DNS解析。

aiohttp 默认用 socket.getaddrinfo 做 DNS 解析,这是同步阻塞调用 —— 无论你写了多少 async def,只要碰到没缓存过的域名,事件循环就卡住。这不是你代码写错了,是默认行为本身就不异步。
为什么 aiohttp 默认 DNS 解析会拖慢并发请求
现象很典型:发 100 个 session.get("https://example.com"),响应时间波动极大,甚至频繁触发 TimeoutError,但目标服务器本身毫秒级响应。用 strace -e trace=getaddrinfo,connect 一看,大量时间停在 getaddrinfo 上。
-
aiohttp.TCPConnector默认ttl_dns_cache=0,即完全禁用 DNS 缓存,每次请求都重新查 - 即使同一域名反复请求,也无复用;查一次可能耗时 200ms+,100 次就是额外 20 秒
- 这个环节根本不在 asyncio 调度范围内,线程级阻塞,整个 event loop 被拖住
如何用 aiodns 替换默认 DNS 解析器
关键不是“自己调用 aiodns 查 IP 再拼 URL”,而是让 aiohttp 在建连阶段自动走异步解析路径。这需要替换其底层 resolver 实例。
- Linux/macOS:直接
pip install aiodns,然后传AsyncResolver()给TCPConnector - Windows:必须先
pip install pycares,否则aiodns初始化失败(报ImportError: No module named '_cares') - 别漏掉
ttl_dns_cache=300,否则首次解析虽异步,但重复解析仍走系统调用 - 示例配置:
connector = aiohttp.TCPConnector(<br> ttl_dns_cache=300,<br> resolver=aiodns.AsyncResolver()<br>)
limit_per_host 设置不当会让 DNS 成雪崩点
很多人只设 limit=100,但没配 limit_per_host。结果是:80 个请求全挤在 example.com 上,DNS 查询排队,其余域名干等。
-
limit_per_host=0(默认)=不限制单域名并发,极易触发 DNS 请求堆积 - 建议显式设为
limit_per_host=10,配合limit=100,可均衡多域名负载 - 若只爬 1~2 个域名,
limit_per_host比limit更影响实际吞吐
不推荐用 run_in_executor 做 DNS 解析兜底
虽然能绕过阻塞,但引入线程切换开销,且无法利用 DNS 缓存机制,实测吞吐常低于原生 aiodns 方案。
立即学习“Python免费学习笔记(深入)”;
-
loop.run_in_executor(None, socket.gethostbyname, "example.com")是临时救急手段,不是生产方案 - 它不共享
aiohttp的连接池和 DNS 缓存,每次都要重建上下文 - 高并发下线程数失控风险高,尤其在容器或低内存环境
aiodns + ttl_dns_cache + limit_per_host 这三者必须一起配齐,缺一不可。漏掉任意一个,DNS 就还是那个看不见的瓶颈。


















