GridFS 设置 readPreference: "secondary" 仅作用于 fs.files 元数据查询,openDownloadStream() 拉取 fs.chunks 时驱动默认仍走 primary,因从节点缺少 {files_id: 1, n: 1} 索引导致全表扫描、超时降级或 ChunkNotFound。

GridFS 无法通过简单设置 readPreference: "secondary" 实现真正读取从节点 —— 元数据查得到,chunk 流读不到,是常态,不是配置错误。
为什么 readPreference: "secondary" 对 GridFS 大部分无效
GridFS 是两阶段操作:先查 fs.files(元数据),再按 files_id 和 n 查 fs.chunks(数据块)。官方驱动(Node.js/Python/Java)只把 readPreference 应用到第一阶段;第二阶段 openDownloadStream() 内部硬编码走 primary,原因很实际:
-
fs.chunks在从节点默认没有{ files_id: 1, n: 1 }复合索引 → 查询全表扫描,机械盘或高延迟链路下直接超时 - 驱动检测到从节点 chunk 查询失败或响应慢 → 自动 fallback 到 primary,多一次网络往返
- 主从同步延迟 > 100ms 时,
fs.files已存在但对应fs.chunks尚未复制完成 → 流打开成功,但后续读 chunk 返回空或ChunkNotFound
手动创建索引是强制 secondary 读的前提
不建索引,谈读分离就是纸上谈兵。必须在每个从节点上显式执行:
db.fs.chunks.createIndex({ files_id: 1, n: 1 }, { readPreference: "primary" })
注意:
- 该命令必须在
primary上执行(readPreference: "primary"是为确保索引创建成功),但索引会自动同步到所有副本集成员 - 仅对新写入的文件生效?错。索引建好后,所有已有 chunk 文档立即可被高效查询
- 验证是否生效:
mongosh连从节点,执行db.fs.chunks.explain("executionStats").find({ files_id: ObjectId("..."), n: 0 }),看executionStats.nReturned是否 > 0 且executionTimeMillis
绕过驱动限制:手动拆解两阶段流程
别依赖 bucket.openDownloadStream() 的黑盒逻辑。改成显式控制:
- 第一步:用
bucket.find()或bucket.findOne()显式带{ readPreference: "secondary" }查元数据 - 第二步:自己构造
fs.chunks查询,手动指定readPreference,例如 PyMongo 中:
chunks_coll = db.get_collection("fs.chunks")
cursor = chunks_coll.find(
{"files_id": file_doc["_id"], "n": {"$gte": 0}},
sort=[("n", 1)],
read_preference=ReadPreference.SECONDARY
)
- 必须加
maxTimeMS: 2000防卡死,因为从节点 IO 能力通常弱于主节点 - HTTP Range 请求(视频拖拽)要特别小心:元数据和 chunk 必须来自同一节点,否则
length字段与实际 chunk 总数不一致,服务端可能放弃分片响应,返回 200 + 全量 body
最容易被忽略的点:Mongoose 和连接字符串全局设置基本没用
MongoClient 构造时 URI 里写 ?readPreference=secondary,或 Mongoose 全局 readPreference: "secondary",对 GridFS 操作几乎零影响。原因很底层:
- GridFS Bucket 构造时若传了
readPreference,只固化到元数据操作;chunk 读取路径完全绕过该配置 - 驱动内部对
fs.chunks查询做了独立连接池和路由决策,不继承 bucket 级或 client 级的readPreference - 真正起效的只有:索引就位 + 手动拆 query + 显式传
read_preference参数到find()调用











