Kubernetes 中运行 MongoDB:StatefulSet 与持久化存储配置
全面剖析在 Kubernetes 中运行生产级 MongoDB 集群的方方面面,聚焦于核心的 StatefulSet 和持久化存储配置,并延伸至高可用、安全、备份及运维的完整生命周期管理。
一:引言——为什么是 StatefulSet?
在 Kubernetes 中部署应用,最先想到的可能是 Deployment。但对于 MongoDB 这类有状态应用,Deployment 存在致命缺陷:
- 网络标识不稳定:Deployment 管理的 Pod 名称和 IP 是随机的、可变的。MongoDB 副本集成员需要稳定的网络标识来相互发现和通信。
- 存储无关联性:Deployment 的 Pod 是无状态的,其挂载的 PersistentVolume (PV) 在 Pod 被重新调度时,可能会被挂载到另一个新 Pod 上,但无法保证这个新 Pod 就是原来数据的“主人”。对于数据库,数据与 Pod 必须有严格的、一对一的绑定关系。
- 无序的启停:Deployment 的 Pod 启动、更新、扩缩容是无序的。而 MongoDB 副本集的初始化、主从选举、数据同步等操作对顺序有严格要求。
StatefulSet 正是为解决这些问题而设计的:
- 稳定的网络标识:StatefulSet 为每个 Pod 提供一个唯一的、稳定的名称,格式为 -(例如 mongod-0, mongod-1, mongod-2)。结合 Headless Service,它可以为每个 Pod 提供稳定的 DNS 记录:...svc.cluster.local。
- 稳定的持久化存储:StatefulSet 的每个 Pod 实例都对应一个唯一的 PersistentVolumeClaim (PVC) 模板。当 Pod 被重新调度时,Kubernetes 会绑定到同一个 PV,确保数据不会丢失且永远属于“正确”的 Pod。
- 有序的部署和扩缩:StatefulSet 默认遵循严格的顺序性(OrderedReady):Pod 按索引顺序(0, 1, 2…)创建、终止时则按逆序(2, 1, 0)进行。这对于数据库集群的初始化和管理至关重要。
因此,StatefulSet 是在 Kubernetes 中运行 MongoDB 副本集或分片集群的唯一正确选择。
二:深度剖析 StatefulSet 配置
让我们从一个最基础的 MongoDB 副本集 StatefulSet 清单开始,并逐部分进行深度解析。
2.1 核心元数据 (Metadata)
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mongod
namespace: database
labels:
app: mongodb
replicaSet: rs0
- name 和 namespace:定义了 StatefulSet 及其 Pod 的命名空间和基础名称。
- labels:关键的标识,用于被 Service 和其他资源选择。
2.2 Pod 模板 (Spec.Template)
这是 StatefulSet 最核心的部分,定义了每个 Pod 副本的样貌。
a) Pod 元数据与标签
spec:
serviceName: "mongodb-service" # 关联的 Headless Service 名称,必须提前定义
replicas: 3
selector:
matchLabels:
app: mongodb
replicaSet: rs0
template:
metadata:
labels:
app: mongodb
replicaSet: rs0
role: primary # 这是一个动态标签,初始化后需通过sidecar容器更新
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9216" # 用于 MongoDB Exporter 监控
- serviceName:必须指向一个已存在的 Headless Service。这是提供稳定 DNS 的基础。
- Pod 标签:除了用于选择器匹配的稳定标签(app, replicaSet),我们还可以预留一个动态标签(如 role),用于在副本集初始化后,通过 sidecar 容器动态更新它,以标识 Pod 是 Primary 还是 Secondary。这可用于创建指向 Primary 的 Service。
b) 容器定义 - MongoDB
spec:
containers:
- name: mongod-container
image: mongo:6.0.11
command: ["mongod"]
args:
- "--bind_ip_all"
- "--replSet"
- "rs0"
- "--dbpath"
- "/data/db"
- "--oplogSize"
- "512" # 单位MB,生产环境应更大,或使用--wiredTigerCacheSizeGB控制内存
- "--keyFile"
- "/etc/mongo-secrets/keyfile"
- "--auth"
ports:
- containerPort: 27017
name: mongodb-port
resources:
requests:
memory: "4Gi"
cpu: "2"
limits:
memory: "8Gi"
cpu: "4"
env:
- name: MONGO_INITDB_ROOT_USERNAME
valueFrom:
secretKeyRef:
name: mongodb-secrets
key: root-username
- name: MONGO_INITDB_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mongodb-secrets
key: root-password
- command 和 args:明确指定启动命令和参数。–bind_ip_all 允许监听所有网络接口,在 K8s 网络模型中必须设置。
- –replSet:指定副本集名称,所有成员必须一致。
- –keyFile 和 --auth:启用内部认证。Keyfile 是副本集成员间通信的“密码”,必须所有成员一致。这是生产环境的强制要求。
- resources:必须设置。MongoDB 对内存和 CPU 敏感。不设置 limits 可能导致“邻居吵闹”问题,甚至被 OOMKill。wiredTigerCacheSizeGB 通常应设置为可用内存的 50%-60%。
- env:从 Secret 中获取认证信息,绝不可硬编码在配置文件中。
c) 容器定义 - 探针 (Probes)
livenessProbe:
exec:
command:
- mongosh
- --eval
- "db.adminCommand('ping')"
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
exec:
command:
- mongosh
- --eval
- "db.adminCommand('ping')"
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
- Liveness Probe:判断容器是否活着。失败会重启 Pod。初始延迟要设置得足够长,以保证 MongoDB 有足够时间启动。
- Readiness Probe:判断容器是否准备好接收流量。失败会从 Service 的 Endpoints 中移除该 Pod。这对于避免将请求发送给正在启动、正在做初始同步或已经僵死的 Secondary 节点至关重要。
d) 容器定义 - 卷挂载 (Volume Mounts)
volumeMounts:
- name: mongodb-data
mountPath: /data/db
- name: mongodb-secrets
mountPath: /etc/mongo-secrets
readOnly: true
- name: config
mountPath: /etc/mongod.conf
subPath: mongod.conf
- mongodb-data:这是核心的数据目录,必须持久化。
- mongodb-secrets:用于挂载 Keyfile 和可能的其他敏感信息。必须设置为 readOnly: true。
- config:用于挂载自定义的 MongoDB 配置文件 (mongod.conf)。使用 subPath 可以只挂载 ConfigMap 中的单个文件,而不覆盖整个目录。
2.3 卷声明模板 (VolumeClaimTemplates)
这是 StatefulSet 实现稳定存储的魔法所在。
volumeClaimTemplates:
- metadata:
name: mongodb-data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "ssd-high-iops" # 必须指定一个合适的 StorageClass
resources:
requests:
storage: 100Gi # 根据需求调整
- volumeClaimTemplates:StatefulSet 控制器会为每个 Pod(mongod-0, mongod-1, mongod-2)自动创建一个独立的 PVC,名称格式为 --(例如 mongodb-data-mongod-0)。
- storageClassName:这是关键。你必须指定一个已存在的、合适的 StorageClass。绝不能依赖默认值,因为默认的 StorageClass 可能性能很差(如使用 HDD)。
- accessModes:MongoDB 是单点读写,所以 ReadWriteOnce (RWO) 足够。
- resources.requests.storage:初始存储大小。后续可以通过 kubectl edit pvc 来扩展,但通常不能缩小。
三:持久化存储的深度考量
存储是数据库的根基,在 K8s 中需要格外小心。
3.1 StorageClass:存储的抽象层
StorageClass 定义了存储的“供应商”和“性能套餐”。管理员提前创建,开发者按需引用。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ssd-high-iops
provisioner: ebs.csi.aws.com # 或 pd.csi.storage.gke.io, disk.csi.azure.com, csi.vsphere.vmware.com等
parameters:
type: gp3
iops: "10000"
throughput: "250"
fsType: ext4
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
allowVolumeExpansion: true
- provisioner:指定用于调配 PV 的 CSI 驱动。不同的云厂商和本地存储方案都有其特定的 CSI 驱动。
- parameters:传递给底层存储系统的参数。例如在 AWS GP3 EBS 上,可以独立配置 IOPS 和吞吐量。
- volumeBindingMode: WaitForFirstConsumer:强烈推荐。这意味着 PV 不会在 PVC 创建时立即绑定,而是等到第一个使用它的 Pod 被调度到某个节点上时才绑定。这允许调度器综合考虑存储和计算资源,避免 Pod 因节点资源不足而无法启动。
- reclaimPolicy: Retain:对于数据库,强烈建议设置为 Retain。当 PVC 被删除时,对应的 PV 不会被自动删除,数据得以保留,防止误操作导致数据丢失。后续需要管理员手动清理和回收。
- allowVolumeExpansion: true:允许后续在线扩展存储空间。这是非常有用的功能。
3.2 本地存储 (Local PV) 与高性能场景
对于追求极致性能和高 IOPS 的场景,云盘可能无法满足需求。此时可以使用本地 NVMe SSD。
方式一:使用 local Volume Provisioner
需要预先在节点上准备好磁盘并格式化。通常需要 DaemonSet 和节点标签来管理。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-ssd
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
然后需要手动创建 PV 来对应每个节点上的实际磁盘:
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-node1-mongod-0
spec:
capacity:
storage: 1000Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-ssd
local:
path: /mnt/ssd0 # 节点上磁盘的挂载路径
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- node-1.example.com # 绑定到特定节点
缺点:管理繁琐,需要人工干预,Pod 只能被调度到拥有对应 PV 的节点上。
方式二:使用 LVM Operator 或 OpenEBS LocalPV
这些工具可以自动化本地存储的管理,动态地创建 Local PV,简化了运维。
重要警告:使用本地存储意味着 Pod 与节点强绑定。如果节点宕机,Pod 无法被调度到其他节点,除非有完善的数据同步复制机制(这由 MongoDB 副本集本身保证)。此时需要人工介入,修复节点或替换成员。
四:完整部署流程与初始化
4.1 准备秘钥和配置
a) 创建 Keyfile Secret
Keyfile 是 base64 编码的随机字符串,所有副本集成员必须相同。
openssl rand -base64 756 > keyfile
chmod 400 keyfile
kubectl create secret generic mongodb-keyfile --from-file=keyfile=./keyfile -n database
b) 创建用户密码 Secret
kubectl create secret generic mongodb-secrets -n database \
--from-literal=root-username=admin \
--from-literal=root-password=$(openssl rand -base64 16)
4.2 部署网络服务
a) Headless Service (用于 Pod 发现)
apiVersion: v1
kind: Service
metadata:
name: mongodb-service
namespace: database
labels:
app: mongodb
spec:
clusterIP: None # 这就是 Headless Service 的定义
ports:
- port: 27017
name: mongodb
selector:
app: mongodb
replicaSet: rs0
此 Service 不会分配 ClusterIP,而是直接为每个匹配的 Pod 创建 DNS SRV 记录。
b) Client Service (用于应用连接)
apiVersion: v1
kind: Service
metadata:
name: mongodb
namespace: database
spec:
ports:
- port: 27017
targetPort: mongodb-port
selector:
app: mongodb
replicaSet: rs0
# type: ClusterIP 是默认值
应用可以连接到此 Service 的 ClusterIP,它会对所有 Pod(Primary 和 Secondary)进行负载均衡。注意:这可能导致应用写入被错误地导向 Secondary。 更高级的做法是使用 Readiness Probe 和标签,动态创建一个只指向 Primary 的 Service。
4.3 部署 StatefulSet 并初始化副本集
- 应用 YAML 文件:
kubectl apply -f mongodb-statefulset.yaml -n database
- 等待所有 Pod 进入 Ready 状态:
kubectl get pods -l app=mongodb -n database -w
- 执行副本集初始化:
由于 StatefulSet 的有序性,mongod-0 会最先启动。我们需要在它上面执行初始化命令。
# 连接到 mongod-0 Pod
kubectl exec -it mongod-0 -n database -- mongosh
# 在 mongo shell 中执行初始化
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "mongod-0.mongodb-service.database.svc.cluster.local:27017" },
{ _id: 1, host: "mongod-1.mongodb-service.database.svc.cluster.local:27017" },
{ _id: 2, host: "mongod-2.mongodb-service.database.svc.cluster.local:27017" }
]
})
# 创建根用户(如果未通过环境变量自动创建)
use admin
db.createUser({
user: "admin",
pwd: "从Secret中获取的实际密码",
roles: [ { role: "root", db: "admin" } ]
})
关键:使用稳定的 DNS 名称 (mongod-0.mongodb-service.database.svc.cluster.local) 来配置成员,而不是 Pod IP。
- 验证副本集状态:
kubectl exec -it mongod-0 -n database -- mongosh -u admin -p --authenticationDatabase admin --eval "rs.status()"
五:高级主题与生产实践
5.1 自动化初始化:使用 Init Container 或 Sidecar
上述手动初始化不适合生产。我们可以使用 Init Container 来实现自动化。
在 StatefulSet 的 Pod 模板中添加:
spec:
template:
spec:
initContainers:
- name: init-mongo
image: mongo:6.0.11
command:
- bash
- -c |
# 等待本地的mongod服务启动
until mongosh --eval "db.adminCommand('ping')"; do
echo "Waiting for local mongod..."
sleep 2
done
# 只有 Pod-0 执行初始化
if [[ $(hostname) == "mongod-0" ]]; then
echo "Initializing replica set on mongod-0"
mongosh --eval '
if (rs.status().codeName != "NoReplicationEnabled") {
quit(0);
}
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "mongod-0.mongodb-service.database.svc.cluster.local:27017" },
{ _id: 1, host: "mongod-1.mongodb-service.database.svc.cluster.local:27017" },
{ _id: 2, host: "mongod-2.mongodb-service.database.svc.cluster.local:27017" }
]
});
'
fi
env:
# ... 传递认证信息
这个 Init Container 会在主容器之前运行,mongod-0 会尝试执行初始化,其他节点则会跳过。
更健壮的做法是使用一个专门的 Sidecar 容器(例如一个运行脚本的小容器),持续监控副本集状态并自动处理添加新成员、重新配置等操作。社区有类似 Operator 的工具基于此理念。
5.2 使用 MongoDB Kubernetes Operator
对于生产环境,强烈推荐使用 Operator 来管理 MongoDB。Operator 是一种 Kubernetes 扩展,它将运维知识(如部署、扩展、备份、升级)编码成自定义控制器。
- 社区版:MongoDB Community Kubernetes Operator
- 企业版:MongoDB Enterprise Kubernetes Operator
Operator 的好处: - 全自动化:一键部署副本集、分片集群。
- 声明式配置:通过一个自定义资源(MongoDBDeployment)描述所需状态,Operator 自动实现。
- 内置备份与恢复:集成 Ops Manager 或 Cloud Manager。
- 安全:自动处理 TLS 证书、密钥轮换。
- 升级与扩展:简化运维操作。
使用 Operator 是管理生产级 MongoDB on K8s 的最佳实践。
5.3 备份与恢复策略
绝不能直接使用 kubectl cp 或卷快照作为唯一的备份方式。必须使用 MongoDB 原生的工具(mongodump/mongorestore 或文件系统快照+Oplog)来保证数据一致性。
- 定期快照:如果存储后端支持(如云厂商的磁盘快照),可以对 PV 进行定期快照。前提是:必须确保在快照瞬间,MongoDB 的数据文件处于一致状态。通常需要先冻结写入或停止节点,这很复杂。
- 使用 mongodump:在 Sidecar 容器中定期执行 mongodump -h localhost --oplog --gzip,并将备份文件上传到对象存储(如 S3)。–oplog 选项可以保证点-in-time 恢复。
- 使用 Ops Manager / Cloud Manager:这是 MongoDB 官方的企业级管理平台,提供持续备份、点-in-time 恢复等最完善的功能。它与 Kubernetes Operator 集成良好。
5.4 监控与日志
- 监控:部署 prometheus-mongodb-exporter 作为 Sidecar 容器,暴露 MongoDB 指标。使用 Grafana 展示监控仪表盘,监控操作数、复制延迟、连接数、缓存命中率等关键指标。
- 日志:确保 MongoDB 日志输出到 stdout/stderr。使用 DaemonSet(如 Fluentd、Fluent Bit)或 Sidecar 容器收集日志,并发送到中央日志系统(如 Elasticsearch, Loki)。在 StatefulSet 中,每个 Pod 的日志是独立的,需要聚合查看。
5.5 安全加固
- Network Policies:使用 Kubernetes NetworkPolicy 严格限制对 MongoDB Pod 的访问,只允许应用 Pod 和运维工具访问 27017 端口。
- Pod Security Standards:应用 Pod Security Context 和 SecurityContext,以非 root 用户运行容器,限制权限。
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: "OnRootMismatch"
- TLS:为副本集成员间的通信启用 TLS 加密。这需要创建和管理 X.509 证书,通常可以通过 cert-manager 自动化。
总结
在 Kubernetes 上运行生产级的 MongoDB 是一项复杂的系统工程,绝不仅仅是 kubectl apply 一个 YAML 文件那么简单。
- 核心模式:使用 StatefulSet 配合 持久化的 VolumeClaimTemplates 和 Headless Service。
- 存储关键:精心选择和配置 StorageClass(WaitForFirstConsumer, Retain, AllowVolumeExpansion),对于高性能场景考虑 本地存储。
- 生产必备:务必配置资源限制、探针、基于 Keyfile 的认证和安全的 Secret 管理。
- 进阶实践:采用 Init Container/Sidecar 实现自动化,并最终过渡到使用 MongoDB Kubernetes Operator 进行全生命周期管理。
- 全局视角:建立完善的备份恢复、监控日志和安全体系。
通过遵循上述架构和最佳实践,你可以在 Kubernetes 上构建出稳定、高性能、可扩展且易于运维的 MongoDB 数据服务层。