Kubernetes 基础体系 · 第 41/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

数据库运行在 Kubernetes:Operator、存储、拓扑、备份和责任边界

数据库运行在 Kubernetes,并不是把数据库进程改成一个 Deployment 就完成了容器化。Kubernetes 能够管理 Pod、网络、卷和调度,但数据库还需要处理日志、选主、复制、故障恢复、版本升级、备份一致性和数据删除。这些职责之间如果没有明确边界,系统通常会出现一种危险状态:Pod 看起来是 Running,但数据库已经丢失仲裁、写入被阻塞,或者恢复出来的数据无法使用。

本文使用当前 Kubernetes 稳定 API 的概念说明这条边界。示例主要使用 apps/v1 的 StatefulSet、storage.k8s.io/v1 的 StorageClass、policy/v1 的 PodDisruptionBudget,以及 Kubernetes 的拓扑调度能力。Operator、CSI 快照和 Velero 的具体字段、插件行为会随版本和云厂商变化,涉及这些部分时会明确指出实现差异。

一、先建立正确的系统模型

一个运行在 Kubernetes 中的数据库至少包含以下状态:

  1. 身份状态:节点叫什么、是否仍然是原来的节点。
  2. 数据状态:表文件、索引、事务日志、控制文件等。
  3. 复制状态:主库、备库、同步位点、仲裁成员。
  4. 控制状态:期望运行几个实例、当前谁是主库、是否正在升级。
  5. 备份状态:某个备份对应的时间点、日志范围和恢复方式。

Kubernetes 原生对象主要描述第 1、4 类状态,并通过 Pod、PVC、Service 等对象间接承载其他状态。数据库自身负责第 2、3、5 类的语义。

因此,下面这种等式并不成立:

Pod 已恢复 + PVC 已挂载 = 数据库已恢复

更准确的关系是:

可用数据库=进程存活存储可访问数据一致复制状态合法客户端连接到正确角色\text{可用数据库} = \text{进程存活} \land \text{存储可访问} \land \text{数据一致} \land \text{复制状态合法} \land \text{客户端连接到正确角色}

其中任一条件不成立,Kubernetes 的 Ready 也不能自动代表数据库可用。一个只执行 TCP 端口检查的探针,可能在数据库仍处于恢复、只读或未加入复制组时返回成功。

1. Kubernetes 管理什么,数据库管理什么

Kubernetes 原生控制器通常处理:

  • Pod 是否存在;
  • 容器是否重启;
  • PVC 是否绑定;
  • Service 是否拥有匹配的 Endpoint;
  • 节点故障后的重新调度;
  • 滚动更新和副本数。

数据库需要处理:

  • WAL、redo log 或 binlog 的持久化;
  • 崩溃恢复;
  • 主备复制;
  • 选主和脑裂防护;
  • 复制延迟;
  • 数据库版本升级;
  • 逻辑备份、物理备份和时间点恢复;
  • 数据库角色变化后的连接路由。

Operator 的价值,就是把后一组数据库知识编码为 Kubernetes 控制器,使它能够根据数据库状态创建或修改前一组 Kubernetes 对象。

二、Operator:不是“更强的 StatefulSet”

1. Operator 的组成

Operator 通常由以下部分构成:

  • CRD:定义数据库集群的声明式 API,例如 PostgresClusterMySQLCluster
  • 自定义资源(CR):用户提交的具体集群期望状态。
  • Controller:监听 CR、Pod、PVC、Service 等事件,并持续执行调谐。
  • 数据库管理逻辑:初始化、加入集群、选主、扩缩容、升级、备份和故障修复。
  • RBAC 权限:允许 Operator 读写相关 Kubernetes 对象。
  • Webhook 或校验逻辑:在 API 接受资源前检查参数。

Operator 的核心不是一组 YAML,而是一个持续运行的控制循环:

Action=f(Desired State,Observed State)\text{Action} = f(\text{Desired State}, \text{Observed State})

控制器读取用户希望的状态和当前观察到的状态,计算差异,然后执行动作。执行后再次读取状态,直到系统逐渐收敛。

例如,用户声明:

apiVersion: database.example.io/v1
kind: ExampleDatabaseCluster
metadata:
  name: orders
spec:
  instances: 3
  storage:
    size: 500Gi
  backup:
    enabled: true

