课程目录(第 31 章 / 共 33 章)
数据库 Operator:把有状态中间件做成平台能力
有状态中间件的难点不在「跑起来」而在「一直跑对」,这一章讲 Operator 怎么把人的运维手册变成控制器,并用 KubeBlocks、Redis、TiKV、FoundationDB 对齐生产视角。
学完这一章,你将能够
- ✓说得出有状态中间件比无状态应用多了哪几类运维动作,以及 Operator 为什么能接管它们
- ✓用 KubeBlocks 的 Cluster 资源部署一套 Redis 主从,并完成扩缩容、升级与备份的声明式操作
- ✓在单实例 Redis、主从加哨兵、TiKV、FoundationDB 之间做出有依据的选型
- ✓知道 StatefulSet 与 Operator 的边界,以及数据库上 Kubernetes 必须提前处理的资源与备份问题
为什么有状态中间件难
无状态应用没有「身份」和「历史」,任何副本都能随时杀掉换台机器重来。数据库恰好相反:它有一块必须活下来的数据,还有「谁是主、谁是从」这种只存在于内存和复制关系里的状态。照搬 Deployment 的写法,几乎一定会出事。
有状态中间件比无状态应用多出六类动作,每一类都不是「重启一下」能解决的。
| 难点 | 无状态应用怎么做 | 有状态中间件必须怎么做 |
|---|---|---|
| 数据持久性 | 不存数据,Pod 重建即可 | 每副本独占一块 PVC,重建后要挂回原来那块 |
| 主从与选主 | 不存在主从 | 持续探测健康度,故障时把从提升为主并通知客户端 |
| 扩缩容 | 改 replicas 就行 | 扩容要加节点并追平数据,缩容要先把数据迁走 |
| 备份恢复 | 不需要 | 定期备份,并保证「真的能恢复」 |
| 版本升级 | 换镜像滚动替换 | 有的引擎必须先升从库再切主,不能随便滚动 |
| 脑裂 | 不存在 | 网络分区时两个节点都以为自己是主,要靠仲裁避免 |
这些动作在传统运维里是一份人的手册:出问题按步骤敲命令。手册的问题很明显——只存在于人的脑子里,凌晨三点没人执行,执行到一半被打断也没人知道。
Operator 要做的事只有一句:把这份手册变成一段永远在运行、永远不会忘记下一步的代码。
Operator:把运维手册变成控制器
Operator 不是新概念,它就是第 2 章里那个「控制器」模式,加上扩展 API 的能力。
- CRD(CustomResourceDefinition):注册你自己的资源类型,比如
Cluster、FoundationDBCluster。它让「一套三副本 Redis 主从、10Gi 存储、版本 7.2.4」变成 etcd 里一条可以kubectl get的对象。 - 控制器:常驻进程,watch 这些对象并执行 reconcile(协调)循环:读期望 → 读现实 → 动手让两者靠近 → 再读一次。
- 状态机:控制器内部维护「现在处于哪个阶段」——创建中、运行中、切换中、升级中。它知道这一步没做完不能做下一步,这正是手册里最难自动化的部分。
一句话说清它和 Helm 的差别:
Helm 装完就走,Operator 持续纠偏。 Helm 的产物是一堆被创建出来的对象,Operator 的产物是一个一直在盯着这些对象、并会自己动手修的进程。
这张图是本章后面所有内容的空间感来源:
用户/应用 ──▶ Service(读写端点,选主后指向主库)
│
Operator ─watch──▶ Cluster CR(期望:引擎/版本/副本数/存储)
│ │ 创建 / 提升 / 迁移
└───────▶ 副本组:redis-0(主) redis-1(从) redis-2(从)
或 TiKV 的一个 Raft 组(Leader + Follower × 3)
│ 挂载
PVC(每副本一块独立卷)──▶ StorageClass ──▶ CSI ──▶ 真实存储最后两层的细节在生产存储,本章只关心 Operator 那层怎么把上面几层串起来。
KubeBlocks:一个平台管多种数据库
如果每个引擎都配一套自己的 Operator(Redis 一套、MySQL 一套、MongoDB 一套),平台团队最终要维护七八个互不相关的控制器,升级节奏、监控口径、备份方式全都不一样。KubeBlocks 的定位就是把这层统一起来:它自己是一个通用 Operator 框架,具体引擎通过「引擎定义」插进来,对上层只暴露一套 Cluster API。
| 对象 | 作用 |
|---|---|
Cluster | 一套数据库集群的期望:引擎、版本、组件、每个组件的副本数与存储 |
| Component | Cluster 里的一个角色或一组实例,如 redis、redis-sentinel、tikv、pd |
ComponentDefinition | 描述某引擎「怎么部署」:容器、启动脚本、探针、配置模板 |
ComponentVersion / ClusterVersion | 引擎版本与镜像的映射表,决定你能选哪些版本 |
OpsRequest | 一次运维动作的声明对象(扩缩容、升级、备份),控制器执行完会记录结果 |
早期版本(0.8/0.9 系)以 ClusterDefinition + ClusterVersion 为主,新版本逐步转向 ComponentDefinition + ComponentVersion,所以装完第一件事是确认你手里的 CRD 是哪一套:
kubectl get crd | grep kubeblocks
kubectl explain cluster.spec | head -30
# 安装用官方 Helm 仓库,命名空间固定 kb-system;下面清单按 0.9 系写,可加 --version 对齐
helm repo add kubeblocks https://apecloud.github.io/helm-charts
helm repo update
helm install kubeblocks kubeblocks/kubeblocks --namespace kb-system --create-namespace
kubectl -n kb-system get pods && kubectl get componentdefinition | grep -E 'redis|mysql'kb-system 里的 Pod 全部 Running,并且 componentdefinition(旧版本看 clusterdefinition)里能看到 redis,就说明 Operator 与引擎定义都就绪了。缺引擎就用 kbcli addon list 检查(kbcli 从 apecloud/kbcli 的 release 页获取;新版本改用 Helm chart 安装引擎,以官方文档为准)。
动手:用 KubeBlocks 跑一套 Redis 主从
动手练习:声明一套三副本 Redis,并做一次扩容
第一步,保存这份 redis-cluster.yaml:三个 Redis 实例加三个哨兵实例,哨兵负责探测健康度并在主库挂掉时选主。清单用 local-path,如果你的集群用 Ceph RBD 或云硬盘,把 storageClassName 换成 kubectl get sc 里实际存在的那个。
apiVersion: apps.kubeblocks.io/v1alpha1
kind: Cluster
metadata: {name: demo, namespace: demo}
spec:
clusterDefinitionRef: redis
clusterVersionRef: redis-7.2.4
terminationPolicy: Delete
componentSpecs:
- name: redis
componentDefRef: redis
replicas: 3
resources:
requests: {cpu: 200m, memory: 512Mi}
limits: {memory: 1Gi}
volumeClaimTemplates:
- name: data
spec: {accessModes: [ReadWriteOnce], storageClassName: local-path, resources: {requests: {storage: 10Gi}}}
- name: redis-sentinel
componentDefRef: redis-sentinel
replicas: 3
resources:
requests: {cpu: 100m, memory: 256Mi}
limits: {memory: 512Mi}
volumeClaimTemplates:
- name: data
spec: {accessModes: [ReadWriteOnce], storageClassName: local-path, resources: {requests: {storage: 5Gi}}}第二步,提交并观察。创建是异步的,STATUS 会先 Creating 再变成 Running;验证时 KubeBlocks 会为每个组件生成一个 Service(名字形如 <cluster>-<组件名>-<引擎>,读写端点指向当前主库),连接密码在带 conn-credential 的 Secret 里(名字随版本略有差异):
kubectl create namespace demo
kubectl apply -f redis-cluster.yaml
kubectl -n demo get cluster
kubectl -n demo get pods -l app.kubernetes.io/instance=demo -o wide
kubectl -n demo get secret | grep conn-credential
kubectl -n demo exec -it demo-redis-0 -- redis-cli ping
kubectl -n demo exec -it demo-redis-0 -- redis-cli info replication | head -5第三步,从 3 副本扩到 5 副本,然后清理。terminationPolicy: Delete 表示删 Cluster 时连 PVC 一起删,练习环境够用,生产别这么设:
kbcli cluster hscale demo --components redis --replicas 5 --namespace demo
kubectl -n demo edit cluster demo # 或直接改 componentSpecs 里 redis 的 replicas
kubectl -n demo delete cluster demo验证成功的三个特征:kubectl -n demo get cluster 的 STATUS 是 Running;demo-redis-0/1/2 与 demo-redis-sentinel-0/1/2 全部 Running;redis-cli ping 返回 PONG,info replication 里能看到一个 role:master 和两个 role:slave。日常运维动作都是这种形态,先记命令骨架,参数细节用 kbcli cluster <子命令> --help 确认(备份与恢复要单独配仓库,见下文「运维细节」):
# 垂直扩缩容:改 CPU / 内存配额,会滚动重启组件
kbcli cluster vscale demo --components redis --cpu 1 --memory 2Gi --namespace demo
# 扩容数据盘(前提是 StorageClass 允许扩容)
kbcli cluster volume-expand demo --components redis \
--volume-claim-templates data --storage 20Gi --namespace demo
# 升级引擎版本(版本名先 kbcli clusterversion list | grep redis 查)
kbcli cluster upgrade demo --cluster-version redis-7.2.4 --namespace demo版本差异要自己确认
本章清单按 0.9 系的 apps.kubeblocks.io/v1alpha1 写,字段是 clusterDefinitionRef / componentDefRef。KubeBlocks 1.0 起 API 版本与部分字段名有调整,kbcli 子命令参数也会变,动手前先 kubectl explain cluster.spec.componentSpecs 对照你装的版本。
Redis 的两种形态:单实例与哨兵/集群
Redis 最容易被「随手一上」,因为它看起来无状态。但它默认把数据放在内存里,Pod 重建而没挂 PVC,数据就全没了。所以先想清楚要哪种形态。
形态一:单实例 Deployment + PVC。 够用、简单、成本最低。三个关键点:数据目录必须挂 PVC、滚动策略必须是 Recreate(RWO 卷不能同时被两个节点挂载)、持久化要打开。再配一个指向 app: redis 的 Service(写法见第 7 章 Service)就能被业务访问。
apiVersion: apps/v1
kind: Deployment
metadata: {name: redis, namespace: demo}
spec:
replicas: 1
strategy: {type: Recreate}
selector: {matchLabels: {app: redis}}
template:
metadata:
labels: {app: redis}
spec:
containers:
- name: redis
image: redis:7.4
args: ["redis-server", "--appendonly", "yes", "--save", "60", "1", "--maxmemory", "800mb"]
ports: [{name: redis, containerPort: 6379}]
resources:
requests: {cpu: 200m, memory: 1Gi}
limits: {memory: 1Gi}
volumeMounts:
- {name: data, mountPath: /data}
volumes:
- {name: data, persistentVolumeClaim: {claimName: redis-data}}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: redis-data, namespace: demo}
spec:
accessModes: [ReadWriteOnce]
storageClassName: local-path
resources: {requests: {storage: 10Gi}}持久化参数决定「重启后还剩多少数据」:--save 60 1 是 RDB 快照(文件小、恢复快,但两次快照之间宕机会丢数据),--appendonly yes 是 AOF 日志(丢数据窗口小,代价是磁盘 IO 与体积),--appendfsync everysec 是 AOF 的刷盘频率(默认每秒,最多丢 1 秒)。`--maxmemory` 必须明显小于容器 memory limit,否则会被 OOMKilled。RDB 和 AOF 文件都写在 /data 下,所以它们和 PVC 是一件事的两面:/data 不落 PVC,持久化配置再全也没意义。用 kubectl -n demo exec redis -- ls /data 应该能看到 appendonly.aof 或 dump.rdb。
形态二:主从 + 哨兵,或 Redis Cluster。 什么时候才需要多副本?三个信号:数据不能丢且要求自动故障切换、单机内存或 QPS 到顶、要按容量横向扩展。反过来,如果只是页面缓存、丢了能重建,单实例加备份就是正确选择——多一套哨兵只是多一份排障成本。主从加哨兵能自动选主,但容量不扩展、写入仍只能打主库;Redis Cluster 用分片解决容量与写入扩展,代价是多 key 操作跨 slot 受限、客户端必须支持,最少 3 主 3 从。
在 Kubernetes 上落地,要么用专用 Redis Operator(如 spotahome/redis-operator 的 RedisFailover,或 OT-CONTAINER-KIT/redis-operator),要么直接用 KubeBlocks 的 redis 加 redis-sentinel 组件。判据是:已经用 KubeBlocks 管其它数据库,就不要再为 Redis 单独引入一套 Operator。
哨兵不会替你扩容量
哨兵只做一件事:探测主库是否存活,并在需要时把某个从库提升为主。数据量还是受单机内存限制,写入还是只能打主库。 要扩容量必须用 Redis Cluster 分片,而分片会带来跨 slot 限制与客户端改造,这是两件不同的工程。
故障切换演练:别等真出事才第一次切
主从加哨兵的方案,没演练过就等于没有高可用。KubeBlocks 部署的集群里哨兵名与集群名一致,可以手动触发一次切换:先确认当前拓扑,把目标从库的优先级调高(数值越小越优先,默认 100),再让哨兵发起故障转移。
# 让 demo-redis-1 更可能被选为新主(replica-priority 越小越优先,默认 100)
kubectl -n demo exec -it demo-redis-1 -- redis-cli -a "$REDIS_DEFAULT_PASSWORD" config set replica-priority 10
# 主动触发一次故障转移,观察业务是否无感
kubectl -n demo exec -it demo-redis-sentinel-0 -- redis-cli -a "$SENTINEL_PASSWORD" -p 26379 SENTINEL FAILOVER demo
# 验证角色真的换了
kubectl -n demo exec -it demo-redis-0 -- redis-cli -a "$REDIS_DEFAULT_PASSWORD" info replication | head -3info replication 里 role:master 从 demo-redis-0 挪到 demo-redis-1 就说明切换成功。生产上必须演练的理由有两条:Redis 主从是异步复制,切换瞬间最后一次未同步的写会丢,这个窗口只能在演练里量出来并告知业务;应用必须连读写 Service 而不是具体 Pod 名或 Pod IP,否则切完请求还打在旧主上。备份侧用 redis-cli BGSAVE 生成 RDB,再把 /data 下的 dump.rdb 与 appendonly.aof 转存到对象存储——卷快照只能整体回滚,逻辑备份才能跨版本、跨云恢复。
参考:reference/k8s-in-action/db/kubeblocks/kbcli-0.9.3/day-3-redis.mdTiKV 与 FoundationDB:分布式 KV 什么时候值得
当单机内存装不下、又需要强一致时,才轮到分布式 KV。这类系统比单机 Redis 复杂一个量级,因为「数据」和「谁是主」都不再是单个节点的事。
TiKV:Raft + PD 调度。 TiKV 的定位是分布式事务 KV 存储,典型形态是 PD + TiKV(再加 TiDB 就是 HTAP 数据库,也可以只当 KV 用)。PD(Placement Driver)是集群的大脑,存元数据、做调度与负载均衡,自身用 Raft 仲裁,3 节点起步;TiKV 是真正存数据的节点,数据切成若干 Region,每个 Region 用 Raft 复制到多个 TiKV 节点,副本数 3 起步;TiDB 是可选的 SQL 层。
它比单机 Redis 复杂在「副本数」有两个不同含义:PD 的 3 是控制面高可用(保证大脑不脑裂),TiKV 的 3 是数据副本数(保证机器挂了数据不丢)。一句话记住扩容逻辑:水平扩展靠加 TiKV 节点,然后由 PD 把 Region 重新分片搬过去。 所以扩容不是「改个数字就完事」,要等数据搬迁完成;缩容更谨慎——必须先把 store 标记为待下线,等 Region 全部迁走再移除节点(TiUP 的 store delete 加 prune 就是这两步)。
在 Kubernetes 上通常由 TiDB Operator 或 KubeBlocks 管理。TiDB Operator 用 Helm 安装,再用 TidbCluster 声明集群(版本号按最新 release 替换):
helm repo add pingcap https://charts.pingcap.org/
helm repo update
helm install tidb-operator pingcap/tidb-operator --namespace tidb-admin --create-namespace
kubectl -n tidb-admin get podsTidbCluster 的关键结构是三个组件各自的 replicas 与存储——spec.pd(replicas: 3、storageClassName、requests.storage)、spec.tikv(同样三项,存储要给足)、spec.tidb(无状态 SQL 层,2 副本起)。字段名随 TiDB Operator 版本变化,写之前用 kubectl explain tidbcluster.spec.tikv 确认;验证看 kubectl -n demo get pods 里 tikv-demo-tikv-0/1/2 与 tikv-demo-pd-0/1/2 全部 Running。
FoundationDB:分布式事务 KV。 它更偏「给别人当元数据底座」——JuiceFS、3FS 这类分布式文件系统常拿它存元数据。它同样由 Operator 管理,安装走 kustomize(版本号按最新 release 替换):
kubectl apply -k "https://github.com/FoundationDB/fdb-kubernetes-operator/config/default?ref=v2.10.0"
kubectl -n fdb-system get podsFoundationDBCluster(apps.foundationdb.org/v1beta2)有四个必须想清楚的注意点:集群文件(cluster file) 是客户端找到集群坐标的依据,它不是「连接串」而是集群的身份,Operator 会写进 Secret / ConfigMap,业务挂载时必须用集群里那一份;存储引擎(如 ssd-2)在数据库配置里指定,选错会影响性能与升级路径;副本数 / 冗余模式用冗余模式表达,double 大致对应三副本,要与节点数和故障域匹配;故障域默认按 kubernetes.io/hostname 分散进程,节点数少于副本数时无法真正分散,等于白配。关键字段是 spec.version、spec.faultDomain.key、spec.processCounts(如 storage: 3、stateless: -1)与 spec.processes.general.volumeClaimTemplate;冗余模式与存储引擎在 databaseConfiguration 里,字段名随版本变化,用 kubectl explain foundationdbcluster.spec 核对。验证时找一个名字带 log 或 storage 的 Pod 执行 kubectl -n fdb-system exec -it fdb-demo-log-1 -- fdbcli --exec "status minimal",看到 The database is available 才算真的起来了。
选型决策表:不是所有数据库都值得搬进 Kubernetes
前面讲的都是「能这么做」,但更重要的一句是:不是所有数据库都值得搬进 Kubernetes。搬进来的收益是标准化、自助化、可复现;代价是你得自己承担存储性能、备份恢复、版本升级与故障演练。团队里没有能兜住这些事的人,自建就只是把风险从「云厂商的 SLO」搬到了「你的值班表」。
| 你的情况 | 建议路线 | 理由 |
|---|---|---|
| 强一致、要事务、要 SQL,团队没有专职 DBA | 云托管数据库(RDS / 云 Redis / 云 TiDB) | 备份、升级、故障切换都有人负责 |
| 一致性要求高,数据量单机放得下 | PostgreSQL / MySQL Operator(KubeBlocks 等) | 主从与备份方案成熟,比分布式 KV 简单 |
| 缓存、会话,可容忍少量数据丢失 | 单实例 Redis + PVC + 定期备份 | 便宜简单,多副本只是多一份排障成本 |
| 缓存且要求自动故障切换 | 主从 + Sentinel(KubeBlocks 或 Redis Operator) | 切换自动化,容量仍在单机范围内 |
| 缓存/会话且单机内存到顶 | Redis Cluster 或 TiKV | 需要分片,客户端要改造,先评估代价 |
| KV 且要强一致、数据量超单机 | TiKV(TiDB Operator / KubeBlocks) | Raft 三副本,可水平扩展 |
| 事务型 KV,作为文件系统元数据底座 | FoundationDB | 事务语义强,但要理解集群文件与冗余模式 |
运维细节:下线节点、备份恢复与集群文件
TiKV:下线一个节点比加一个节点危险
TiKV 扩容只是加节点等 PD 搬数据,缩容则必须先让 Region 迁走再移除节点,否则副本数掉到 2 以下就有丢数据风险。这一步在 TiUP 上的顺序是 scale-in → 等状态变 Tombstone → prune;在 Kubernetes 里等价动作是把 spec.tikv.replicas 调小,由 TiDB Operator 或 KubeBlocks 调 PD 的接口完成。手工核对进度的命令是:
# 在 PD Pod 里看 store 状态:Up → Offline → Tombstone,region_count 归零才算迁完
kubectl -n demo exec -it tikv-demo-pd-0 -- pd-ctl store
# 把某个 store 标记为下线(Operator 缩容时替你做的事)
kubectl -n demo exec -it tikv-demo-pd-0 -- pd-ctl store delete <store-id>三条生产约束:下线前确认剩余节点装得下这些 Region,容量不够会一直卡在 Offline;挑业务低峰做,迁移会打满磁盘与网络;耗时从几分钟到几小时不等,迁移没完别去重启其它节点。临时维护和永久下线是两回事:前者只停进程,PD 发现 Disconnect 后自动补齐副本,节点回来再重平衡,不需要 prune。
备份也要分两件事:PD 元数据备份(tiup cluster meta backup)保住的是集群拓扑与调度元数据;业务数据备份要导出到对象存储(TiDB 生态用 BR,K8s 里是 Backup 对象),才是能恢复数据的那一份。只备元数据不备数据,恢复出来是一个空集群。
参考:reference/k8s-in-action/db/tikv/permanently-take-a-node-offline.md、reference/k8s-in-action/db/tikv/backup-restore.md
FoundationDB:集群文件与冗余模式
FDB 有两个别的数据库没有的概念,配错了业务直接连不上、或者冗余白配:集群文件(cluster file)是客户端发现集群的入口(内含协调器地址与集群 ID),Operator 会把它写进 Secret / ConfigMap,业务必须挂载集群里那一份,不要自己拼地址——扩缩容或换协调器时只有集群文件会更新。冗余模式决定数据放几份,double 大致对应三副本、triple 对应五副本,且必须与故障域(默认 kubernetes.io/hostname)匹配:只有 3 个节点却配 triple,等于把多份数据放在同一台机器上。
kubectl -n fdb-system exec -it fdb-demo-log-1 -- fdbcli --exec "status details"status minimal 打出 The database is available 才算可用;status details 里的 Redundancy mode 与 Datacenter/Machine 容错行,是判断「冗余有没有真的生效」的唯一依据。改冗余模式不是改个字段就完事:从 double 升到 triple 需要额外节点容量并触发数据重分布,先在测试集群量出容量与耗时。
参考:reference/k8s-in-action/db/fdb/README.mdKubeBlocks:备份与恢复的固定动作
KubeBlocks 的备份是「先配仓库、再建备份、最后演练恢复」三步,缺任何一步备份都只会停在 Pending。
# 1. 备份仓库:S3 或一块共享 PVC,必须先存在且 STATUS 为 Ready
kubectl get backuprepo
# 2. 按需备份,并确认真的完成(STATUS 期望 Completed)
kbcli cluster backup demo --namespace demo
# 3. 恢复到新集群,而不是原地覆盖
kbcli cluster restore demo-restore --backup demo-backup --namespace demo定时备份要在 Cluster 的 spec.backup 里写 schedule(cron 表达式)与保留策略,比人手敲 kbcli 可靠。备份方法由引擎的 BackupPolicy 决定(PostgreSQL 的 pg-basebackup、Redis 的 RDB),用 kubectl get backuppolicy 查实际可用值——不同引擎支持的备份方式不同,`backupMethod` 选错会一直失败。
参考:reference/k8s-in-action/db/kubeblocks/kubeblocks-0.9.3/README.md、reference/k8s-in-action/db/kubeblocks/kubeblocks-0.8.4/README.md
生产要点与常见坑
StatefulSet 与 Operator 的边界。 StatefulSet 给的是稳定身份:稳定的 Pod 名字与网络标识、每副本独立 PVC、按序启停。它不给的是业务语义:不知道谁是主、不知道数据有没有追平、不会选主、不会备份、不会编排版本升级。所以边界很清楚:能靠 StatefulSet 解决的别上 Operator;需要理解主从、自动切换、备份恢复的就必须是 Operator——Operator 内部通常也用 StatefulSet 承载 Pod,只是在上面加了一层「懂数据库」的控制器。
PDB 与滚动升级。 节点维护时 kubelet 会驱逐 Pod,三个副本同时被驱逐数据库就不可用了。给数据库 Pod 配 PodDisruptionBudget 能限制自愿驱逐的数量:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: {name: redis-pdb, namespace: demo}
spec:
minAvailable: 2
selector: {matchLabels: {app.kubernetes.io/instance: demo}}PDB 只约束自愿驱逐(kubectl drain、节点维护),不阻止节点宕机,也未必阻止 Operator 自己发起的升级滚动。同时必须保证副本分散在不同节点(反亲和或拓扑约束),否则三个副本落在一台机器上,PDB 也救不了。
存储、备份、观测与 OOM。 三条经验:
- 存储性能直接决定数据库性能:优先本地 NVMe 或网络块存储(三副本),不要用 NFS 跑数据库,量法见生产存储。
- 备份不是快照,监控也不是可选项:快照与源卷往往在同一套存储里,集群整体故障时一起没,真正的备份要落到另一套介质并定期演练恢复(见备份与恢复);连接数、复制延迟、慢查询、磁盘水位、OOM 次数都要有指标与告警(见可观测性)。
- 内存型数据库最容易 OOM:
requests要接近真实工作集;引擎自己的内存上限必须小于容器limits(经验取 70%~80%),剩下的留给复制缓冲与 fork 的写时复制,否则一次全量同步就可能被 OOMKill。
常见错误速查表。 数据库类故障的排查顺序和普通应用不同:先看 Operator 说的状态,再看 Pod,最后看引擎自己的日志。
| 现象 | 原因 | 怎么确认 | 怎么办 |
|---|---|---|---|
Cluster 卡在 Creating | 引擎定义(addon / ComponentDefinition)没装、版本名写错、Operator 未就绪 | kubectl get componentdefinition、kubectl describe cluster <name> 的 Events、kubectl -n kb-system logs deploy/kubeblocks | 装对应引擎、用 kbcli clusterversion list 核对版本名、等 Operator 就绪 |
Pod 一直 Pending,Events 说无可用 PV | StorageClass 不存在、容量不足、WaitForFirstConsumer 在等调度、访问模式不匹配 | kubectl get sc、kubectl describe pvc、kubectl describe pod | 换成真实存在的 storageClassName,见生产存储 |
| 主从切换后应用连不上(连的是旧主) | 应用连了具体 Pod IP 或旧主地址,没走读写 Service | kubectl -n demo get svc 看读写端点;redis-cli info replication 看角色 | 改用 ReadWrite 端点或支持哨兵的客户端,别把 Pod IP 写进配置 |
| 扩缩容一直不完成 | 新副本 PVC 绑不上、数据追平慢、缩容时 Region 还在迁移 | kubectl get cluster -o yaml 看 conditions、kubectl get pods -w、TiKV 用 pd-ctl store 看进度 | 先解决存储,再等迁移;缩容必须等迁移完再移除节点 |
Pod 反复 OOMKilled | 引擎内存上限高于容器 limits、复制缓冲或 fork 峰值超限 | kubectl describe pod 看 Last State: OOMKilled、kubectl top pod | 下调引擎内存上限到限制的 70%~80%,或提高 limits.memory |
| 备份任务失败 | BackupRepo 没配、对象存储凭证错误、容量不足、引擎不支持该备份方式 | kubectl get backup、kubectl describe backup <name>、kubectl get backuprepo | 先修 BackupRepo 与凭证再重跑;备份失败要告警,别等恢复时才发现 |
| 升级后版本不一致 | 部分副本没升成功、镜像拉取失败、Operator 卡在升级中间态 | kubectl get cluster -o wide 看版本列、逐 Pod 看镜像 | 先修镜像与资源问题,再让 Operator 继续;升级前确认有可用备份 |
四个最容易踩的坑
- 用 `Deployment` 跑数据库:没有选主、没有 PVC 复用,Pod 重建后数据全丢,或多副本抢同一块 RWO 卷。数据库请用 StatefulSet 或 Operator。
- `terminationPolicy: Delete` 用在生产:删
Cluster时连 PVC 一起删。生产改成保留策略,并确认 PV 的reclaimPolicy是Retain。 - 三个副本放在一台机器上:单节点故障就全灭。用反亲和或拓扑约束让副本落在不同节点。
- 备份没演练过:备份成功不等于能恢复。定期在独立命名空间做一次真实恢复,这是唯一能验证备份有效的方式。
自测题
自测:为什么数据库要用 Operator,而不是写个 Deployment 加 PVC?(点击展开答案)
因为 Deployment 的控制器只理解「副本数」,它不知道什么叫主、什么叫从。你给它 3 个副本,它会老老实实起 3 个一模一样的进程,全都以为自己是主;换成 StatefulSet 能得到稳定身份,但依然没有任何组件去做选主、追平与切换。
Operator 多了两样东西:一是描述期望的自定义资源(Cluster 里能写引擎、版本、组件、副本数、存储),二是理解这套语义的控制器(知道谁是主、什么时候该提升、升级要先升从库)。这些正是「人的运维手册」里的判断,Deployment 的控制器里没有这类知识,也不可能凭空获得。
自测:为什么副本数一般从 3 开始,而不是 2 或 4?(点击展开答案)
关键在仲裁(quorum)。Raft 或多数派协议要求「超过半数」的节点同意才能选主并写入:3 副本能容忍 1 个节点故障;2 副本只能容忍 0 个,因为 2 的一半是 1,坏一个就凑不出多数派,集群变成只读或不可用。所以 2 副本在多数派协议里是最差的配置——既没有容错能力,又比单副本贵一倍。
4 副本能容忍 1 个故障(4 的一半是 2,坏 1 个还剩 3),容错能力与 3 副本相同,却多占一台机器的资源与一份网络开销,不划算。要提升容错就跳到 5 副本(容忍 2 个故障)。一句话:副本数取奇数,3 是能真正容忍一次故障的最小值。
自测:什么时候应该用托管数据库,而不是自建?(点击展开答案)
先问三个问题:谁在半夜三点处理主库挂了、谁负责验证备份能恢复、谁负责版本升级与安全补丁。答案如果是「没人」或「兼职的我」,托管就是更负责的选择。
优先托管的场景:团队没有专职 DBA 或平台工程师;业务量不大,自建高可用与备份的成本远超数据库本身;合规或 SLA 要求可追责;需要快速上线,不想先花两周搭 TiKV 或 FDB。自建的合理场景:数据合规要求必须放在自己机房;规模大到自建明显更便宜;对引擎有深度定制需求;团队已有成熟的 Operator 运维经验与演练机制。自建的价值不在于省钱,而在于可控;如果可控性换不来实际收益,托管就是更专业的答案。
小结
- 有状态中间件比无状态应用多出数据持久性、主从选主、数据迁移、备份恢复、版本升级、脑裂六类运维动作,这些动作原本写在人的手册里。
- Operator = CRD(描述期望)+ 控制器(reconcile 纠偏)+ 状态机(知道下一步该做什么);与 Helm 的差别是一句话:Helm 装完就走,Operator 持续纠偏。
- KubeBlocks 用一套
ClusterAPI 统一多种引擎,核心对象是Cluster/ Component /ComponentDefinition/ClusterVersion/OpsRequest,扩缩容、升级、备份都有对应的kbcli cluster子命令。 - Redis 单实例(Deployment + PVC +
Recreate策略)适合可重建的缓存;要自动故障切换就上主从加哨兵,要扩容量才需要 Redis Cluster。 - TiKV 用 PD 管调度、TiKV 存数据,Raft 三副本起步,水平扩展靠加 TiKV 节点加 PD 重新分片;FoundationDB 适合做事务型 KV 底座,注意集群文件、存储引擎、冗余模式与故障域。
- 不是所有数据库都值得搬进 Kubernetes:运维能力不足时,托管数据库是更专业的选择。
练习
- 用本章的选型表,给你手头最熟悉的一个有状态服务写出结论:单实例、主从加哨兵、分布式 KV,还是干脆托管?写出三条理由,并指出最大的风险点在哪。
- 把练习里的
Cluster清单改成「两副本」版本,然后想清楚:如果这一套用 Raft 协议,两副本在故障时会变成什么状态?为什么?对照第二道自测题验证你的推理。 - 假设平台要给 10 个业务团队提供「自助申请一套 Redis」的能力,列出你需要的三样东西:命名空间配额方案、备份与恢复演练流程、必须监控的指标。配额可参考命名空间与 RBAC,指标口径参考可观测性。
数据库与中间件做成平台能力之后,平台本身也要被守住:交付流程回到 GitOps 持续交付,恢复能力回到备份与恢复,集群侧的资源规划回到生产集群部署。