阶段四 · 有状态实战

有状态应用与存储选型

一句话总结

把前七篇的网络与存储知识, 落到一个真实问题上: 有状态应用怎么在 K8s 里安全跑起来?
StatefulSet 用 volumeClaimTemplates 给每个副本一块独立持久盘; 存储选型在本地盘(快但绑死节点), 网络盘(灵活可漂移), 分布式存储(高可用但重)间权衡; 而"数据库该不该上 K8s"--答案是"看情况", 但要先想清备份与运维这两道坎.

前置回顾 · 全系列收束

这是云原生网络与存储的最后一篇, 也是把全部知识落地的一篇. 前面我们打通了网络(01-05: 从 veth 到 CNI 到零信任)和存储(06-07: 从存储抽象到 CSI 三段式).

现在回到最初的动机--让有状态应用在这个为"无状态, 易逝"而生的系统里安全地保存数据. 本篇把网络, 存储, 调度的知识拧成真实的选型决策.

有状态应用为什么是 K8s 的"硬骨头"

K8s 天生为无状态应用设计: Deployment 管理一堆完全对等, 可随意替换的 Pod--挂一个补一个, 谁是谁无所谓. 但有状态应用(数据库, 消息队列, 分布式存储)有三个 Deployment 满足不了的诉求:

无状态(Deployment 擅长)          有状态(需要 StatefulSet)
───────────────────── ─────────────────────────
Pod 完全对等, 可互换 vs 每个实例有身份(主库 / 从库 不一样)
随便扩缩, 随便重建 vs 启动/缩容要有序(先起主, 再起从)
不需要稳定的网络标识 vs 需要稳定的网络名(从库要一直找得到主库)
共享或无持久存储 vs 每个实例要独占自己的那块数据盘

这三个诉求--稳定身份, 有序操作, 独立存储--正是 StatefulSet 这个工作负载(Kubernetes 笔记里见过)存在的理由. 本篇聚焦它和存储结合的那一面.

StatefulSet + volumeClaimTemplates: 每个副本一块独立盘

关键机制是 volumeClaimTemplates(卷申领模板). 普通 Deployment 如果引用一个 PVC, 所有副本会共用同一个 PVC(对数据库是灾难--多个实例写同一份数据文件会损坏). StatefulSet 则用模板为每个副本自动创建一个独立的 PVC:

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 3
template:
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates: # ← 不是固定的 PVC, 而是"模板"
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"] # 每块盘单节点独占(第 06 篇)
storageClassName: fast-ssd
resources:
requests:
storage: 20Gi

这个模板的效果是: K8s 给每个副本生成一块专属, 稳定绑定的盘:

postgres-0  →  PVC: data-postgres-0  →  独立的 20Gi 盘
postgres-1 → PVC: data-postgres-1 → 另一块独立 20Gi 盘
postgres-2 → PVC: data-postgres-2 → 又一块独立 20Gi 盘

关键: postgres-0 这个 Pod 重建后, 仍然绑回 data-postgres-0 这块盘
→ 实例身份(序号)和它的数据盘是终身绑定的, 数据不会错乱

稳定身份 + 专属存储 = 有状态的根基

StatefulSet 的精髓: Pod 有稳定的序号身份(postgres-0/1/2, 而非 Deployment 那种随机后缀), 每个身份终身绑定自己的 PVC.

于是 postgres-0 不管重建多少次, 漂移到哪个节点(只要存储能跟过去, 第 07 篇), 都还是"那个有自己数据的 0 号实例". 这就把第 06 篇"数据活过容器"从单个 Pod 推广到了"一组有身份的实例各自的数据都不丢, 不乱"--这是跑数据库集群的地基.

存储选型: 本地 vs 网络 vs 分布式

给有状态应用选什么底层存储(即第 06 篇 StorageClass 背后是什么), 是核心决策. 三类选择, 又是一组经典权衡:

类型 是什么 优点 致命短板 适合
本地存储(Local PV) 直接用节点本机的磁盘 性能最高(本地 NVMe, 无网络开销) 绑死在那台节点, Pod 不能漂移到别的节点; 节点挂了数据就危险 对性能极致敏感, 且应用层自己做了多副本(如 Kafka, ES)
网络块存储(云盘等) 云厂商的 EBS/PD 等, 通过网络挂载 跟随 Pod 漂移(detach/attach, 第 07 篇); 云厂商保证盘本身的可靠性 性能不如本地; 仍是 RWO 单节点 绝大多数有状态应用的默认选择
分布式存储(Ceph/Rook 等) 跨多节点的存储集群, 数据多副本 高可用, 支持 RWX, 容量弹性, 能在自建环境提供"类云盘"能力 : 自己运维一套分布式存储系统, 复杂度高 自建机房, 需要 RWX 或统一存储平台