这段资源不能直接提交到普通 Kubernetes 集群,除非名为 ExampleDatabaseCluster 的 CRD 已经安装。它只是说明 Operator API 的形状,不是 Kubernetes 内置 API,也不是所有数据库 Operator 通用的配置。

一个实现可能据此创建:

  • 3 个带稳定身份的数据库 Pod;
  • 3 个 PVC;
  • 一个客户端 Service;
  • 一个只读副本 Service;
  • 复制用户和初始化配置;
  • 备份 CronJob 或数据库原生备份任务;
  • 监控和告警对象;
  • 用于安全升级的 PDB。

2. 调谐不是一次性安装脚本

假设主库 Pod 被节点故障删除,调谐过程可能是:

  1. Kubernetes 根据 StatefulSet 或 Operator 逻辑重新创建 Pod。
  2. Pod 挂载原有 PVC。
  3. 数据库启动并执行崩溃恢复。
  4. Operator 发现该实例尚未成为可用副本。
  5. Operator 根据数据库状态让它追赶主库,或者重新初始化为备库。
  6. 只有复制状态满足条件后,Operator 才把它加入读流量或标记为可用。

如果 Operator 只观察 Pod 是否为 Running,就无法完成第 3 到第 6 步。因此生产环境应查看 Operator 的自定义资源状态、事件、数据库角色和复制位点,而不应只看:

kubectl get pods

常用的第一轮诊断是:

kubectl get events -n database --sort-by=.lastTimestamp
kubectl describe pod -n database orders-0
kubectl get pvc -n database
kubectl get <数据库CRD类型> -n database orders -o yaml
kubectl logs -n database deploy/<operator-deployment>

其中 <数据库CRD类型> 和 Operator Deployment 名称取决于具体实现,不能假设所有 Operator 都使用相同命名。

3. Operator 的边界

Operator 并不会自动解决所有问题:

  • 它不能把单副本数据库变成跨可用区高可用数据库;
  • 它不能让没有跨区域复制的集群抵抗区域级故障;
  • 它不能证明快照具备事务一致性;
  • 它不能替代备份恢复演练;
  • 它不能消除错误删除 PVC 的风险;
  • 它不能保证数据库客户端永远连接到正确角色,除非它同时维护了角色感知的 Service 或连接配置。

Operator 自身也是生产组件。它的升级、权限、故障恢复和兼容性需要单独管理。Operator 宕机通常不会立即停止已经运行的数据库,但会使故障修复、主从切换、扩缩容和升级停止,直到控制器恢复。

三、StatefulSet:稳定身份,不是数据库高可用

StatefulSet 是 Kubernetes 用来管理有状态 Pod 的工作负载 API。它提供的核心能力包括:

  • 稳定、可预测的 Pod 名称;
  • 稳定的网络身份;
  • 通过 volumeClaimTemplates 为每个 Pod 创建独立 PVC;
  • 可配置的创建和删除顺序;
  • 滚动更新策略。

例如:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: demo-db
  namespace: database
spec:
  serviceName: demo-db
  replicas: 3
  selector:
    matchLabels:
      app: demo-db
  template:
    metadata:
      labels:
        app: demo-db
    spec:
      containers:
        - name: db
          image: example/database:1.0
          ports:
            - name: db
              containerPort: 5432
          volumeMounts:
            - name: data
              mountPath: /var/lib/database
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: fast-rwo
        resources:
          requests:
            storage: 100Gi
---
apiVersion: v1
kind: Service
metadata:
  name: demo-db
  namespace: database
spec:
  clusterIP: None
  selector:
    app: demo-db
  ports:
    - name: db
      port: 5432
      targetPort: db

serviceName 关联一个 Headless Service。对于合适的 DNS 配置,Pod 可以获得类似 demo-db-0.demo-db.database.svc 的稳定地址。demo-db-0 被删除后重新创建,名称仍然相同,PVC 通常也仍然对应它。

但是,StatefulSet 不理解“主库”“只读副本”“复制位点”这些数据库概念。它可以保证 demo-db-0 这个 Pod 身份稳定,却不会自动判断:

  • demo-db-0 是否真的拥有最新数据;
  • demo-db-1 是否可以提升为主库;
  • 删除 demo-db-0 会不会破坏法定人数;
  • 滚动更新是否会同时中断过多副本;
  • 客户端应该连接哪个实例。

