因为默认行为是把整个响应体读进内存,真正安全做法是用resp.content逐块读取并异步写入文件。

为什么直接用 aiohttp.ClientSession.get() 会内存爆掉?
因为默认行为是把整个响应体读进内存——哪怕你只调用 resp.read() 或 await resp.text(),aiohttp 也会等全部数据到达才返回。下载几个 GB 的文件时,Python 进程可能瞬间吃光几 GB 内存,甚至触发 OOM killer。
真正安全的做法是边收边写:用 resp.content(它是个 aiohttp.StreamReader)逐块读取,同时写入文件对象(必须是异步文件对象,或用线程池绕过阻塞)。
-
resp.content是流式接口,支持read(n)、readany()、readchunk()等方法 - 不能用内置
open()直接写,那是同步 IO,会阻塞事件循环 - 推荐用
aiofiles库做异步文件写入;若不想引入新依赖,可用loop.run_in_executor调用os.write或file.write
用 aiofiles 实现零拷贝流式写入
aiofiles 封装了线程池 + 文件系统调用,API 接近内置 open,但所有操作都 awaitable。关键是要以 "wb" 模式打开,并在循环中用 await f.write(chunk)。
注意 chunk 大小别设太小(如 1KB),否则 syscall 过多影响吞吐;也别太大(如 100MB),失去流控意义。一般 64KB–1MB 较稳妥。
立即学习“Python免费学习笔记(深入)”;
import aiohttp import aiofiles <p>async def download_file(url: str, filepath: str): async with aiohttp.ClientSession() as session: async with session.get(url) as resp: resp.raise_for_status() async with aiofiles.open(filepath, "wb") as f: async for chunk in resp.content.iter_chunked(512 * 1024): # 512KB/chunk await f.write(chunk)
如何处理重定向、超时和断点续传?
aiohttp 默认跟随重定向,但如果你需要感知跳转过程(比如记录最终 URL),得关掉 allow_redirects=False 并手动处理 302 响应头里的 Location。
超时必须显式设置——ClientTimeout 控制连接、读取、总耗时;流式下载要特别加大 read 超时,否则大文件慢速网络下容易中断。
断点续传靠 Range 请求头实现,但前提是服务端支持 Accept-Ranges: bytes。你需要先 HEAD 获取 Content-Length,再检查本地文件大小,算出 Range: bytes={offset}-,并用 resp.headers.get("Accept-Ranges") == "bytes" 确认服务端能力。
- HEAD 请求用
session.head(url),比 GET 更轻量 - 写入前需用
os.stat(filepath).st_size获取已下载字节数(注意异常捕获) - 如果服务端不支持 Range,
resp.status会是200而非206,此时应清空重下
为什么下载速度慢?常见瓶颈在哪?
不是代码写错了,而是底层被卡住了:aiohttp 默认只开 100 个 TCP 连接(TCPConnector(limit=100)),并发下载多个大文件时会排队;DNS 解析默认用同步 getaddrinfo,高并发下成瓶颈;还有 SSL 握手开销、磁盘 I/O 吞吐不足等。
提速关键配置:
- 增大连接池:
TCPConnector(limit=300, limit_per_host=30) - 启用异步 DNS:
resolver=aiohttp.AsyncResolver() - 禁用 SSL 验证(仅测试环境):
connector = TCPConnector(ssl=False) - 写入时用
os.O_DIRECT(Linux)或预分配文件(os.posix_fallocate)减少碎片
真实场景中,网络带宽和磁盘顺序写速度才是上限,异步本身不提速,只是避免单任务阻塞其他协程——这点常被误解。


















