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 Probemongosh --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 适用

  1. 统一基础设施——团队已有 K8s 集群,用 StatefulSet 统一管理。
  2. 中小型规模——3-5 节点复制集 + 少量分片。
  3. 开发/测试环境——快速拉起、销毁、重建。
  4. 自动化运维——Operator 提供自动备份、升级、扩容。

不适用场景

  1. 超大规模集群(百 TB 级)——裸机部署能更精细调优内核参数和磁盘。
  2. 极致的延迟要求(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 思考题

  1. 在 K8s 上部署分片集群(mongos + config server + 多个 shard),每个组件应该用什么 K8s 工作负载?(提示:mongos 是无状态的,config server 和 shard 是复制集)
  2. 如果在 K8s StatefulSet 中设置了 podManagementPolicy: OrderedReady,而 PVC 的 storageClassName 对应的后端正处于维护不可用状态——mongo-0 的 PVC 无法绑定,mongo-1 和 mongo-2 会启动吗?为什么?

(答案将在第 31 章末尾揭晓)

上一章思考题答案

  1. 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。

  2. 恢复后集合文档数为 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 实战修炼与源码剖析

posted on 2026-08-14 17:44  一天不进步,就是退步  阅读(5)  评论(0)    收藏  举报