因此,直接用 StatefulSet 运行数据库更适合测试、单实例服务或由数据库团队自行实现完整控制逻辑的场景。生产集群通常由 Operator 生成或管理 StatefulSet,而不是把 StatefulSet 当作数据库 Operator。

创建、更新与删除的风险

StatefulSet 的有序行为是 Kubernetes 工作负载语义,不等于数据库安全语义。默认的 OrderedReady 会倾向于按序创建和更新 Pod,但“Pod Ready”必须由正确的探针定义。如果探针只检查进程存在,Kubernetes 可能在数据库尚未完成复制初始化时继续更新下一个实例。

删除 StatefulSet 也不必然删除 PVC。PVC 是否保留、PV 的回收行为和 Operator 的 finalizer 共同决定数据是否被删除。生产删除前应分别检查:

kubectl get statefulset,pvc,pv -n database
kubectl get storageclass fast-rwo -o yaml

不要仅因为 StatefulSet 已删除,就推断数据已经删除或一定安全保留。

四、存储:PVC 解决“挂载什么”,不解决“数据是否一致”

1. 从 Pod 到磁盘的对象链

数据库使用的持久化路径通常经过以下对象:

Pod
  -> volumeMount
  -> PVC
  -> PV
  -> StorageClass / CSI Driver
  -> 云盘、网络存储或本地磁盘
  • PVC 是工作负载对存储容量和访问模式的请求。
  • PV 是实际可分配的存储资源。
  • StorageClass 描述动态供应所使用的 CSI 驱动、参数和回收策略。
  • CSI Driver 将 Kubernetes 的挂载请求翻译成云盘、SAN、分布式块存储等后端操作。

PVC 绑定成功只代表 Kubernetes 找到了满足请求的卷,不代表该卷具备数据库需要的 IOPS、延迟、吞吐或故障域属性。

常见访问模式包括:

  • ReadWriteOnce:通常表示可由单个节点读写;它不是“只能有一个 Pod”这一绝对语义。
  • ReadOnlyMany:多个节点可只读挂载,是否支持取决于驱动。
  • ReadWriteMany:多个节点可读写,常见于文件存储;并不自动适合数据库。
  • ReadWriteOncePod:更严格地限制为单个 Pod 使用,支持情况取决于 CSI 驱动和集群版本。

ReadWriteMany 也不等于数据库共享磁盘集群。多个数据库实例同时写同一数据目录,除非数据库明确支持这种架构并使用相应的锁和一致性协议,否则可能导致文件损坏。

2. StorageClass 的延迟绑定与拓扑

一个重要字段是:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-rwo
provisioner: csi.example.com
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
reclaimPolicy: Retain

WaitForFirstConsumer 表示 PVC 不一定在创建时立即绑定,而是等待调度器已经知道 Pod 的节点约束后,再选择合适拓扑中的存储。对于具有可用区限制的云盘,这可以避免卷在 zone-a 创建,而 Pod 被调度到 zone-b。

但这不是跨区复制。单个云盘通常仍然属于一个故障域。要抵抗 zone-a 故障,需要数据库副本分别位于不同 zone,并且每个副本拥有自己的卷。

3. PVC 扩容不是数据库扩容

当 PVC 的容量请求从 100Gi 改为 200Gi 时,可能发生两个阶段:

  1. CSI 驱动扩展底层卷;
  2. 文件系统在线扩展或在重新挂载时扩展。
kubectl patch pvc data-demo-db-0 -n database \
  --type merge \
  -p '{"spec":{"resources":{"requests":{"storage":"200Gi"}}}}'

kubectl get pvc data-demo-db-0 -n database -w

PVC 进入 FileSystemResizePending 等状态时,说明底层卷已经变化,但文件系统尚未完成扩展。即使文件系统已扩容,数据库的表空间、配额和内部存储管理也可能需要额外操作。容量扩大通常不能自动缩小;直接修改 PVC 使其变小往往不被支持,强行处理可能破坏数据。

4. 存储性能必须按数据库工作负载验证

数据库关心的不是“磁盘大小”一个数字,而是:

  • 随机读写延迟;
  • IOPS;
  • 吞吐;
  • fsync 或等价持久化语义;
  • 突发性能耗尽后的行为;
  • 节点或区域故障时的可访问性;
  • 快照和克隆是否影响前台 I/O。

