asyncio.FileIO不能直接用于HTTP上传,因其底层为阻塞式系统调用;可行方案是用run_in_executor配合同步read或os.pread,且须严格管控分块大小、并发数、线程池与校验逻辑绑定。

asyncio.FileIO 不能直接用于 HTTP 上传
很多人一上来就尝试用 asyncio.FileIO 配合 aiohttp 或 httpx.AsyncClient 读取大文件分块上传,结果发现上传卡死、内存暴涨或抛出 BlockingIOError。根本原因是 asyncio.FileIO 虽然名字带 async,但它底层调用的是阻塞式系统调用(如 read()),在 Windows 和多数 Linux 发行版上不支持真正的异步文件 I/O。
真正可行的路径只有两条:
- 用
loop.run_in_executor()把同步文件读取扔进线程池(简单、兼容性好、适合中小文件) - 用
os.open()+os.pread()(Linux/macOS)配合loop.run_in_executor()实现零拷贝预读(高吞吐场景需手动控制 offset/size)
别碰 asyncio.to_thread() 读大文件——它默认用 concurrent.futures.ThreadPoolExecutor,但没设 max_workers,上传并发高时会拖垮线程池。
分块上传必须自己管理 offset 和 chunk_size
aiohttp 或 httpx 的 data= 参数不会自动切片;你传进去的 bytes 或 AsyncIterator[bytes] 必须由你严格控制每块大小和起始位置。否则服务端收到的分块会错位,合并失败。
立即学习“Python免费学习笔记(深入)”;
关键点:
华为云 Python SDK 查询工具。自动发现已安装 SDK 的服务包、Client类、API 方法、入参详情、类信息(__init__ 参数、属性、类型、描述),支持关键字搜索,快速定位目标 API 使用方法。
- 不要用
file.read(chunk_size)循环——这是同步阻塞调用,会阻塞整个 event loop - 用
await loop.run_in_executor(None, partial(file.read, chunk_size)),且每次调用前确保文件指针已 seek 到正确offset - chunk_size 建议设为 512KB~4MB:太小增加 HTTP 头开销和 round-trip 次数;太大导致单次上传超时或内存占用飙升
- 上传前先发个
POST /upload/init获取 upload_id,后续每块带上X-Upload-ID和X-Chunk-Index,方便服务端校验顺序
并发上传多块时必须限制连接数和 executor workers
看似“用 asyncio 就能无脑开 100 个协程上传”,实际会触发两个瓶颈:
-
aiohttp.ClientSession默认 connector 并发上限是 100,但 TCP 连接、TLS 握手、内核 socket buffer 都会耗尽资源 - 线程池默认无上限,
run_in_executor大量阻塞读会挤占线程,最终协程等不到 executor 空闲线程而挂起
实操建议:
- 创建 session 时显式配置:
aiohttp.TCPConnector(limit=20, limit_per_host=5) - 自定义线程池:
executor = ThreadPoolExecutor(max_workers=8),传给loop.run_in_executor(executor, ...) - 用
asyncio.Semaphore(5)控制同时上传的 chunk 数(不是协程数),避免服务端被压垮
漏掉任一限制,服务在压测时大概率出现 ConnectionResetError、OSError: [Errno 24] Too many open files 或 CPU 100% 但吞吐不增。
MD5/SHA256 校验必须在读取时同步计算
分块上传常要求每块带 X-Chunk-MD5,整文件再校验合并后哈希。如果等所有 chunk 上传完再读一遍文件算总哈希,等于白做分块——I/O 成本翻倍。
正确做法是:
- 每次从文件读 chunk 的同时,用
hashlib.md5(chunk).digest()算当前块哈希(注意:用digest()不是hexdigest(),减少字符串开销) - 用一个全局
hashlib.sha256()实例,在每次update(chunk),最后上传完成时调用hexdigest()得到文件总哈希 - 别在协程里新建 hash 对象——Python 的 hash 实例不是线程安全的,但
update()在同一线程内是安全的;只要保证每个 chunk 的读取和 update 在同一个线程池 worker 中完成即可
最容易被忽略的是:校验逻辑必须和文件读取绑定在同一 executor 调用中,否则 chunk 内容可能被 GC 或复用,导致哈希错乱。


















