StatefulSet、PVC 与 RWO 持久化存储

容器会消失,数据不能跟着失忆

Pod 是可替换资源。镜像更新、节点维护、健康检查失败或控制器重建,都可能让旧 Pod 消失并创建新 Pod。若数据只写在容器可写层,旧 Pod 一删,数据也没了。

本项目需要保存:

  • CPA 配置;
  • 账号认证数据;
  • CPA 日志;
  • cpa-manager 的 SQLite 数据库;
  • cpa-manager 的数据密钥。

因此每个 SubPool 都需要独立的持久卷。Operator 通过 StatefulSet 的 volumeClaimTemplates 为它创建一个 PVC。

四个容易混淆的角色

对象可以理解为由谁关心
VolumePod 中的挂载定义Pod
PVC应用提交的存储申请应用与控制器
PV集群中实际可分配的存储资源Kubernetes 存储控制面
StorageClass动态创建存储的规格与驱动集群管理员

关系大致是:

1
2
3
4
5
6
StatefulSet
  └─ volumeClaimTemplates
       └─ PVC: 我要 20Gi、RWO、指定 StorageClass
            └─ StorageClass / CSI provisioner
                 └─ PV: 实际云盘或其他存储
                      └─ 挂载进 runtime-0 Pod

PVC 不等于磁盘本身,它更像一张存储申请单。PV 才代表已经供 Kubernetes 使用的一块存储。

为什么本项目使用 StatefulSet

Deployment 更适合可替换的无状态副本;StatefulSet 为 Pod 和存储提供稳定身份。

本项目生成的 StatefulSet 名为 runtime,第一个 Pod 通常叫:

1
runtime-0

由 volume claim template 创建的 PVC 名称通常带模板名和 Pod 名,例如:

1
data-runtime-0

Pod 重建后名字仍然是 runtime-0,并继续使用原来的 PVC。对于依赖本地 SQLite、配置文件和账号目录的工作负载,这种稳定身份比 Deployment 更合适。

不过 StatefulSet 并不自动等于“有状态高可用”。本项目只有一个 replica,同一个 RWO PVC 也只服务这个 runtime Pod。

volumeClaimTemplates 做了什么

项目中的核心声明可以抽象为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes:
        - ReadWriteOnce
      storageClassName: standard-rwo
      resources:
        requests:
          storage: 20Gi

它不是引用一个预先写死的 PVC,而是告诉 StatefulSet controller:“给每个 Pod 按这个模板创建自己的 PVC。”

本项目 replicas 固定为 1,所以每个 Pool 正常只有一个由模板生成的 PVC。ResourceQuota 也固定最多允许一个 PVC,进一步阻止意外扩张。

RWO 的真实含义

ReadWriteOnce 常被误读成“只能被一个 Pod 使用”。它更准确的含义是:卷可以被单个 Node 以读写方式挂载。

这意味着:

  • 同一 Node 上的多个 Pod 是否能同时访问,要看具体驱动和挂载方式;
  • 不应把 RWO 当作应用级分布式锁;
  • Pod 跨 Node 重调度时,旧挂载需要先解除,新 Node 才能挂载;
  • 云盘 attach/detach 可能让恢复比无状态 Pod 慢。

本项目用一个 Pod 承载两个容器,正好适配 RWO:两个容器通过同一 Pod 挂载同一个卷,不需要跨 Node 多写。

StorageClass 是平台能力,不是应用细节

当前 Operator Configuration 指定:

1
2
storage:
  className: standard-rwo

这表示 SubPool 只声明容量,平台统一决定使用哪种存储。换一个集群时,管理员必须确认目标集群存在该 StorageClass:

1
2
kubectl get storageclass
kubectl get storageclass standard-rwo -o yaml

如果 StorageClass 不存在或 CSI provisioner 异常,PVC 会一直 Pending,Pod 也无法正常启动。

典型检查:

1
2
3
kubectl -n subpool-demo-pool get pvc
kubectl -n subpool-demo-pool describe pvc data-runtime-0
kubectl -n subpool-demo-pool get events --sort-by=.lastTimestamp