例如,事务提交通常要等待日志持久化。如果存储对 fsync 的实现只是缓存确认,而不是可靠落盘,数据库即使返回提交成功,也可能在节点或存储故障后丢失最近事务。这个问题不能通过 Kubernetes 的 ReadinessProbe 或 PVC 状态发现,需要数据库级别的故障测试和供应商存储语义验证。

五、拓扑:副本数量不等于故障域数量

1. 拓扑的三个层次

数据库拓扑至少要同时考虑:

  1. Pod 拓扑:Pod 位于哪个节点、可用区或区域。
  2. 卷拓扑:PVC 对应的 PV 能在哪些节点挂载。
  3. 数据库复制拓扑:哪些实例互为副本,谁能成为主库,复制是否同步。

只把 Pod 分散到不同节点而把所有数据放在同一个共享存储故障域中,并不能提供完整的故障隔离。

Kubernetes 常用标签包括:

kubernetes.io/hostname
topology.kubernetes.io/zone
topology.kubernetes.io/region

这些标签由集群或云平台提供,具体值和可靠性应在目标环境中验证。

2. 使用反亲和性和拓扑分布

下面的片段要求相同数据库集群的 Pod 尽量分散到不同 zone:

spec:
  template:
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app: demo-db
              topologyKey: topology.kubernetes.io/zone
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: demo-db

两者关注点不同:

  • podAntiAffinity 可以表达“不能与同类 Pod 处于同一拓扑域”;
  • topologySpreadConstraints 表达跨拓扑域的数量平衡。

如果集群只有两个 zone,却要求三个副本严格分散,那么 DoNotSchedule 可能使第三个 Pod 长期处于 Pending。这是约束正确生效的表现,不是调度器故障。

可以检查原因:

kubectl describe pod demo-db-2 -n database

在事件中通常能看到不满足的反亲和性、拓扑或卷约束。

3. 仲裁与故障域的算例

假设数据库使用多数派提交,集群有 NN 个投票成员,法定人数为:

Q=N2+1Q = \left\lfloor \frac{N}{2} \right\rfloor + 1

N=3N=3 时,Q=2Q=2。如果三个成员分别在 zone-a、zone-b、zone-c:

  • zone-a 故障:剩余 2 个成员,仍满足 Q=2Q=2
  • 任意两个 zone 同时故障:只剩 1 个成员,不满足仲裁;
  • 三个 Pod 都在 zone-a:zone-a 故障时直接剩 0 个成员。

因此,“3 副本”只有结合故障域分布才有意义。还要注意非投票只读副本可能不计入仲裁;数据库 Operator 的 instances: 3 也未必表示三个投票成员,必须查看具体数据库语义。

4. PDB 不能防止所有中断

PodDisruptionBudget 用于限制自愿驱逐,例如节点排空。示例:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: demo-db
  namespace: database
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: demo-db

它不保证:

  • 云平台不会发生强制节点故障;
  • 数据库一定可以在剩余副本上写入;
  • 误删 Pod 不会发生;
  • 版本升级一定安全;
  • Operator 一定理解数据库角色。

PDB 还是一个调度和驱逐层约束,不能替代数据库的选主和同步复制机制。

六、备份:复制、快照和逻辑备份不是同一件事

1. 复制不是备份

复制通常追求降低故障恢复时间和提高可用性。备份则追求在误删、逻辑损坏、勒索、错误升级或全局配置错误后回到历史状态。

如果错误 SQL 被主库执行并同步到所有副本,复制会把错误传播到所有副本。这个场景中,复制不能替代备份。

2. RPO 和 RTO

  • RPO(Recovery Point Objective):最多可以接受丢失多久的数据。
  • RTO(Recovery Time Objective):从故障发生到服务恢复允许花费多久。

如果目标是 RPO 15 分钟,备份周期不能简单地设为 1 小时。对于时间点恢复,还需要连续归档事务日志。可以粗略表示为:

可恢复时间点最近一次完整备份时间+已成功归档的日志范围\text{可恢复时间点} \leq \text{最近一次完整备份时间} + \text{已成功归档的日志范围}

如果日志归档中断,即使完整备份任务显示成功,也可能无法恢复到期望时间点。

3. Kubernetes 资源备份与数据库数据备份

