MongoDB 主从切换排查实战:从 docker ps 到 jq,一套 SOP 定位死因

一、半夜被告警叫醒之后

凌晨三点 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 止血、再排查根因,不要本末倒置。

⚠️ 只杀查询 / aggregatekillOp 杀写操作不会回滚已写到 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: trueusedDisk: 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 主从切换 线上排障 Prometheus 告警 Docker / cgroup WiredTiger

相关推荐
睡不醒男孩0308231 小时前
第四篇:数据库国产化与信创替代的守护者:基于CLup的异构数据库一站式运维平台构建
运维·数据库·金融·clup·中启乘数
极客先躯1 小时前
高级java每日一道面试题-2026年02月04日-实战篇[Docker]-如何在容器之间共享数据?
java·运维·网络·docker·容器·自动化·高级面试题
Lumistory1 小时前
2026年城市照明工程4大核心痛点及解决方案
大数据·数据库
程序猿小野1 小时前
在阿里云服务器上安装Docker部署后台项目
阿里云·docker·云计算
岳麓丹枫0011 小时前
PG数据库无法接受连接问题分析定位
数据库·postgresql
“码”力全开1 小时前
打破芯片与协议壁垒:基于 Docker+边缘计算 的企业级 AI 视频管理平台架构解析(附 GB28181/RTSP 统一接入与源码交付方案)
人工智能·docker·边缘计算
ai产品老杨1 小时前
【架构深评】基于 Docker 与 边缘计算,如何打通 GB28181/RTSP 与 X86/ARM 异构算力的企业级 AI 视频流网关?(附源码交付)
人工智能·docker·架构
JdSnE27zv2 小时前
SQLite内存数据库
数据库·sql·sqlite