大数组是 MongoDB 隐蔽性能杀手,导致复制抖动、oplog 膨胀、WiredTiger 压缩压力剧增;应使用 $slice+$sort 自动截断或拆分为独立集合。

大数组是 MongoDB 中最隐蔽的性能杀手之一——它不报错、不告警,但会在复制、索引更新、文档序列化和 WiredTiger 压缩阶段持续拖慢吞吐,尤其在高写入场景下引发明显抖动。
为什么大数组会触发复制和写入抖动
当数组字段被修改(哪怕只追加一个元素),MongoDB 默认将整个数组内容重写进 oplog;副本集 Secondary 节点同步时需完整解析并应用该数组,导致:
- oplog 条目体积暴涨(一个 10MB 数组 = 一条 10MB oplog 记录)
- WiredTiger 压缩压力陡增,checkpoint 阶段 CPU 和 I/O 毛刺明显
- 从节点 applyLog 线程阻塞,
replApply延迟升高,甚至触发OplogBufferBlocking等待
这不是“慢”,而是“周期性卡顿”:每分钟几次写入就可能让 P99 延迟跳变 200ms+。
用 $slice 和 $sort 实现自动截断
别在应用层做数组长度判断再 $push —— 竞态下仍可能超限。直接交给 MongoDB 原子处理:
db.logs.updateOne(
{ _id: ObjectId("...") },
{
$push: {
events: {
$each: [{ ts: new Date(), msg: "login" }],
$slice: -100, // 只保留最后 100 条
$sort: { ts: 1 } // 按时间升序,$slice 取尾部即最新
}
}
}
)
关键点:
-
$slice: -N是硬限制,无论原数组多大,最终只存 N 个元素 -
$sort必须与$slice同级,且字段类型需支持排序(如Date、Number) - 不支持对嵌套数组(如
user.activities.items)直接操作,需先解构或改用聚合管道
更彻底的方案:用独立集合替代内嵌数组
当数组元素本身有查询、分页、按条件过滤需求时(比如“查用户最近 10 次失败登录”),嵌入式数组已超出设计边界。此时应拆出为独立集合:
- 新建
user_login_attempts集合,userId建索引 + TTL(如{ expiresAt: 1 }) - 写入走
insertOne,而非$push,避免文档膨胀 - 查询用
find({ userId: ..., status: "failed" }).sort({ createdAt: -1 }).limit(10),完全可控 - WiredTiger 对小文档压缩率更高,
page_write压力下降 40%+(实测于 7.0 版本)
这本质是用一次网络往返换掉长期的复制抖动和内存碎片——只要你的读写比例 > 1:5,就值得。
警惕那些“看起来安全”的数组操作
以下写法看似无害,实则埋雷:
-
$addToSet:去重逻辑需遍历整个数组,数组越大越慢,且仍触发全量写入 oplog -
$pullwith regex:匹配过程无法利用索引,容易在后台线程中卡住数秒 - 在
updateMany中更新含大数组的字段:批量操作放大抖动,可能触发WiredTigerCacheOverflow
真正安全的数组操作只有两种:固定长度的枚举型数组(如 ["read", "write", "admin"]),或用 $slice+$sort 严格控长的时序型数组。其余都该被质疑。



