一个 Kubernetes 备份工具可能备份:

  • Namespace、Service、Secret、ConfigMap;
  • StatefulSet 或 Operator CR;
  • PVC/PV 的引用关系;
  • CSI VolumeSnapshot;
  • Pod volume 中的文件。

这些对象备份并不自动等价于数据库一致性备份。恢复 Kubernetes YAML 后,数据库可能仍然缺少:

  • 尚未刷盘的事务;
  • 归档日志;
  • 数据库系统表中的角色信息;
  • 备份元数据;
  • 加密密钥;
  • 需要按顺序恢复的集群配置。

因此必须区分:

备份方式 主要内容 一致性来源 典型恢复用途
逻辑备份 表、模式、对象 数据库导出协议 跨版本、部分对象恢复
物理备份 数据目录或数据库原生备份格式 数据库备份工具与日志 快速整库恢复
日志归档 WAL、redo、binlog 等 数据库日志机制 时间点恢复
存储快照 卷块或文件系统状态 存储系统 快速克隆或卷级恢复
Kubernetes 资源备份 API 对象 Kubernetes API 重建控制面资源

4. 一个可执行的逻辑备份例子

以 PostgreSQL 客户端为例,备份命令可以在具有数据库网络访问权限和对象存储上传权限的 Job 中执行:

pg_dump \
  --host=orders-rw.database.svc \
  --port=5432 \
  --username=backup \
  --format=custom \
  --file=/backup/orders-$(date -u +%Y%m%dT%H%M%SZ).dump \
  orders

前置条件包括:

  • orders-rw 是数据库 Operator 或管理员维护的可写入口;
  • backup 用户拥有需要导出的对象权限;
  • PGPASSWORD 或 Secret 注入了凭据;
  • /backup 位于可靠的备份存储,而不是只挂载在当前 Pod 的临时目录;
  • 备份文件生成后被上传到集群外的对象存储。

custom 格式支持 pg_restore 选择对象恢复,但具体选项和锁行为仍受 PostgreSQL 版本影响。备份完成后必须验证退出码、文件大小、校验和,并在独立环境执行一次恢复,而不是只验证 Job 为 Completed

对于 MySQL、MongoDB、Redis 等数据库,备份命令、快照语义和恢复步骤完全不同,不能把 pg_dump 替换成一个不同容器镜像后就认为方案成立。

5. Velero 的角色和限制

Velero 常用于备份 Kubernetes 资源,并结合 CSI 快照或文件系统数据移动能力保护卷。典型命令形式如下:

velero backup create orders-cluster \
  --include-namespaces database \
  --wait

velero backup describe orders-cluster --details
velero backup logs orders-cluster

这些命令的前提是 Velero 已安装、对象存储位置已配置,并且集群安装了与存储后端匹配的插件或 CSI 支持。不同 Velero 版本对 CSI 快照、数据移动和备份位置的选项不同,不能假定某个云厂商的参数适用于所有环境。

Velero 的资源备份通常包含 Kubernetes 对象;卷数据是否被保护取决于配置和能力:

  • 使用 CSI VolumeSnapshot 时,需要可用的 VolumeSnapshotClass 和兼容的 CSI 驱动;
  • 使用文件系统数据移动时,需要节点代理、权限和相应的数据移动组件;
  • 只备份 PVC 对象而没有卷数据,恢复时可能得到一个空卷或无法绑定的引用;
  • 只恢复卷而没有 Secret、Service 和 Operator CR,数据库可能无法按原拓扑启动。

CSI VolumeSnapshot 相关 API 由 Kubernetes CSI snapshotter 生态提供,并非所有能力都由 Kubernetes 核心 API 单独保证。其 snapshot.storage.k8s.io/v1 API、驱动支持和跨集群恢复能力必须在目标版本中核实。

6. 快照的一致性边界

在数据库仍持续写入时对卷做快照,可能得到崩溃一致状态,也可能依赖存储驱动的具体实现。即使数据库能通过 WAL 或 redo log 启动恢复,也不能因此推断:

  • 所有事务都存在;
  • 快照跨多个卷具有同一时间点;
  • 外部对象存储中的文件与数据库状态一致;
  • 备份满足业务的逻辑一致性要求。

应用一致快照通常需要:

  1. 调用数据库或 Operator 的备份接口;
  2. 暂停或协调写入;
  3. 刷新并固定日志;
  4. 创建相关卷快照;
  5. 记录备份元数据;
  6. 恢复后应用日志并验证数据库。

