直接用AsyncHTTPClient下载GridFS文件会卡住,因GridFS无HTTP接口且open_download_stream()返回的GridOut.read()是同步阻塞调用;应通过ThreadPoolExecutor异步执行read()以避免阻塞事件循环。

为什么直接用 tornado.httpclient.AsyncHTTPClient 下载 GridFS 文件会卡住?
因为 GridFS 本身没有 HTTP 接口,它只是 MongoDB 的文件存储抽象层;你不能像请求 http://example.com/file.zip 那样直接异步 GET 它。常见错误是误以为启动一个 Web handler 后调用 gridfs.GridFSBucket().open_download_stream() 就自动异步了——其实这个方法返回的是同步的 GridOut 对象,其 read() 是阻塞的,会拖垮整个 Tornado event loop。
如何让 GridFS 读取不阻塞 Tornado 事件循环?
核心思路:把阻塞的 I/O 操作(如 gridout.read())挪到线程池里执行,再用 await self.executor.submit(...) 包装。Tornado 的 IOLoop 默认自带 concurrent.futures.ThreadPoolExecutor,无需额外启线程管理。
-
self.executor必须在 Application 初始化时显式传入,例如:Application(handlers, executor=ThreadPoolExecutor(max_workers=4)) - 不要在 handler 中新建
ThreadPoolExecutor,否则无法被 Tornado 正确关闭 - 读取 chunk 时建议固定大小(如
1024 * 64),避免一次性read()整个大文件导致内存暴涨
async def get(self, file_id):
try:
gridout = await self.executor.submit(
self.gridfs_bucket.open_download_stream,
ObjectId(file_id)
)
self.set_header("Content-Type", gridout.content_type or "application/octet-stream")
self.set_header("Content-Disposition", f'attachment; filename="{gridout.filename}"')
<pre class="brush:php;toolbar:false;"> while True:
chunk = await self.executor.submit(gridout.read, 1024 * 64)
if not chunk:
break
self.write(chunk)
await self.flush() # 确保分块发送,不缓存整段
except NoFile:
raise HTTPError(404, "File not found")
except Exception as e:
raise HTTPError(500, f"Read error: {e}")
为什么不能用 motor 替代线程池?
motor 是 MongoDB 官方异步驱动,支持真正的 async/await,但它对 GridFS 的封装仍不完整:截至 motor==3.5.0,MotorGridFSBucket 的 open_download_stream() 返回的是 MotorGridOut,其 read() 方法**不是协程**,仍是同步阻塞调用。所以即便用了 motor,仍需线程池兜底。
立即学习“Python免费学习笔记(深入)”;
- 若强行
await gridout.read(),会报TypeError: object bytes can't be used in 'await' expression - 目前唯一真正 async 的路径是用
motor+await gridout.read_chunk(),但该方法只暴露内部 chunk 编号,不推荐业务代码依赖 - 结论:现阶段生产环境更稳的方式仍是
pymongo + ThreadPoolExecutor
响应大文件时容易忽略的 Tornado 配置项
Tornado 默认对单次响应体大小和超时有严格限制,下载几百 MB 文件时极易触发中断或 599 错误。
- 设置
self.set_status(200)前,先调用self.set_header("Content-Length", str(gridout.length))(如果已知长度),避免 Tornado 自动启用 chunked encoding 导致客户端解析异常 - 在
Application初始化中增加:settings={"max_buffer_size": 1024 * 1024 * 1024}(默认仅 100MB) - 为避免连接被 Nginx 或负载均衡器断开,需延长
request_timeout和idle_connection_timeout,例如:HTTPServer(app, idle_connection_timeout=3600)
实际部署时,如果文件普遍大于 1GB,建议改用预签名 URL + Nginx X-Accel-Redirect,把 GridFS 读取完全移出 Python 进程。


