Event 中常能看到 provision、topology、quota 或 attach 失败原因。

容量为什么创建后不可修改

Kubernetes 的 PVC 本身在满足 StorageClass 与 CSI 条件时可能支持扩容,但本项目把 spec.storage.size 定义为不可变。这是项目 API 契约,而不是一句“Kubernetes 永远不能扩容”。

原因在于当前 Operator 直接把容量放进 StatefulSet 的 volumeClaimTemplates。对已存在 StatefulSet,这部分字段受更新限制。若 CR 允许修改而底层模板不能可靠同步,就会出现:

1
2
3
SubPool spec 写 40Gi
现有 PVC 仍是 20Gi
StatefulSet 更新又被 API Server 拒绝

与其提供一个表面可改、实际行为不一致的字段,v1 选择在 CRD 层直接禁止修改。

未来若要支持扩容,需要明确设计:校验 StorageClass 能力、直接扩 PVC、处理文件系统扩容、记录进度和失败恢复,而不是只删除一条 CEL 校验。

暂停为什么能保留数据

设置:

1
2
kubectl patch subpool demo-pool --type=merge \
  -p '{"spec":{"suspend":true}}'

Operator 会把 StatefulSet replicas 调成 0,但保留:

  • SubPool;
  • 派生 Namespace;
  • PVC;
  • Credential Secret;
  • 其他基础配置。

恢复时再把 replicas 调回 1,新 Pod 重新挂载旧 PVC,从上次状态继续运行。

暂停适合临时停机、节省计算资源或等待外部依赖修复。它不释放持久存储费用。

删除为什么是高风险操作

删除 SubPool 会删除它拥有的 Namespace,Namespace 中的 PVC 也随之删除。PVC 删除后,底层 PV 如何处理取决于 StorageClass 的 reclaim policy:

  • Delete:通常连底层存储一起删除;
  • Retain:PV 和底层存储可能保留,但需要人工回收。

项目的产品语义明确把删除视为永久丢失号池运行数据。不要因为某个 Pod 卡住就删除 SubPool“试试看”。

删除前至少确认:

1
2
3
kubectl get sp demo-pool -o yaml
kubectl -n subpool-demo-pool get pvc
kubectl get pv

当前 v1 不负责备份恢复,因此重要数据必须另有备份流程。

PDB 不能保护磁盘,也不能制造副本

runtime StatefulSet 有一个 minAvailable: 1 的 PodDisruptionBudget。它只限制某些自愿中断,例如节点 drain 时的 eviction。

它不能防止:

  • 容器进程崩溃;
  • Node 突然宕机;
  • 磁盘故障;
  • 用户删除 StatefulSet 或 Namespace;
  • 只有一个副本时的滚动更新中断;
  • 数据损坏。

本项目单副本加 RWO 的设计重点是隔离和持久化,不是数据面无中断高可用。

常见状态与判断

PVC Pending

优先检查 StorageClass、CSI provisioner、容量配额和拓扑限制。

Pod Pending 且提示 unbound PVC

Pod 在等 PVC 绑定。不要先查应用日志,因为容器还没有运行。

Multi-Attach error

通常是卷仍挂在旧 Node,或新旧 Pod 在不同 Node 争用 RWO 卷。检查 Pod 调度位置和 VolumeAttachment,等待安全 detach,不要强行绕过。

Pod 运行但 Permission denied

检查镜像运行 UID、fsGroup、卷中文件的历史权限以及 CSI 驱动行为。本项目使用 UID/GID 65532,并设置 FSGroupChangeOnRootMismatch

实用观察命令

1
2
3
4
5
kubectl -n subpool-demo-pool get statefulset runtime
kubectl -n subpool-demo-pool get pod runtime-0 -o wide
kubectl -n subpool-demo-pool get pvc -o wide
kubectl -n subpool-demo-pool describe pvc data-runtime-0
kubectl get storageclass

排查存储时沿着“Pod → PVC → StorageClass/PV → CSI”逐层向下,不要把所有 Pending 都归咎于应用镜像。

使用 Hugo 构建
主题 StackJimmy 设计