Velero Hook 可以在备份或恢复前后执行命令,但 Hook 只是执行机制,不会自动理解数据库事务。Hook 的注解格式、超时和失败处理应以所用 Velero 版本文档为准。若 Hook 失败却被忽略,备份任务可能看起来完成,实际却未达到一致性要求。

七、恢复不是“先恢复 Pod”

数据库恢复是一个有依赖顺序的状态转换过程:

flowchart TD
    A[确认故障范围与恢复目标] --> B[准备目标集群与存储]
    B --> C[恢复 Secret、ConfigMap、Service 和 Operator]
    C --> D[恢复数据库 CR 或工作负载]
    D --> E[恢复 PVC 或从快照创建卷]
    E --> F[数据库实例启动并执行崩溃恢复]
    F --> G[应用物理备份或逻辑备份]
    G --> H[重放 WAL、redo 或 binlog]
    H --> I[验证一致性、角色和复制状态]
    I --> J[切换客户端入口并观察]

关键路径是:身份和凭据先于数据库启动,数据卷先于数据恢复,日志先于时间点确认,流量切换最后执行

一个典型恢复错误是先恢复 Service,再让客户端连接到一个正在恢复的数据库。客户端可能执行写操作,导致恢复目标被污染。更安全的做法是:

  • 恢复期间暂时不暴露写入口;
  • 使用独立恢复 Namespace;
  • 恢复后验证数据库版本、系统表、表数量、关键业务校验和;
  • 确认角色是主库还是只读副本;
  • 最后修改 DNS、Service selector 或连接配置。

如果使用 Velero 恢复资源,应检查:

velero restore create orders-restore \
  --from-backup orders-cluster \
  --wait

velero restore describe orders-restore --details
velero restore logs orders-restore

恢复后还要检查:

kubectl get pods,pvc,svc -n database
kubectl get events -n database --sort-by=.lastTimestamp

Completed 只表示 Velero 的恢复流程完成,不表示数据库业务恢复成功。业务验证必须包括实际连接、读写测试、复制状态、备份日志连续性和关键数据校验。

八、故障路径与诊断顺序

场景一:Pod 长期 Pending

可能原因包括:

  • PVC 尚未绑定;
  • PV 与 Pod 不在同一 zone;
  • 节点无足够 CPU 或内存;
  • 反亲和性要求无法满足;
  • ReadWriteOnce 卷仍挂载在旧节点;
  • 节点缺少 CSI 驱动或挂载权限。

诊断顺序:

kubectl describe pod db-0 -n database
kubectl get pvc -n database
kubectl describe pvc data-db-0 -n database
kubectl get pv
kubectl get csidriver

不要先删除 PVC“重试”。如果根因是拓扑不匹配,删除 PVC 可能触发新卷创建;如果根因是旧节点仍持有卷,强行删除可能造成双挂载或数据损坏。

场景二:Pod Running 但数据库不可用

检查:

  • Readiness 探针是否只检查端口;
  • 数据库是否正在 crash recovery;
  • 复制是否延迟或断开;
  • 主库是否已经变更;
  • Service selector 是否仍指向旧角色;
  • 网络策略是否阻断了 Operator 或数据库节点间通信。
kubectl get endpointslice -n database -l kubernetes.io/service-name=orders-rw
kubectl describe pod orders-0 -n database
kubectl logs orders-0 -n database --previous

然后使用数据库原生客户端查询角色和复制状态。Kubernetes 只能告诉你端点和容器状态,不能替代数据库状态查询。

场景三:删除一个副本后集群停止写入

这通常表示数据库保护机制生效:剩余成员不满足同步提交或仲裁条件。需要确认:

  1. 故障的是非投票副本还是投票成员;
  2. 剩余成员是否位于同一故障域;
  3. 复制位点是否落后;
  4. Operator 是否在等待安全选主;
  5. 是否存在网络分区而不是单纯 Pod 删除。

贸然修改副本数、删除数据目录或强制提升某个实例,可能制造脑裂。应优先使用数据库 Operator 提供的故障处理流程,并保存事件、Operator 日志和数据库日志。

场景四:恢复后数据库启动但数据不完整