本地存储的诱惑与陷阱

本地 NVMe 性能诱人, 但它和云原生的"Pod 可漂移"理念根本冲突: 数据绑死在节点 X 上, 那个 Pod 就只能调度回节点 X(第 07 篇: 本地盘没法跨节点 attach). 一旦节点 X 宕机, 这个实例和它的数据就一起趴窝.

所以用本地存储的前提是--应用层自己有多副本和数据冗余(像 Kafka/ES/Cassandra 这种本就在应用层做了分片复制的), 单个节点挂了不影响整体. 如果是单实例数据库用本地盘, 等于把鸡蛋全放一个会随时摔碎的篮子里.

Rook: 在 K8s 上跑 Ceph 的'运维大脑'

自建环境想要"类云盘"的网络/分布式存储, 常用 Ceph(强大的开源分布式存储). 但 Ceph 运维复杂, 于是有了 Rook--它是一个 K8s Operator, 用声明式方式把 Ceph 的部署, 扩容, 故障恢复自动化(声明"我要一个 Ceph 集群", Rook 帮维护).

Rook 让 Ceph 通过 CSI(第 07 篇)给集群提供块/文件/对象三种存储. 这又是声明式 + Operator 模式(Kubernetes 笔记)的应用--把复杂运维知识沉淀进控制器.

灵魂拷问: 数据库到底该不该上 Kubernetes

这是有状态领域最有争议的问题. 先给结论: 没有标准答案, 取决于运维能力和数据库类型, 但要先想清两道坎.

两种立场, 各有道理:

支持上 K8s:                      谨慎/反对:
· 统一用一套编排管理所有负载 · 数据库是"皇冠上的明珠", 出事代价极高
· Operator 能自动化主从/备份/故障转移 · 有状态运维(备份/恢复/迁移)远比无状态难
· 开发自助, 环境一致 · 性能敏感场景, 存储抽象层有损耗
· 云原生生态(监控/GitOps)天然集成 · 团队若缺 K8s + DB 双重运维能力, 风险高

务实的判断框架

与其纠结立场, 不如问自己几个问题:

  1. 有没有靠谱的 Operator?(如 CloudNativePG, Zalando postgres-operator--它们把主从复制, 故障转移, 备份自动化了, 没有 Operator 裸跑数据库风险很大)
  2. 团队的备份/恢复演练做到位了吗?(见下节)
  3. 是核心交易库还是边缘库?(核心库求稳, 可以先用云托管 RDS; 边缘库, 开发测试库上 K8s 试水)

趋势是成熟 Operator 让"数据库上 K8s"越来越可行, 但它要求的运维成熟度, 远高于跑无状态应用. 别因为"云原生"就盲目把核心数据库搬上去.

绕不开的最后一道坎: 备份

无论数据库在不在 K8s 上, 有状态应用都有一条铁律: 持久化(Persistence)不等于备份(Backup). 这是最致命的误区之一:

PV 把数据存下来了 ≠ 数据安全了

PV/PVC 解决的是"数据活过容器重建"(第 06 篇), 但它救不了这些: 误删了一张表(数据还在盘上, 但内容错了), 误删了 PVC 且回收策略是 Delete(第 06 篇, 盘连数据一起没了), 存储后端整体故障, 勒索软件加密.

这些都需要"另一份独立的, 时间点的副本"--也就是备份. 持久化是"防止容器重启丢数据", 备份是"防止数据本身出错或丢失", 两者解决完全不同的问题, 缺一不可.

K8s 环境下的备份手段, 正好用上前面的知识:

  • 卷快照(第 07 篇 CSI Snapshot): 给数据盘拍时间点快照, 可快速恢复或克隆. 注意快照通常和原卷在同一存储后端, 不能替代异地备份.
  • 应用层逻辑备份: 如 pg_dump 导出数据库, 存到对象存储(第 06 篇, S3 这类异地, 便宜, 耐久).
  • 专用工具: 如 Velero(备份整个 K8s 资源 + PV 数据), 各数据库 Operator 自带的备份能力.

没演练过恢复的备份, 等于没有备份

这是 SRE 的血泪经验(呼应可观测性系列第 08 篇的复盘文化): 很多团队"有备份", 但从没试过恢复--真出事时才发现备份是坏的, 流程没人会, 恢复要十几个小时.

备份的价值不在"备了", 而在"能在可接受时间内恢复". 定期做恢复演练, 把它写进 runbook, 才是真正的数据安全.

全系列收束: 两套对称的可插拔体系

回望这八篇, 云原生的网络与存储其实讲的是同一个故事的两半:

