在 Kubernetes 上跑有状态数据库,最难的从来不是"让它跑起来",而是让它跑得对:副本集要按顺序初始化并互相发现、成员故障要能自动重新配置、备份要能在不阻塞业务的前提下完成、升级要能滚动进行且保证选举安全。如果全靠手写 StatefulSet + 脚本,这些逻辑会散落在十几份 YAML 与运维文档里。
Operator 模式把这些运维知识编码进控制器(Controller):你声明一个"期望状态"(比如"3 成员的副本集,开启 TLS"),控制器负责把现实收敛到这个状态。本文以 MongoDB 官方的 Community Operator 为主线,讲清它的架构、部署方式与日常运维要点。
1. Operator 解决了什么问题
1.1 手写 StatefulSet 的痛点
用裸 StatefulSet 部署 MongoDB 副本集,至少需要人工处理以下五件事:
- 成员发现:每个 Pod 的 DNS 名要写进
rs.initiate()的配置里,Pod 重建后主机名变化需重新配置。 - 初始化顺序:必须等第一个 Pod 就绪并发起副本集初始化,其余成员才能加入。
- 认证引导:首个成员要创建管理员用户,密钥要在 Pod 之间共享。
- 滚动升级:必须按"先升从节点、再
rs.stepDown()主节点"的顺序,否则会引发不必要的选举。 - 备份:要保证快照或 dump 在主从一致的时间点进行。
每一项单独看都不难,难的是它们之间有顺序依赖,且任一步失败都可能让副本集进入需要人工介入的状态。
1.2 能力对比
Operator 把上述每一项都变成了控制循环里的一个步骤。对比:
| 维度 | 手写 StatefulSet | Operator |
|---|---|---|
| 副本集初始化 | 人工脚本 | 控制器自动 initiate |
| 成员增减 | 手工 rs.add/rs.remove | 改 replicas 字段即可 |
| TLS 证书 | 手工签发与挂载 | cert-manager 集成,自动轮换 |
| 备份 | 外部 cron + 脚本 | CRD 声明式备份到 S3 |
| 升级 | 手工编排顺序 | 自动按安全顺序滚动 |
| 状态可见性 | kubectl get pod | CR 的 status 子资源 |
| 故障自愈 | 需自行实现 | 控制器持续调和 |
1.3 代价与适用边界
引入 Operator 不是没有成本。出问题时排查路径变成"CR 状态 → 控制器日志 → Pod 日志"三层,需要熟悉这套模型。此外,Operator 封装了绝大部分运维决策,遇到需要非常规操作(比如临时手工 rs.reconfig)时,可能与控制器的调和逻辑冲突——控制器会试图把状态拉回它认为正确的样子。
因此适用边界是:标准的副本集/分片部署用 Operator,非常规拓扑或需要深度定制的场景仍可能需要手写 StatefulSet。对绝大多数团队而言,Operator 带来的收益远大于成本。
2. 两种官方发行版
2.1 MongoDB Community Operator
MongoDB 提供两套 Kubernetes 方案,选型前必须分清:
| 方案 | 核心 CRD | 适用场景 | 许可 |
|---|---|---|---|
| MongoDB Community Operator | MongoDBCommunity | 社区版、副本集/分片、自建 | 开源 |
| MongoDB Controllers for Kubernetes (MCK) | MongoDB、MongoDBOpsManager、MongoDBAtlasProject | 企业版、Ops Manager 纳管、Atlas 项目 | 企业许可 |
Community Operator 适合绝大多数自建场景,部署轻量、依赖少。它的 CRD 只有一个 MongoDBCommunity,学习曲线平缓。
2.2 MongoDB Controllers for Kubernetes
MCK 面向企业版特性(如 LDAP、审计、Kerberos)以及希望通过 Ops Manager 统一纳管的团队。其核心 CR 是 MongoDB,可指定 type: ReplicaSet 或 type: ShardedCluster,由 Ops Manager 驱动实际的部署与监控。MCK 还提供 MongoDBAtlasProject,用于从 Kubernetes 侧声明式管理 Atlas 项目与集群。
2.3 共同机制
两者都是典型的 Operator 实现(自定义资源 + 控制器),与 Kubernetes Operator 与 CRD 模式 的通用机制一致:控制器 watch CR 的变化,调和(reconcile)出实际的 StatefulSet、Service、Secret 等原生对象。理解这套"声明式 + 控制循环"模型是使用任何 Operator 的前提。
3. 用 Helm 部署 Operator
3.1 安装
以 Community Operator 为例,用 Helm 安装:
helm repo add mongodb https://mongodb.github.io/helm-charts
helm repo update
# 安装 operator(含 CRD)
helm install community-operator mongodb/community-operator \
--namespace mongodb \
--create-namespace \
--set operator.watchNamespace="mongodb"
# 校验
kubectl -n mongodb get pods
kubectl get crd | grep mongodb
安装后会得到一个 Deployment(控制器)与 MongoDBCommunity 这个 CRD。
3.2 命名空间隔离与 leader election
watchNamespace 决定控制器能管理哪些命名空间下的 CR。生产环境建议按命名空间隔离,避免一个控制器误操作全部 CR——若留空,控制器会 watch 所有命名空间,任何一处 CR 写错都可能波及全局。
控制器本身是无状态的,可多副本部署并开启 leader election,保证同一时刻只有一个实例在调和,避免多个控制器并发修改同一副本集导致状态抖动。
3.3 常见安装问题
- CRD 未安装:
helm install时若跳过 CRD(--skip-crds),后续创建 CR 会报no matches for kind。 - RBAC 不足:控制器需要创建 StatefulSet、Service、Secret、PVC 的权限,自定义 ServiceAccount 时要补全 Role。
- 镜像拉取失败:私有集群需要配置 imagePullSecret 或镜像代理。
3.4 升级 Operator 自身
Operator 也需要升级,且Operator 版本与 CRD 版本必须匹配。升级顺序是:
# 1. 先升级 CRD(新字段可能依赖它)
kubectl apply -f https://raw.githubusercontent.com/mongodb/mongodb-kubernetes-operator/master/config/crd/bases/mongodbcommunity.mongodb.com_mongodbcommunity.yaml
# 2. 再升级 Helm release
helm upgrade community-operator mongodb/community-operator -n mongodb
先升 CRD 再升控制器这个顺序很重要:反过来会导致新控制器期望的字段在旧 CRD 里不存在,调和报错。升级前应先在测试环境验证 CR 兼容性——新版 Operator 有时会修改 CR 的默认值,可能触发生产环境的非预期滚动。
4. 定义副本集资源
4.1 最小可用 CR
一份可用的 MongoDBCommunity 资源:
apiVersion: mongodbcommunity.mongodb.com/v1
kind: MongoDBCommunity
metadata:
name: rs-prod
namespace: mongodb
spec:
members: 3
type: ReplicaSet
version: "7.0.5"
security:
authentication:
modes: ["SCRAM"]
users:
- name: app
db: admin
passwordSecretRef:
name: app-password
roles:
- name: readWrite
db: appdb
scramCredentialsSecretName: app-scram
statefulSet:
spec:
volumeClaimTemplates:
- metadata:
name: data-volume
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 200Gi
template:
spec:
containers:
- name: mongod
resources:
requests:
cpu: "2"
memory: 8Gi
limits:
cpu: "4"
memory: 16Gi
4.2 关键字段解读
members: 3是副本集成员数,改成 5 会触发控制器自动rs.add,无需人工干预。副本集的选举与一致性语义可参考 https://plumephp.com/mongodb-replica-set/。security.authentication.modes设为["SCRAM"]时,控制器会生成 SCRAM 凭据的 Secret,并把连接串所需信息写进 CR 的status。statefulSet.spec是"透传"字段,可以塞入任何 StatefulSet 的合法配置,包括资源限制、亲和性、卷模板。这意味着 Operator 并没有封闭能力,需要细粒度控制时依然可以下探。volumeClaimTemplates决定数据卷,storageClassName必须指向集群里真实存在的 StorageClass。
4.3 分片集群
分片集群则用 type: ShardedCluster 并额外声明 shardCount、mongosCount 与 config server 配置:
spec:
type: ShardedCluster
shardCount: 3
mongosCount: 2
mongodsPerShardCount: 3
configServerCount: 3
分片键的选择决定了数据分布是否均匀,这部分设计见 https://plumephp.com/mongodb-sharding-key-design/。需要注意的是,分片集群的组件数会迅速放大:3 分片 × 3 副本 + 3 config + 2 mongos = 14 个 Pod,资源规划要提前算清。
4.4 用户与角色管理
users 字段声明应用用户与角色。角色遵循 MongoDB 内建角色体系,最常用的几个:
| 角色 | 权限范围 | 典型用途 |
|---|---|---|
read | 只读指定库 | 报表、只读副本查询 |
readWrite | 读写指定库 | 业务应用 |
dbAdmin | 库内管理(索引、统计) | 运维工具 |
clusterMonitor | 读集群状态 | 监控采集 |
root | 超级权限 | 仅在应急时使用 |
实践要点:应用用户绝不用 root,按最小权限分配;监控采集单独用一个只有 clusterMonitor 的账号;每个环境(dev/staging/prod)用独立的 Secret,避免密钥跨环境泄漏。控制器会在 CR 变化时同步更新用户,但修改已有用户的密码不会自动生效,需要显式删除并重建对应的 Secret,或由控制器触发用户更新。
5. 认证、TLS 与证书
5.1 SCRAM 认证
modes: ["SCRAM"] 是最常用的认证方式。控制器会在首次部署时自动创建管理员用户并写入 Secret,应用用户通过 users 字段声明。用户密码以 Secret 引用方式传入,不要把明文密码写在 CR 里:
kubectl -n mongodb create secret generic app-password \
--from-literal=password="$(openssl rand -base64 24)"
5.2 TLS 与 cert-manager
生产环境必须开启 TLS。Community Operator 支持与 cert-manager 集成,由 cert-manager 签发并自动轮换证书:
spec:
security:
tls:
enabled: true
caCertificateSecretRef:
name: mdb-ca
certificateKeySecretRef:
name: mdb-cert
更推荐的方式是让 cert-manager 作为 Issuer 自动管理证书,Operator 在 Pod 启动时把证书挂载进容器。
5.3 证书 SAN 陷阱
TLS 配置最容易踩的坑是证书的 SAN(Subject Alternative Name)。证书的 SAN 必须包含所有成员的服务 DNS 名(含 -0、-1、-2 后缀以及 headless service 名),否则副本集成员之间会因主机名校验失败而无法互联,表现为 rs.status() 里成员一直处于 STARTUP 或反复重连。
典型需要覆盖的名字:
rs-prod-0.rs-prod-svc.mongodb.svc.cluster.local
rs-prod-1.rs-prod-svc.mongodb.svc.cluster.local
rs-prod-2.rs-prod-svc.mongodb.svc.cluster.local
*.rs-prod-svc.mongodb.svc.cluster.local
此外,证书轮换后需要滚动重启才能生效,Operator 会触发,但要确认滚动过程不会同时重启多数成员。客户端侧,TLS 开启后连接串需要带上 tls=true&tlsCAFile=/path/ca.pem。证书与密钥的存放、轮换与审计属于安全运维范畴,可结合 https://plumephp.com/mongodb-security-backup/ 一起规划。
6. 备份与滚动升级
6.1 声明式备份
Community Operator 支持通过 MongoDBCommunity 的备份配置,或独立使用 MongoDB Backup 控制器把快照推送到 S3 兼容存储。核心思路是用副本集一致性读快照保证备份的一致性点,避免备份到跨成员不一致的状态。
备份策略应与恢复演练配套。仅备份不演练等于没有备份——真正出事时才发现备份不可用是最糟的情况。完整的恢复流程与 RPO/RTO 设计见 https://plumephp.com/mongodb-security-backup/。
6.2 滚动升级顺序
修改 spec.version 即可触发升级。控制器的升级顺序是:
- 先逐个升级从节点(secondary),每次只动一个,等其重新同步并追平。
- 所有从节点升级完成后,对主节点执行
rs.stepDown(),触发选举,新主产生后升级旧主。
这个顺序保证了任一时刻多数成员可用,不会因升级丢主。
6.3 oplog 与初始同步
升级前务必确认 oplog 足够大,否则从节点重启后可能因落后太多而触发全量重同步(initial sync),代价极高——initial sync 会重新拉取全部数据并重建索引,对一个大集合可能耗时数小时。
// 查看 oplog 大小与时间窗口
db.getReplicationInfo()
// 关注 timeDiffHours,应大于预期的最长维护窗口
滚动升级期间还应确保 PodDisruptionBudget 配置合理,避免与其他运维操作(节点驱逐、集群升级)叠加导致多数成员同时不可用。
7. 存储与资源规划
7.1 StorageClass 选择
storageClassName 应选支持 allowVolumeExpansion 的 CSI 驱动,便于后续在线扩容:
kubectl get storageclass
# 确认 ALLOWVOLUMEEXPANSION 为 true
若选用的 StorageClass 不支持扩容,后续磁盘不足时只能重建 PVC 并迁移数据,代价极大。
7.2 内存与 WiredTiger
资源限制要给足。MongoDB 的 WiredTiger 缓存默认取 (内存 - 1GB) / 2,limits.memory 过低会让缓存过小、频繁刷盘,更严重的是触发 cgroup OOMKill,Pod 被杀后重启又会引发重同步。经验值:limits.memory 至少是工作集的 1.5 倍,且 requests 与 limits 不宜相差过大,否则调度器难以保证内存。
7.3 反亲和与拓扑分布
生产环境应配置反亲和(anti-affinity),让副本集成员尽量落在不同节点甚至不同可用区:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: rs-prod-svc
topologyKey: kubernetes.io/hostname
requiredDuringScheduling 保证硬性分散,但节点不足时会导致 Pod 无法调度;preferredDuringScheduling 更灵活但可能全部落在同一节点。3 成员的副本集通常用 preferred + 跨可用区拓扑键。
7.4 网络暴露与客户端接入
Operator 默认创建两种 Service:
- Headless Service(
<name>-svc):无 ClusterIP,用于副本集成员之间的稳定 DNS 发现,成员之间靠它互联。 - ClusterIP Service:供集群内客户端接入,自动指向当前主节点。
集群内应用连接串应使用带 replicaSet 参数的多主机形式:
mongodb://app:pass@rs-prod-0.rs-prod-svc.mongodb.svc.cluster.local:27017,rs-prod-1...:27017,rs-prod-2...:27017/appdb?replicaSet=rs-prod&authSource=admin
若需要从集群外访问,通常用 kubectl port-forward 做临时调试,生产环境则通过 LoadBalancer Service 或 Ingress(需 TCP 转发)暴露。不要把 MongoDB 直接暴露到公网——即使有认证,暴露 27017 端口仍会招来大量扫描与暴力破解。
7.5 常见故障速查
| 现象 | 常见根因 | 排查入口 |
|---|---|---|
| Pod 一直 Pending | PVC 无法绑定 / 资源不足 / 反亲和冲突 | kubectl describe pod 的 Events |
| 成员一直 STARTUP | TLS SAN 不匹配 / 主机名解析失败 | rs.status()、成员日志 |
| 反复重同步 | oplog 太小 / 磁盘抖动 | db.getReplicationInfo() |
| 写入变慢 | WiredTiger 缓存不足 / OOMKill | 容器 limits.memory、dmesg |
| 控制器不调和 | CRD 版本不匹配 / RBAC 不足 | 控制器 Pod 日志 |
排查时记住三层路径:CR 状态 → 控制器日志 → Pod 日志。大多数"Operator 不干活"的问题最终都落在 CRD 版本或 RBAC 上。
8. 实践建议
- 按命名空间隔离控制器,用
watchNamespace限制其管理范围,降低误操作半径。 - 证书 SAN 要覆盖全部成员 DNS 名,这是 TLS 副本集最常见的初始化失败原因。
- 升级前检查 oplog 大小,并确认
PodDisruptionBudget不会允许多数成员同时下线。 - 备份必须配套恢复演练,并明确 RPO/RTO 目标。
- 资源限制要给足,WiredTiger 缓存依赖内存,
limits.memory过低会引发 OOMKill 与频繁重同步。 - 善用
statefulSet.spec透传,需要亲和性、拓扑分布约束时不必绕开 Operator。 - 分片集群提前算清组件数,资源与网络开销随组件数成倍放大。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。