常见原因:

  • 只恢复了 PVC 对象,没有恢复卷内容;
  • 快照创建时没有数据库一致性保证;
  • 缺少 WAL、redo 或 binlog;
  • 恢复了错误的时间点;
  • Secret 或加密密钥不匹配;
  • 多个卷没有使用同一时间点;
  • 数据库版本与备份格式不兼容。

应记录恢复点和日志范围,而不是只看 Pod 是否 Ready。对关键表执行行数、校验和、业务账务平衡或事件序列验证,才能判断恢复是否达到业务要求。

九、责任边界:谁对什么结果负责

责任边界应写成可验证的接口,而不是一句“平台负责,DBA 负责”。

领域 Kubernetes/平台通常负责 数据库团队通常负责
Pod 生命周期 调度、重启、节点替换 启动参数、优雅关闭、探针语义
身份与卷 StatefulSet、PVC、CSI 挂载 数据目录布局、卷使用方式
拓扑 节点标签、调度约束、PDB 副本角色、仲裁、同步策略
高可用 提供网络和故障域 选主、复制、脑裂防护
备份基础设施 对象存储、Velero、快照插件 一致性、日志归档、恢复点
安全 RBAC、Secret 存储、网络策略 数据库账户、权限、加密配置
升级 镜像拉取、工作负载更新机制 版本兼容、迁移、回滚和数据格式
业务恢复 提供目标集群资源 数据完整性、业务校验和切流

云厂商还可能对块存储的多可用区能力、快照一致性、备份保留、跨区域复制和 IOPS 计费负责,但这些能力必须以具体产品文档和测试结果为准,不能由 Kubernetes API 名称推断。

十、生产设计中的几个取舍

1. Operator 还是自建 StatefulSet

选择 Operator 的成本包括 CRD、控制器、权限和升级依赖;收益是将数据库故障处理、复制和备份流程自动化。选择原生 StatefulSet 的成本则是团队必须自行实现这些数据库语义。

如果团队不能清楚回答“主库故障后谁提升、如何防止双主、PVC 如何重建、备份如何恢复”,直接使用 StatefulSet 通常只是把复杂度转移到了人工操作。

2. 单区低延迟还是跨区高可用

跨 zone 副本可以降低单区故障影响,但通常会引入更高网络延迟、复制延迟和存储成本。同步复制要求提交等待远端确认,可能降低写入性能;异步复制提高可用性或性能,却允许故障时丢失最近事务。

应先由业务定义可接受的 RPO/RTO,再选择同步或异步复制、跨区范围和备份频率,而不是先指定“必须三副本”。

3. 快照恢复速度还是逻辑迁移能力

卷快照往往适合同版本、同存储体系内的快速恢复;逻辑备份更适合跨版本、跨引擎或只恢复部分表,但通常恢复时间更长。实际方案常常同时保留:

  • 定期完整物理备份;
  • 连续日志归档;
  • 必要的逻辑导出;
  • Kubernetes 资源备份;
  • 独立恢复环境。

这不是重复建设,而是分别应对硬件故障、逻辑错误、版本迁移和集群重建。

十一、上线前必须验证的闭环

数据库在 Kubernetes 上是否可靠,最终取决于故障和恢复演练,而不是 YAML 是否通过校验。至少应验证以下闭环:

  1. 删除一个数据库 Pod,确认实例能否重新挂载原 PVC 并完成恢复。
  2. 驱逐节点,确认 PDB、拓扑和数据库仲裁行为符合预期。
  3. 模拟一个 zone 不可用,确认剩余副本和客户端入口状态。
  4. 让备份任务失败,确认告警能够发现,而不是只看 CronJob 存在。
  5. 在隔离 Namespace 恢复一次完整备份和日志。
  6. 验证恢复后的数据库角色、关键数据和应用读写。
  7. 测试错误 SQL 后从历史备份恢复到指定时间点。
  8. 测试 Operator 停止一段时间后,已有数据库和后续调谐分别表现如何。
  9. 记录每一步耗时,得到真实 RTO,而不是文档中的估算值。

Kubernetes 为数据库提供了声明式资源、稳定身份、调度、存储和故障重建能力;Operator 将数据库控制逻辑接入这些能力;CSI 和 Velero 提供存储与资源保护的基础设施。但数据一致性、复制安全、备份有效性和恢复正确性仍然属于数据库系统本身。只有把这几层的状态和责任分开建模,数据库运行在 Kubernetes 才不会退化为“容器重启成功,却无法证明数据安全”。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。