网络这一半:                          存储这一半:
01 内核原语(namespace/veth/网桥) 06 存储抽象(块/文件/对象, PV/PVC)
02 跨节点(Overlay vs 路由) ──对称──
03 CNI 接口外包给插件 ◀═══════▶ 07 CSI 接口外包给插件
04 eBPF/Cilium 数据平面 (三段式 provision/attach/mount)
05 NetworkPolicy 零信任隔离
╲ ╱
╲ ╱
08 在有状态应用上, 网络与存储一起落地
(StatefulSet 要稳定网络名 + 独立存储盘)

贯穿始终的是那条主线: Kubernetes 自己不实现网络和存储, 而是定义 CNI / CSI 标准接口, 把实现外包给可插拔的插件. 正是这套"机制与策略分离"的设计, 让 K8s 核心保持精简, 而网络与存储生态各自繁荣, 自由竞争与替换. 所有眼花缭乱的选择(Flannel/Calico/Cilium, EBS/Ceph/NFS), 本质都是同一个接口下的不同实现--抓住接口, 就抓住了不变的骨架.

快速回顾

  • 有状态的三诉求: 稳定身份, 有序操作, 独立存储--Deployment 满足不了, 要用 StatefulSet
  • volumeClaimTemplates: 为每个副本自动建独立 PVC, 实例序号与数据盘终身绑定(postgres-0 永远绑 data-postgres-0), 数据不乱
  • 存储选型: 本地盘(最快但绑死节点, 需应用层多副本) / 网络块存储(可漂移, 默认选择) / 分布式如 Ceph+Rook(高可用支持 RWX 但运维重)
  • 数据库上 K8s: 无标准答案, 关键看有无成熟 Operator, 备份演练是否到位, 是否核心库; Operator 让它越来越可行但运维门槛高
  • 持久化 ≠ 备份: PV 防容器重启丢数据, 救不了误删/逻辑错误/后端故障; 需独立时间点备份(CSI 快照/pg_dump 到对象存储/Velero)
  • 没演练过恢复的备份等于没有; 全系列主线--CNI/CSI 把网络与存储外包给可插拔插件, 机制/策略分离

StatefulSet 缩容(比如 3 副本减到 1)时, 那些 PVC 和数据会被自动删掉吗?

默认不会--这是个贴心的安全设计. StatefulSet 缩容时, 删掉的 Pod 对应的 PVC 默认保留(不自动删除), 数据还在. 这样误缩容, 或之后想扩回来时, 数据能复用, 不丢.

代价是会留下"孤儿 PVC"占用存储, 需要确认数据不要了之后手动清理.(较新版本 K8s 提供了 persistentVolumeClaimRetentionPolicy 可配置缩容/删除时是否自动清 PVC, 但默认仍偏保守保留.) 对有状态应用, "宁可留着也别自动删数据"是正确的默认.

既然数据库上 K8s 这么麻烦, 为什么不直接都用云托管数据库(RDS)?

云托管(RDS/CloudSQL 等)确实是很多场景的省心之选--云厂商替我们搞定高可用, 备份, 补丁. 该不该用它, 权衡点是:

  1. 用云托管省运维, 但成本通常更高, 被云厂商绑定, 灵活性受限(版本, 插件, 参数)
  2. 自己在 K8s 上跑则相反--更可控, 可移植(混合云/自建), 能和应用同栈管理, 但要自担运维

常见策略: 核心生产库用云托管求稳, 非核心库/有多云诉求/自建机房的场景用 K8s. 没有绝对优劣, 这又是一道"省心 vs 可控"的权衡--和本系列(乃至 CI/CD, 可观测性)反复出现的选型逻辑一脉相承.

动手练习

  1. 验证每副本独立 PVC: 用 StatefulSet 部署一个 3 副本的有状态应用(如 PostgreSQL 或简单的 nginx+PVC), 观察 kubectl get pvc: 确认每个副本各有一个独立 PVC(data-xxx-0/1/2)
  2. 验证身份与数据绑定: 往 xxx-0 的数据目录写点东西, 删除 xxx-0 这个 Pod, 等它重建后确认数据还在, 且仍绑回 data-xxx-0--验证"身份与数据终身绑定"
  3. 判断存储类型: 查看集群默认 StorageClass 背后是哪类存储(本地/网络/分布式), 用本篇的权衡表判断它适合什么场景
  4. 亲手验证备份: 用 CSI 快照(第 07 篇)给数据盘拍一个快照, 然后故意删掉数据, 再从快照恢复--亲手验证"备份能救命", 并体会"持久化 ≠ 备份"
  5. 实际选型分析: 写一段话回答: 团队现在的(或计划中的)数据库, 该上 K8s 还是用云托管? 用本篇的判断框架(Operator, 备份演练, 核心与否)给出理由