aiohttp.ClientResponse.iter_chunked() 不能直接用于大文件下载是因为它仅控制分块大小,若不立即落盘而累积内存仍会导致OOM;真正防溢出需配合aiofiles流式写入,每次chunk拿到后立刻异步写入磁盘。

为什么 aiohttp.ClientResponse.iter_chunked() 不能直接用在大文件下载
它返回的是异步迭代器,但如果你写成 async for chunk in resp.content.iter_chunked(8192) 然后全塞进内存拼接,和同步读没区别——照样 OOM。真正防溢出的关键不是“用了 async for”,而是“不累积、不缓存、立刻落盘”。
- 常见错误现象:
MemoryError或进程被系统 kill,尤其在 500MB+ 文件、低内存 VPS 上必现 - 根本原因:
iter_chunked()只控制每次 yield 多大块,不控制你后续怎么处理;chunk 拿到后若不做流式写入,就等于白用 - 别指望靠增大 chunk size(比如设成
1024*1024)来“优化”,反而可能因单次分配过大触发 GC 延迟或分配失败
怎么用 aiofiles 配合 iter_chunked 实现真流式落盘
必须让每个 chunk 拿到后立刻写入文件句柄,且整个过程保持异步链路不断开。核心是用 aiofiles.open(..., 'wb') 替代内置 open,否则 await f.write(chunk) 会阻塞事件循环。
- 使用场景:下载 ISO、视频、数据库备份等 >100MB 的二进制资源
- 关键参数:
iter_chunked(n)的n建议保持默认8192或设为65536,兼顾网络吞吐与内存压力 - 必须加
async with aiofiles.open(path, 'wb') as f:,不能用open()+await loop.run_in_executor,后者破坏流式语义且更慢 - 示例片段:
async with session.get(url) as resp:
resp.raise_for_status()
async with aiofiles.open('out.bin', 'wb') as f:
async for chunk in resp.content.iter_chunked(8192):
await f.write(chunk)
iter_any() 和 iter_chunked() 选哪个?
iter_any() 是更底层的接口,它不管 chunk 边界,只按底层 TCP buffer 尽量吐数据;iter_chunked(n) 则强制按 n 字节切分(最后 chunk 可能更小)。对纯下载场景,差别极小,但 iter_any() 更轻量。
- 性能影响:实测在千兆内网下,
iter_any()吞吐高 3–5%,但差异随网络延迟增大而消失 - 兼容性无区别,两者都要求
aiohttp >= 3.8 - 容易踩的坑:别混用——比如用
iter_any()却手动按\r\n解析 HTTP 分块传输编码(Transfer-Encoding: chunked),那是服务器的事,客户端不该碰 - 建议:优先用
iter_chunked(8192),语义清晰、调试友好;压榨极限性能且确定服务端不乱发时再换iter_any()
超时、重试、断点续传这些事,aiohttp 不管
aiohttp 只负责把字节流从 socket 搬出来,不封装业务逻辑。下载中断后想续传,得自己解析 Content-Range、维护已写 offset、构造带 Range header 的新请求。
立即学习“Python免费学习笔记(深入)”;
- 常见错误现象:下载中途报
ServerDisconnectedError或ClientOSError,程序直接退出,没重试也没清理临时文件 - 必须手动处理:
try/except包住整个下载块,捕获aiohttp.ClientError子类,记录当前写入位置,下次从Range: bytes={offset}-继续 - 别依赖
timeout参数全覆盖:DNS 超时、TCP 握手超时、读取超时要分开设,例如aiohttp.ClientTimeout(sock_read=30, connect=10) - 真正复杂的地方在于:多个并发下载共享同一个
session时,连接池复用和超时策略会相互干扰,这时候得调connector的limit和keepalive_timeout










