1. 项目背景
业务场景:本地生活电商的技术栈正在从 Docker Compose 手工运维迁移到 Kubernetes。MongoDB 的部署方式也要从"运维手敲 docker run"升级为 K8s 管理的 StatefulSet。但团队心里没底——MongoDB 是有状态服务,Pod 挂了数据怎么办?PVC 要怎么配置才能保证磁盘性能?复制集的节点之间怎么在 K8s 网络中互相发现?滚动升级时节点退出会不会丢数据?
更头疼的是,运维部门要求所有 K8s 中的服务必须有健康检查和资源限制。但 MongoDB 的探针怎么写——db.runCommand("ping") 探活简单,但"ping 通了但性能极差"怎么检测?CPU 限制设太小 MongoDB 在启动恢复时会被 OOM kill;设太大浪费 K8s Node 的资源。要不要用 MongoDB Community Operator 还是手写 YAML?
痛点:有状态服务在 K8s 上运行,坑点密集——PVC 绑定到特定 AZ 后 Pod 漂移到其他 Node 时无法挂盘;反亲和配置遗漏导致主节点和从节点跑在同一台物理机上——宿主机宕机导致整个复制集不可用;MongoDB 需要直连 Pod IP 进行复制集内部通信,而 K8s Service 的负载均衡反而会破坏复制集的拓扑发现。
2. 项目设计
小胖(盯着 K8s YAML 一脸困惑):大师!我们把 MongoDB 从 Docker Compose 搬到了 K8s,结果重启一个 Pod 后复制集全乱了!为什么 K8s Service 反而坏了事?
大师:因为 MongoDB 的复制集节点之间需要直连 IP 通信——每个节点在自己的 rs.conf() 里记录了其他节点的 host:port。如果这些 host 是 K8s Service 的 ClusterIP,mongod 发现对方不是真正的节点地址,心跳和选主都乱了。
技术映射:MongoDB 的复制集使用节点间直连通信(P2P),不适合通过 K8s Service 的负载均衡进行。正确的做法是使用 Headless Service + StatefulSet——每个 Pod 有稳定的 DNS 名称(如 mongo-0.mongo-svc.default.svc.cluster.local),Pod IP 变化但 DNS 名称不变。
小胖:StatefulSet 和 Deployment 有啥区别?
大师:StatefulSet 三个核心特性让它适合 MongoDB:
| 特性 | StatefulSet | Deployment |
|---|---|---|
| Pod 标识 | 稳定的序号名(mongo-0, mongo-1, mongo-2) | 随机后缀(mongo-7f4b5c-abc) |
| 网络标识 | 每个 Pod 有固定的 DNS 名 | DNS 无稳定性保证 |
| 存储 | 每个 Pod 绑定各自的 PVC(独立磁盘) | 共享 PVC 或临时存储 |
| 启动/终止顺序 | 0→1→2(有序) | 并行 |
技术映射:StatefulSet 的稳定标识是 MongoDB 复制集的基础——节点在复制集配置中以 DNS 名注册,DNS 名永不变化,即使 Pod IP 变了也能正常恢复通信。
小白:那 PVC 和 StorageClass 呢?性能影响大吗?
大师:非常大。MongoDB 的 WiredTiger 存储引擎对磁盘 IOPS 和延迟敏感——如果 PVC 用的是普通的 HDD 存储类,写入延迟 > 10ms,你的复制延迟就会飙升。生产环境必须选 SSD 存储类,且 IOPS 至少 3000(云厂商的 gp3/ESSD PL1 起步)。
小白:那 K8s 探针怎么写?db.runCommand("ping") 够吗?
大师:ping 探针只检查进程是否活着(liveness),不检查是否可用。需要分层:
- Liveness Probe:
mongosh --eval "db.runCommand('ping').ok"——进程是否存活。存活探测失败 → K8s 重启 Pod。 - Readiness Probe:判断节点是否准备好接受流量。对 Primary:
rs.isMaster().ismaster === true;对 Secondary:rs.isMaster().secondary === true且复制延迟 < 10s。未就绪 → 从 Service 中摘除。 - Startup Probe:节点刚启动时可能在 Initial Sync(需要数分钟),此时 liveness 和 readiness 都不应太快判定失败。startup probe 给首次启动更长的宽限期(如 300s)。
技术映射:Liveness 管"要不要重启",Readiness 管"能不能接流量",Startup 管"刚启动给够时间"。
大师(总结):K8s 部署 MongoDB 的核心——StatefulSet + Headless Service(解决复制集节点发现),反亲和配置(防止节点集中在一个宿主机),分层探针(startup/readiness/liveness),制定资源限制(内存 = 工作集 + 1-2GB 系统开销,CPU 不 hard-limit)。
3. 项目实战
3.1 环境准备
需要 K8s 集群(Minikube / Kind / 云 K8s)。本章提供 YAML 配置和部署流程。
# 本地测试用 Kind 创建集群
kind create cluster --name mongodb-lab
3.2 分步实现
步骤一:StorageClass + PVC 模板
目标:定义 SSD 存储类和 StatefulSet 的 PVC 模板。
# storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: mongodb-ssd
provisioner: kubernetes.io/aws-ebs # AWS EBS(云环境)
# 本地测试用:
# provisioner: rancher.io/local-path # k3s / kind
parameters:
type: gp3
iopsPerGB: "100"
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
allowVolumeExpansion: true
# headless-service.yaml
apiVersion: v1
kind: Service
metadata:
name: mongo-svc
labels:
app: mongodb
spec:
clusterIP: None # Headless Service
ports:
- port: 27017
name: mongo
selector:
app: mongodb
步骤二:StatefulSet 定义
目标:部署 3 节点 MongoDB 复制集。
# statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mongo
spec:
serviceName: mongo-svc
replicas: 3
podManagementPolicy: Parallel # 并行启动加快部署
selector:
matchLabels:
app: mongodb
template:
metadata:
labels:
app: mongodb
spec:
# 反亲和:每个 Pod 尽量在不同 Node 上
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: mongodb
topologyKey: kubernetes.io/hostname
containers:
- name: mongod
image: mongo:8.0
command:
- mongod
- --replSet
- myRS
- --bind_ip_all
ports:
- containerPort: 27017
name: mongo
resources:
requests:
memory: "2Gi"
cpu: "1"
limits:
memory: "4Gi"
# 不设 CPU limit 硬限制(避免被 throttle)
env:
- name: MONGO_INITDB_ROOT_USERNAME
valueFrom:
secretKeyRef:
name: mongo-secret
key: username
- name: MONGO_INITDB_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mongo-secret
key: password
volumeMounts:
- name: mongo-data
mountPath: /data/db
startupProbe:
exec:
command:
- mongosh
- --eval
- "db.runCommand('ping').ok"
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 30 # 30 * 10 = 300s 启动宽限期
livenessProbe:
exec:
command:
- mongosh
- --eval
- "db.runCommand('ping').ok"
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
exec:
command:
- mongosh
- --eval
- |
try {
if (rs.status().myState === 1 || rs.status().myState === 2) {
quit(0);
}
} catch (e) {
quit(1);
}
quit(1);
initialDelaySeconds: 60
periodSeconds: 10
volumeClaimTemplates:
- metadata:
name: mongo-data
labels:
app: mongodb
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: mongodb-ssd
resources:
requests:
storage: 50Gi
---
apiVersion: v1
kind: Secret
metadata:
name: mongo-secret
type: Opaque
stringData:
username: admin
password: K8sMongoAdmin2026!
步骤三:初始化复制集
目标:在 Pod 启动后通过 Job 初始化复制集配置。
# init-rs-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: mongo-init-rs
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: init-rs
image: mongo:8.0
command:
- mongosh
- --host
- mongo-0.mongo-svc:27017
- -u
- admin
- -p
- K8sMongoAdmin2026!
- --authenticationDatabase
- admin
- --eval
- |
try {
rs.initiate({
_id: "myRS",
members: [
{ _id: 0, host: "mongo-0.mongo-svc:27017", priority: 2 },
{ _id: 1, host: "mongo-1.mongo-svc:27017", priority: 1 },
{ _id: 2, host: "mongo-2.mongo-svc:27017", priority: 1 }
]
});
print("复制集初始化成功");
} catch(e) {
if (e.message && e.message.includes("already initialized")) {
print("复制集已初始化,跳过");
} else {
throw e;
}
}
restartPolicy: OnFailure
步骤四:滚动升级与故障恢复验证
# 1. 滚动升级——逐个 Pod 更新镜像
kubectl set image statefulset/mongo mongod=mongo:8.0.1
kubectl rollout status statefulset/mongo
# 滚动升级过程中
# - K8s 一次只重启一个 Pod
# - 被重启的 Pod 如果之前是 Primary,会触发 stepDown + 重新选举
# - 重启后 Pod 自动重新加入复制集、追 Oplog
# 2. 模拟故障——删除一个 Pod
kubectl delete pod mongo-1
# 观察:
# - StatefulSet 自动重建 mongo-1
# - 新 Pod 启动后自动重新加入复制集(从其他节点拉取数据)
# - 如果 mongo-0 是 Primary——不受影响
# - 如果 mongo-1 是 Primary——触发选举,mongo-0 或 mongo-2 当选新 Primary
# 查看复制集状态
kubectl exec mongo-0 -- mongosh --eval "rs.status().members.forEach(m => print(m.name + ': ' + m.stateStr))"
# 3. PVC 数据持久性验证
# 删除整个 StatefulSet(保留 PVC)
kubectl delete statefulset mongo --cascade=orphan
# 重新创建 StatefulSet(用相同的名称和 PVC 模板)
kubectl apply -f statefulset.yaml
# 数据通过 PVC 被重新挂载到同名 Pod 上——启动后数据完整
步骤五:MongoDB Operator vs 手工 YAML
目标:对比两种部署方式的优劣。
| 维度 | 手工 YAML (StatefulSet) | MongoDB Community Operator |
|---|---|---|
| 初始配置 | 需要手写 YAML | OpsManager CR 声明式 |
| 升级 | 手动 kubectl set image |
Operator 自动滚动升级 |
| 备份 | 需自建 CronJob | OpsManager 集成备份 |
| 节点扩容 | 手动改 replicas + reconfig | CR 中改 members |
| TLS | 需自管理证书 | Operator 自动签发 |
| 学习成本 | 低(标准 K8s 知识) | 中(需学 CR 规范) |
| 灵活性 | 高 | 中(受限于 Operator 的支持范围) |
推荐:小规模(< 10 节点)用手工 YAML 足够灵活稳定;大规模生产集群用 Operator 来处理证书轮转、自动扩容、备份恢复等复杂运维。
步骤六:资源限制与内核调优
# 在 StatefulSet 的 pod spec 中添加:
# 1. 内核参数调优
initContainers:
- name: sysctl-tuning
image: busybox:1.36
securityContext:
privileged: true
command:
- sh
- -c
- |
sysctl -w vm.swappiness=1
sysctl -w vm.zone_reclaim_mode=0
# 禁用透明大页(MongoDB 官方强烈建议)
if [ -f /sys/kernel/mm/transparent_hugepage/enabled ]; then
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
fi
volumeMounts:
- name: sys
mountPath: /sys
# 2. NodeSelector 将 MongoDB Pod 调度到 SSD 节点
# nodeSelector:
# disktype: ssd
# 3. 为 MongoDB 设置合理的 --wiredTigerCacheSizeGB
# 在 command 中添加:
# - --wiredTigerCacheSizeGB
# - "2.5" # 4GB 内存容器:留 1-1.5GB 给系统和其他开销
3.3 完整代码清单
| 文件 | 用途 |
|---|---|
mongodb-lab/k8s/storageclass.yaml |
SSD StorageClass |
mongodb-lab/k8s/headless-service.yaml |
Headless Service |
mongodb-lab/k8s/statefulset.yaml |
StatefulSet + PVC 模板 + 探针 |
mongodb-lab/k8s/secret.yaml |
管理员密码 Secret |
mongodb-lab/k8s/init-rs-job.yaml |
复制集初始化 Job |
mongodb-lab/k8s/rolling-upgrade.sh |
滚动升级脚本 |
mongodb-lab/k8s/disaster-recovery.sh |
故障恢复验证脚本 |
3.4 测试验证
# 1. 部署到 K8s
kubectl apply -f mongodb-lab/k8s/
# 2. 等待 Pod 就绪
kubectl wait --for=condition=ready pod -l app=mongodb --timeout=300s
# 3. 验证复制集
kubectl exec mongo-0 -- mongosh --quiet -u admin -p K8sMongoAdmin2026! \
--eval "rs.status().members.forEach(m => print(m.name + ' ' + m.stateStr))"
# 4. 验证 Headless Service DNS
kubectl exec mongo-0 -- nslookup mongo-svc
# 应返回 3 个 Pod IP
# 5. 写入测试
kubectl exec mongo-0 -- mongosh --quiet -u admin -p K8sMongoAdmin2026! \
--eval "
use test_k8s;
db.messages.insertOne({msg:'K8s部署成功', ts: new Date()});
print(db.messages.findOne().msg);
"
# 6. PVC 持久性测试
kubectl delete statefulset mongo --cascade=orphan
kubectl apply -f statefulset.yaml
kubectl exec mongo-0 -- mongosh --quiet -u admin -p K8sMongoAdmin2026! \
--eval "print(db.getSiblingDB('test_k8s').messages.findOne().msg)"
# 期望输出: "K8s部署成功"
# 7. 清理
# kubectl delete -f mongodb-lab/k8s/
4. 项目总结
4.1 K8s 部署 MongoDB 关键决策
| 决策点 | 推荐方案 | 理由 |
|---|---|---|
| 工作负载类型 | StatefulSet | 稳定标识 + 独立 PVC |
| 服务发现 | Headless Service | 每个 Pod 独立 DNS 名 |
| 存储 | SSD StorageClass + PVC 模板 | IOPS 和延迟保障 |
| 调度 | 反亲和 (preferred) | 避免单点故障 |
| 探针 | startup 300s + liveness 30s + readiness 10s | 适应 Initial Sync 的长时间窗口 |
| 资源限制 | CPU 不设硬限, Memory 设 1.5-2x 工作集 | 避免 OOM/cgroup throttle |
| 安全 | Secret 存密码, K8s RBAC | 不硬编码在 YAML 中 |
4.2 适用场景
K8s 部署 MongoDB 适用:
- 统一基础设施——团队已有 K8s 集群,用 StatefulSet 统一管理。
- 中小型规模——3-5 节点复制集 + 少量分片。
- 开发/测试环境——快速拉起、销毁、重建。
- 自动化运维——Operator 提供自动备份、升级、扩容。
不适用场景:
- 超大规模集群(百 TB 级)——裸机部署能更精细调优内核参数和磁盘。
- 极致的延迟要求(P99 < 1ms)——K8s 网络 overlay 和容器化的额外延迟。
4.3 注意事项
| 注意事项 | 说明 |
|---|---|
不要用 recreate 部署策略 |
会一次性删掉所有 Pod 再重建→复制集全部宕机 |
| Headless Service 的 DNS 查询 | StatefulSet Pod 的 DNS 名 = pod-name.service-name.namespace.svc.cluster.local |
PVC 的 reclaimPolicy: Retain |
删除 StatefulSet 时 PVC 不会自动删除,数据保留 |
| 滚动升级时的 stepDown | 升级到 Primary Pod 时会触发 stepDown,确保升级期间写入不中断 |
podManagementPolicy: OrderedReady vs Parallel |
Ordered 循序启动 0→1→2,Parallel 同时启动(快但 0 号节点启动较慢可能选主不稳定) |
4.4 常见踩坑经验
故障案例一:K8s Service 的 ClusterIP 负载均衡破坏复制集
某团队把 MongoDB 的 Headless Service 错配成了 ClusterIP Service——mongod 之间通过 Service IP 通信时,K8s 的 iptables 随机路由到任意一个 Pod,导致心跳请求错位、节点认为其他节点"挂了",反复选举导致复制集抖动。解决:使用 clusterIP: None 的 Headless Service,每个 Pod 独立 DNS 名。
故障案例二:CPU limit 设太死导致 MongoDB 被 throttle
某团队把 CPU limit 设为 2 核(MongoDB 默认启动时用满所有核做恢复),Pod 尝试使用超过 2 核被 K8s cgroup throttle,恢复速度从 2 分钟剧降到 20 分钟——readiness probe 超时,Pod 被不断重启。解决:不设 CPU hard limit,只设 request 预留;或者把 limit 设得足够大(如 8 核)。
故障案例三:PVC 跨 AZ 绑定后 Pod 调度失败
云环境中有 3 个可用区(Zone),PVC 创建时被绑定到 Zone-A 的磁盘上。当 Zone-A 的 Node 宕机,K8s 尝试在 Zone-B 上重新调度 Pod——但 PVC 在 Zone-A 无法挂载到 Zone-B 的 Node 上,Pod 永远 Pending。解决:使用支持跨 AZ 的存储类(如 AWS EFS/Portworx);或在每个 AZ 部署独立的复制集,而非让 K8s 调度器跨 AZ 迁移有状态 Pod。
4.5 思考题
- 在 K8s 上部署分片集群(mongos + config server + 多个 shard),每个组件应该用什么 K8s 工作负载?(提示:mongos 是无状态的,config server 和 shard 是复制集)
- 如果在 K8s StatefulSet 中设置了
podManagementPolicy: OrderedReady,而 PVC 的storageClassName对应的后端正处于维护不可用状态——mongo-0 的 PVC 无法绑定,mongo-1 和 mongo-2 会启动吗?为什么?
(答案将在第 31 章末尾揭晓)
上一章思考题答案:
Oplog 窗口 48 小时 + 每天增量 50GB → 需要的 Oplog 磁盘空间 = 50GB × 2 = 100GB(加上安全余量建议 120-150GB)。带宽 100MB/s 传输 50GB ≈ 512 秒 ≈ 8.5 分钟——超过 5 分钟的 RPO 目标。改进:压缩 Oplog 增量(压缩比约 2-3x,传输量可降到 20GB = 3.4 分钟);或升级带宽到 200MB/s。
恢复后集合文档数为 0 而生产正常——最可能的原因是恢复时
mongorestore带了--drop参数清空了集合,但 BSON 文件的文档数据没有被成功导入。原因可能是:BSON 文件损坏、备份时用了错误的条件查询(--query)、或备份的 BSON 文件路径指向了一个空的 dump 目录。也可能是集合名大小写不匹配(Windows 和 Linux 的文件系统差异)。
延伸阅读与资源
NumPy 从入门到生产落地:全链路实战指南(科学计算/向量化)
Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统
Redis 实战修炼与原理进阶
Python 3实战精进:从脚本到高并发订单引擎
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经
Milvus向量数据库实战修炼:从 0 到 1精通向量检索与生产落地
MongoDB 实战进阶与内核修炼
后端工程师的 AI 转型第一课:Ollama 与私有化大模型实战
10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用
后端工程师转型AI第一课-Ollama 与私有化大模型实战
大型语言模型(LLM) vLLM 高性能推理落地实战
Agent开发之LlamaIndex 实战修炼与源码进阶
大语言模型Transformers 实战修炼与源码剖析

微信公众号: 架构师日常笔记 欢迎关注!
浙公网安备 33010602011771号