一、半夜被告警叫醒之后
凌晨三点 PagerDuty 把你叫醒,标题写着:
🔥 MongoDB 副本集成员状态变更 --- Primary 已切换
睡眼朦胧地连进堡垒机,下一秒你的手开始抖:
- 是 mongod 自己挂了?还是网络分区?
- 是被 OOM-Killer 干掉的?还是手动 stepDown?
- 切换完成了吗?还有没有节点存活?
- 业务现在能写吗?
如果没有一套死板可复制 的排查 SOP,绝大多数人这时候会做一件事------到处乱抓。
💡 什么是 SOP:Standard Operating Procedure(标准作业程序)。简单说就是把"老司机踩坑踩出来的肌肉记忆"写成一份谁来都能照抄的流程------新人按表走也能不漏关键证据,老人按表走能节省脑力少犯错。
今天这篇就把"MongoDB 主从切换"的排查流程整理成 8 步 SOP,并配上 9 条 Prometheus 告警规则,下次再被叫醒,照着抄就行。
📖 延伸阅读 :本文聚焦"排查流程 + 监控落地",事故复盘视角可参考前作 《一次 MongoDB OOM 引发主从切换事故复盘:那口"锅"究竟该谁背?》。两篇互补,前作讲"为什么挂",本文讲"挂了怎么排"。
二、为什么主从切换需要单独的 SOP
主从切换是结果,不是原因。常见的"真正原因"有 5 类:
| 真因 | 典型现象 | 处理紧急度 |
|---|---|---|
| cgroup OOM(容器内) | docker OOMKilled: true,dmesg 有 oom |
P0(必复盘) |
| 宿主机 OOM | dmesg Out of memory: Kill process |
P0(机器扩容) |
| SIGTERM / 主动 stepDown | 日志有 received signal 15 |
P2(运维操作) |
| WiredTiger(WT)内部 FATAL | 日志有 assertion / WiredTigerLAS |
P0(数据风险) |
| 磁盘满 / inode 满 | journal 写不下 + disk full |
P0(先扩容) |
你不能在不知道是哪一类的时候就抓药------同样是 mongod 挂,OOM 要查内存配置,FATAL 要查数据完整性,磁盘满要先 truncate 日志。
SOP 的价值 :用 8 个步骤把这 5 种"死法"的指纹都过一遍,不漏证、不重复。
三、8 步排查 SOP
1、第一步:mongod 进程是否还在?三视角交叉确认
bash
# 容器视角(最快,先看)
docker ps -a | grep -i mongo
docker inspect <container> --format '{{json .State}}' | jq
# 进程视角
ps -ef | grep [m]ongod
pgrep -a mongod
# 端口视角
ss -tnlp | grep 27017
目的:先确定 mongod 是"完全没起来"、"在重启中"、还是"起来了但端口没监听"。
docker inspect 输出里特别注意:
| 字段 | 危险值 | 说明 |
|---|---|---|
OOMKilled |
true |
容器超 cgroup 限制被 Linux 干掉 |
ExitCode |
137 |
= 128 + SIGKILL,强信号是 OOM |
Restarting |
true |
容器正在反复重启,可能起不来 |
FinishedAt |
几分钟前的时间 | 最近一次挂掉的时间戳,对账时间线 |
2、第二步:dmesg 查 OOM-Killer 痕迹
bash
# 内核杀过谁
dmesg -T | grep -iE "oom|killed process|mongod" | tail -50
# 容器 vs 宿主机 OOM 区分
dmesg -T | grep -iE "Memory cgroup out of memory|invoked oom-killer"
关键关键词字典:
| 关键词 | 含义 |
|---|---|
invoked oom-killer |
触发了 OOM |
Killed process XXX (mongod) |
mongod 被杀 |
Memory cgroup out of memory |
容器内 OOM(cgroup 限制) |
Out of memory: Kill process |
宿主机级 OOM(更严重) |
⚠️ 坑预警 :机器 uptime > 100 天后 dmesg 时间戳会累计漂移,必须用 mongod.log 时间作为基准校准。我们之前踩过:dmesg 显示 07:04 UTC(= 北京时间 15:04),但 mongod.log 显示进程到 15:10:35 还在打慢查询日志、15:10:39 才重启------以日志为准,OOM 实际发生在 15:10:35-39 之间。
3、第三步:mongod.log 最后 200 行
先找日志路径:
javascript
// 已能连上 mongo
db.adminCommand({ getCmdLineOpts: 1 }).parsed.systemLog.path
或从配置文件 mongod.conf 读:
yaml
systemLog:
path: /data/db/mongod.log
然后看挂之前最后 200 行:
bash
tail -200 /data/db/mongod.log | jq -c '.'
OOM/重启场景的"剧本"(按时间顺序,能匹配上就是 OOM 死法):
| 阶段 | 日志特征 |
|---|---|
| OOM 前夕 | Slow query 持续涌出 + 连接数往上跳 |
| OOM 临界点 | 大量 Interrupted operation as its client disconnected |
| OOM 瞬间 | Connection ended ... connectionCount: N 快速递减 |
| 进程被杀 | mongod.log 戛然而止(SIGKILL 没有优雅 shutdown) |
| 容器拉起 | MongoDB starting pid=1, port=27017, dbPath=...(pid=1 = 容器内进程,说明刚重启) |
| 副本集变更 | transition to PRIMARY from SECONDARY / Member ... is now in state PRIMARY |
其它常见信号(用来排除非 OOM 死法):
| 关键词 | 含义 |
|---|---|
received signal 15 |
SIGTERM,被外部 stop(正常停 / 主动重启) |
received signal 9 |
SIGKILL(可能 OOM 或人为强杀) |
WiredTigerLAS 异常 |
WT 存储层问题,数据风险 |
assertion / Fatal |
内部严重错误 |
4、第四步:副本集状态
bash
# 进容器
docker exec -it mongo-container mongosh \
-u admin -p 'xxx' --authenticationDatabase admin
javascript
rs.isMaster() // 当前节点身份
db.hello() // 6.0+ 推荐写法,等同 isMaster
rs.status() // 各 member 的 stateStr / health / lastHeartbeat
rs.printReplicationInfo() // oplog 时间窗口
rs.printSecondaryReplicationInfo() // 从节点同步延迟
怎么看:
health: 1, stateStr: PRIMARY/SECONDARY→ 节点正常health: 0→ 节点不可达electionDate→ 上次选主时间,用来精确锁定主从切换发生时刻optimeDate各节点差距大 → 同步跟不上,后续可能再次切换stateStr: RECOVERING→ 节点正在追日志,服务能力受限
5、第五步:容器内存 vs mongod 自报内存
这是 SOP 里最容易被跳过、但价值最高的一步。
bash
# 容器实时资源(重点看 MEM USAGE / LIMIT)
docker stats mongo-container --no-stream
# cgroup 限制 + 当前用量
docker inspect mongo-container | grep -i memory
cat /sys/fs/cgroup/memory/docker/<id>/memory.limit_in_bytes
cat /sys/fs/cgroup/memory/docker/<id>/memory.usage_in_bytes
javascript
// mongo 自报内存(MB)
db.serverStatus().mem
// { resident: 4523, virtual: 7234, mapped: ... }
// WiredTiger cache 使用 / 配置上限(字节)
db.serverStatus().wiredTiger.cache["bytes currently in the cache"]
db.serverStatus().wiredTiger.cache["maximum bytes configured"]
目的 :把 mongod 的 resident(RSS)跟容器 MEM LIMIT 对比,RSS 离 cgroup limit 越近越危险。
⚠️ 容器化部署最大的坑 :MongoDB 看不到 cgroup 限制 (直到 6.0 都不能),默认按宿主机物理内存算 WT cache 上限:
(RAM - 1GB) / 2。例如宿主机 64G 内存、容器限制 6G:mongod 默认会把 WT cache 设成
(64-1)/2 = 31.5GB,远超容器上限 → 启动后 cache 一涨就被 cgroup 干掉。务必显式配置
storage.wiredTiger.engineConfig.cacheSizeGB,且建议 ≤ 容器内存的 55%,给临时查询内存留 buffer。
6、第六步:当前正在跑的 op + 止血
javascript
// 全量正在跑的 op(包括 idle cursor)
db.currentOp()
// 只看活跃且跑了 > 1s 的
db.currentOp({
"active": true,
"secs_running": { "$gt": 1 }
})
// 只看慢的 find / aggregate
db.currentOp({
"active": true,
"op": { "$in": ["query", "command"] },
"secs_running": { "$gt": 3 }
})
// 杀掉指定 op(紧急止血)
db.killOp(opid)
目的 :故障未完全恢复时,可能有"罪魁查询"还在跑(比如某个全表扫的 aggregate 把内存又顶起来了)。先 killOp 止血、再排查根因,不要本末倒置。
⚠️ 只杀查询 / aggregate :
killOp杀写操作不会回滚已写到 oplog 的部分,可能造成主从不一致。止血前先看清 op 类型,确认是find/aggregate这类只读操作再杀,别误伤insert/update/findAndModify等写操作(详见第六节坑 5)。
7、第七步:连接数与游标
javascript
db.serverStatus().connections
// { current: 245, available: 51955, totalCreated: 9320776 }
db.serverStatus().metrics.cursor.open
// { total: 12, noTimeout: 0, pinned: 0 }
看什么:
current异常高(接近available)→ 客户端连接泄漏 / 重连风暴cursor.open.total异常高(> 1000)→ cursor 没 close,长查询占内存cursor.noTimeout> 0 → 用了noCursorTimeout,慎用,易泄漏
8、第八步:复盘------从历史日志揪元凶
到这一步 mongod 应该已经活过来了,但没找到元凶就是定时炸弹。
bash
# 按时间窗口切日志
cat /data/db/mongod.log | jq -c '
select(.t."$date" >= "2026-05-22T15:05:00" and .t."$date" <= "2026-05-22T15:11:00")
' > /tmp/mongod-window.log
# 抽慢查询按 docsExamined 排序 top20
cat /tmp/mongod-window.log | jq -r '
select(.msg == "Slow query") |
"\(.attr.durationMillis // 0) \(.attr.docsExamined // 0) \(.attr.nreturned // 0) " +
(.attr.ns // "?") + " " +
(.attr.planSummary // "?") + " " +
(.attr.command | tostring | .[0:200])
' | sort -k2 -nr | head -20
特别关注的"罪魁"特征:
| 特征 | 怎么发现 |
|---|---|
| 正则模糊匹配 | $regex / $regularExpression 出现 → 一般走不到索引、扫大量文档 |
| filter ratio 过高 | docsExamined / nreturned > 1000 → 索引选错或缺索引 |
| 走错索引 | planSummary 不是预期字段 → .explain("executionStats") 复现 |
| 内存排序 | hasSortStage: true 且 usedDisk: false → sort 在内存里做 |
| aggregate 中算计算列再过滤 | $project 算字段后 $match → 必须 load 整文档 |
$in 过大 |
$in 数组 > 1000 → 拆批处理 |
getMore 长时间持有 cursor |
单连接 getMore 持续几十秒 → 业务侧没及时关 cursor |
一个真实案例 :某次主从切换,按上面套路 jq 排序后发现 OOM 前夕有 4 条 $regex 查询,扫描 docsExamined 都接近 4.5 万、nreturned 是个位数------这就是典型的"运营后台用正则模糊搜 → 全集合 load 进内存做过滤"反模式。
四、SOP 速查表
把上面 8 步压成一张表,告警响起来就照着抄:
| # | 阶段 | 命令 | 目的 |
|---|---|---|---|
| 1 | 看进程 | docker ps -a / pgrep mongod / ss -tnlp |
mongod 是否在跑 |
| 2 | 看死因 | `dmesg -T | grep -iE "oom |
| 3 | 看 mongod 日志 | `tail -200 mongod.log | jq` |
| 4 | 副本集 | rs.status() / rs.isMaster() |
主从切换 / health |
| 5 | 内存 | docker stats + db.serverStatus().mem |
RSS vs cgroup limit |
| 6 | 当前 op | db.currentOp() / db.killOp(id) |
揪罪魁 + 止血 |
| 7 | 连接 / cursor | db.serverStatus().connections / .metrics.cursor |
连接泄漏 / cursor 泄漏 |
| 8 | 复盘 | mongod.log + jq 排序 |
历史时段慢查询元凶 |
五、把 SOP 沉淀成监控:9 条 Prometheus 告警规则
排查多了你会发现:每次都按 SOP 走一遍太累,要把"踩过的坑"沉淀成监控规则------让告警自动告诉你"现在已经踩到哪一步了"。
下面这 9 条规则配 mongodb_exporter 使用,覆盖了上面 SOP 里所有的危险信号:
5.1 WiredTiger cache 使用率过高(对应 SOP 第 5 步)
yaml
- alert: MongoDB_WiredTiger_Cache使用率过高
expr: |
mongodb_mongod_wiredtiger_cache_bytes{type="total"}
/ on(instance) mongodb_mongod_wiredtiger_cache_bytes_max
> 0.90
for: 3m
labels:
severity: warning
annotations:
summary: "MongoDB WiredTiger cache 使用率 > 90%"
description: "{{ $labels.instance }} WT cache 使用 {{ printf \"%.1f\" (mulf $value 100.0) }}%,eviction 压力大,瞬时大查询可能顶爆 cgroup"
5.2 WT eviction 跑满(cache 满 + 持续 evict)
yaml
- alert: MongoDB_WT_Eviction压力
expr: |
rate(mongodb_mongod_wiredtiger_cache_evicted_total[1m]) > 0
and on(instance)
(mongodb_mongod_wiredtiger_cache_bytes{type="total"}
/ on(instance) mongodb_mongod_wiredtiger_cache_bytes_max) > 0.85
for: 2m
labels:
severity: warning
annotations:
summary: "WT cache 接近满 + 持续 eviction"
description: "回收速度跟不上输入,考虑下调 cacheSizeGB 或扩容容器内存"
5.3 mongod RSS 接近容器内存上限 ⚠️ 关键
yaml
- alert: MongoDB内存接近容器上限
expr: mongodb_memory{type="resident"} > 4800
for: 2m
labels:
severity: critical
annotations:
summary: "MongoDB RSS 接近容器内存上限"
description: "{{ $labels.instance }} RSS = {{ $value }} MB,距 cgroup limit(6G)仅约 1.3G 且仍在上涨,紧急介入"
阈值 4800 是按容器限制 6G 算的。不同实例容器大小不同要单独调 ,建议未来用
mongodb_memory / container_spec_memory_limit_bytes做相对比例。
5.4 查询效率低(docsExamined / nreturned 比率过高)
这条对应 SOP 第 8 步的"罪魁特征"------直接监控"正则模糊匹配 / 走错索引":
yaml
- alert: MongoDB查询效率低_扫描比过高
expr: |
rate(mongodb_mongod_metrics_query_executor_total{state="scanned_objects"}[1m])
/
(rate(mongodb_mongod_metrics_document_total{state="returned"}[1m]) > 0)
> 1000
for: 2m
labels:
severity: warning
annotations:
summary: "MongoDB 扫描 / 返回比 > 1000"
description: "平均扫 {{ printf \"%.0f\" $value }} 个文档才返回 1 条,疑似正则查询 / 缺索引"
5.5 内存排序频率过高
yaml
- alert: MongoDB内存排序过多
expr: rate(mongodb_mongod_metrics_operation_total{state="scan_and_order"}[5m]) > 5
for: 5m
labels:
severity: warning
annotations:
summary: "MongoDB 内存排序速率过高"
description: "scan_and_order 速率 {{ printf \"%.1f\" $value }}/s,sort 字段未覆盖索引"
5.6 慢查询扫描量过大(1 分钟窗口抓突发)
yaml
- alert: MongoDB慢查询扫描量过大
expr: increase(mongodb_mongod_metrics_query_executor_total{state="scanned_objects"}[1m]) > 100000
for: 1m
labels:
severity: warning
annotations:
summary: "MongoDB 1 分钟内扫描文档数过多"
description: "过去 1 分钟扫描 {{ $value }} 个文档"
5.7 副本集状态变更(主从切换报警)
yaml
- alert: MongoDB副本集状态变更
expr: changes(mongodb_replset_member_state[5m]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "MongoDB 副本集成员状态变更"
description: "{{ $labels.instance }} member {{ $labels.member_idx }} 状态在 5 分钟内发生变更"
5.8 连接数突增(重连风暴)
yaml
- alert: MongoDB连接数突增
expr: increase(mongodb_connections{state="current"}[1m]) > 100
for: 0m
labels:
severity: warning
annotations:
summary: "MongoDB 1 分钟内连接数增长 > 100"
description: "短时间连接数暴涨 {{ $value }},疑似客户端重连风暴"
5.9 操作被中断激增(OOM 前夕特征)
yaml
- alert: MongoDB操作被中断激增
expr: rate(mongodb_mongod_metrics_operation_total{state="killed_due_to_client_disconnect"}[1m]) > 5
for: 1m
labels:
severity: warning
annotations:
summary: "客户端断开导致操作中断速率过高"
description: "每秒 > 5 次操作被中断,可能是服务即将不可用的征兆"
⚠️ 上线前必做 :5.4 / 5.5 / 5.9 依赖
mongodb_exporter暴露的具体指标名,不同版本指标名可能叫mongodb_ss_metrics_*。部署前先curl <exporter>/metrics | grep确认指标存在。
六、踩过的几个坑(给后人避雷)
1. dmesg 时间不能直接用
机器 uptime 久(百天以上)后 dmesg 时间戳会漂移,写复盘时务必用 mongod.log 时间为基准。
2. MongoDB 6.0 之前看不到 cgroup limit
docker run -m 6g 设的容器限制 mongo 自己不知道,照样按宿主机物理内存算 WT cache。必须显式配 cacheSizeGB。
3. cacheSizeGB 不要超过容器的 55%
留 45% 给:索引内存、临时 sort 内存、aggregate working set、connection buffer、journal、tcmalloc 碎片。
举例:容器 6G → cacheSizeGB 建议 3.0~3.3G。
4. 主从切换报警阈值不要拉太高
changes(mongodb_replset_member_state[5m]) > 0 即报 critical,因为一次切换的代价(写入失败、客户端重连)已经足够痛。
5. 别用 db.killOp 杀写操作
killOp 杀写入操作不会回滚已经写到 oplog 的部分,可能造成主从不一致。只对查询 / aggregate 用。
七、写在最后
主从切换告警最让人头疼的不是事故本身,而是"叫醒之后不知道从哪儿下手"。把排查流程 SOP 化、再把 SOP 沉淀成 Prometheus 规则,本质上是把应急响应从依赖个人经验,变成依赖团队基础设施。
下次再被告警叫醒,希望你照着这篇的 8 步走完,10 分钟内能给团队同步出一个准确的死因判断。
延伸阅读 / 一起食用更香:
- 前作:《一次 MongoDB OOM 引发主从切换事故复盘:那口"锅"究竟该谁背?》 --- 复盘视角,讲"为什么挂"
- 同系列:《CPU 占用高排查实战:从 top 到火焰图,一套组合拳搞定》
- 同系列:《Load 占用高排查实战:load 飙到 60,但 CPU 只有 70%,凶手到底藏在哪儿?》
- 同系列:《内存占用高排查实战:从 free 到 MAT,揪出那个吃内存的家伙》
🏷️ 标签 :MongoDB 主从切换 线上排障 Prometheus 告警 Docker / cgroup WiredTiger