真正只读的聚合必须同时满足:客户端显式设置readpreference为secondarypreferred或secondary、聚合管道不含$out/$merge、报表节点配置hidden+priority:0+slavedelay且连接串启用directconnection=true、secondary节点已创建必要索引。

只读聚合必须走 secondary 节点,但默认不生效
副本集 secondary 节点默认禁止写入,但客户端不指定读偏好时,readPreference 默认是 primary,所有请求(包括聚合)都会打到主节点——你写的“只读”根本没落地。这不是节点配置问题,是驱动路由逻辑问题。
- Java 驱动必须显式设置
readPreference=secondaryPreferred或secondary,且不能依赖连接串里没写的默认值 - Node.js 的
MongoClient初始化时要传{ readPreference: 'secondaryPreferred' },仅靠 URI 参数readPreference=secondary在某些版本下可能被忽略 - PyMongo 同理:
MongoClient(..., read_preference=ReadPreference.SECONDARY_PREFERRED),别只改 URI - 如果用了 Spring Data MongoDB,需在
mongoTemplate或@Document级别覆盖读偏好,XML 配置里<mongo-client></mongo-client>不支持直接设readPreference,得靠 Java Config 或@ReadPreference注解
聚合操作本身不改变数据,但某些阶段会隐式触发写入
$out 和 $merge 是聚合管道中唯二能写库的阶段,它们无视节点只读状态,只要权限够、目标库可写,就会执行——哪怕你在 secondary 上跑,它也会转发给 primary 执行。这不是 bug,是设计使然。
- 用
$out时,驱动自动将请求路由至 primary,secondary 节点只是中转;日志里能看到command aggregate failed+not master报错,但实际写入已在主节点发生 -
$merge同样绕过只读限制,且支持写入任意集群节点(包括 hidden 节点),只要目标集合存在且用户有权限 - 真正安全的只读聚合,只能用
$group、$match、$sort、$limit等纯计算阶段;检查 pipeline 是否含$out或$merge,比检查节点配置更关键
报表专用节点必须 hidden+priority:0+slaveDelay,且客户端禁用 replica set discovery
光设 readPreference=secondary 不够:如果报表节点是 hidden: true,标准驱动会把它从可用 secondary 列表里剔除,导致请求永远落不到它头上。
- 添加节点时必须用完整配置:
rs.add({ host: "report-node:27017", priority: 0, hidden: true, slaveDelay: 3600 }) - 客户端连接串必须带
directConnection=true,否则驱动会拉取 replica set 配置,发现该节点hidden: true就跳过 - URI 示例:
mongodb://report-node:27017/db?directConnection=true&readPreference=primary(注意:这里用primary是因为只连单节点,readPreference实际无效,但某些驱动要求非空) - Java/Python/Node.js 都要确认驱动版本 ≥ 4.7(旧版
directConnection支持不全),否则 fallback 到 replica set 模式后仍可能误选节点
聚合性能差?先看索引是否建在 secondary 上
secondary 节点的数据同步延迟(slaveDelay)和 oplog 回放压力会影响查询响应,但更常见的瓶颈是索引缺失——secondary 不会自动继承 primary 的索引创建行为,尤其是后台建索引(background: true)在 secondary 上可能卡住或失败。
- 在 primary 上执行
db.collection.createIndex({a: 1, b: -1}, {background: true})后,secondary 会尝试回放该命令;但若 secondary 正忙于应用大量 oplog,索引构建可能停滞,db.currentOp()里能看到"secs_running": 0却状态为"inDurableState" - 验证索引是否存在:
db.collection.getIndexes()必须在 secondary 节点上单独执行,不能只查 primary - 聚合里用到的字段(如
$group的_id、$sort字段、$match条件)都应有对应索引;explain("executionStats")的totalDocsExamined接近nReturned说明索引有效
$out 写得再隐蔽,权限一开照样落盘。这些点不手动掐死,报表服务迟早把只读节点跑出写负载。











