Kubernetes 存储体系¶
作者:JiangChong | 撰写时间:2026年07月
✅ 学完本文你将能够: ✅ 区分临时卷、PV/PVC 和 StorageClass 的不同用途 ✅ 理解静态供给和动态供给的区别 ✅ 了解 CSI 如何标准化 K8s 的后端存储接入 ✅ 掌握 StatefulSet 在有状态应用中的存储模式
前置阅读:请先读 Kubernetes 核心架构 和 Kubernetes Pod 与 Deployment 实战,了解 Pod 的基本概念。本文假设你了解 Linux 文件系统的基本概念(目录、挂载点)。
0. 本文概览¶
02 篇学会了把应用跑起来,但有一个致命问题:Pod 里的数据去哪了?Pod 删了数据还在吗?部署数据库怎么办?
存储的本质是回答三个问题:
容器重启 → 数据还在吗? → emptyDir(2.1:Pod 内临时存储)
Pod 删了 → 数据还在吗? → PV/PVC(3:持久化存储)
多副本 → 各自的数据怎么分开? → StatefulSet(6:有状态存储模式)
本文沿这个线索,从最简单的「写个文件试试」到「数据库跑在 K8s 上」。
1. 存储层次概述¶
Kubernetes 的存储模型分为三个层次:
┌────────────────────────────────────────────┐
│ PersistentVolume(PV)+ Claim(PVC) │ ← 持久化存储抽象
│ ┌───────────────────────────────────────┐│
│ │ StorageClass(SC) ││ ← 动态 provisioning 模板
│ └───────────────────────────────────────┘│
│ ┌───────────────────────────────────────┐│
│ │ CSI Driver(卷插件) ││ ← 与真实存储系统对接
│ └───────────────────────────────────────┘│
│ ┌───────────────────────────────────────┐│
│ │ Volume 挂载(Pod/Container) ││ ← 应用视角
│ └───────────────────────────────────────┘│
└────────────────────────────────────────────┘
一个不完美的但好理解的类比: PV 相当于一块买好的硬盘,PVC 相当于你填写的"我需要 500GB 硬盘"的申请单,StorageClass 相当于硬盘产品目录(告诉系统用什么品牌、什么型号的硬盘)。管理员负责准备 PV,用户只需提交 PVC 申请。
2. Volume 类型¶
2.1 临时卷(Ephemeral Volume)— 动手实操¶
临时卷与 Pod 生命周期绑定:Pod 在数据在,Pod 删数据丢。但容器重启不影响数据——这个区别很重要。
2.1.1 emptyDir¶
emptyDir: Pod 启动时创建空目录,容器崩溃重启数据保留,Pod 删除时清理。
动手验证「容器重启数据保留」vs「Pod 删除数据丢失」。
# empty-dir-demo.yaml
apiVersion: v1
kind: Pod
metadata:
name: emptydir-test
spec:
containers:
- name: nginx
image: nginx:1.25 # 用已有的 nginx 镜像,nginx -s stop 可以方便停进程
imagePullPolicy: IfNotPresent # 带有明确版本号的时候。默认策略为 IfNotPresent(本地有就直接用); :latest 默认是 Always(每次都从仓库拉),改成 IfNotPresent 优先用本地缓存
command: ['sh', '-c', 'exec nginx -g "daemon off;"']
volumeMounts:
- name: demo-vol
mountPath: /data
volumes:
- name: demo-vol
emptyDir: {}
kubectl apply -f empty-dir-demo.yaml
# pod/emptydir-test created
# 1. 手动写文件,验证存在
kubectl exec emptydir-test -- sh -c 'echo "hello-vol" > /data/msg.txt'
kubectl exec emptydir-test -- cat /data/msg.txt # hello-vol
# 2. 模拟容器崩溃
kubectl get pods | grep emptydir-test # RESTARTS 列当前是 0
kubectl exec emptydir-test -- nginx -s stop # 优雅停止 nginx → 容器退出 → kubelet 自动重启
# 等几秒容器重启后:
kubectl get pods | grep emptydir-test # RESTARTS 变成 1 ← 铁证:容器被重启了
kubectl exec emptydir-test -- cat /data/msg.txt # 还在!容器重启了但数据没丢
# 3. 删除 Pod
kubectl delete pod emptydir-test
kubectl apply -f empty-dir-demo.yaml # 重建同名 Pod
kubectl exec emptydir-test -- cat /data/msg.txt # 没了!Pod 删了数据也丢了
# cat: /data/msg.txt: No such file or directory
# command terminated with exit code 1
2.1.2 hostPath¶
hostPath: 和 emptyDir 刚好相反——数据存在宿主机上,Pod 删了数据还在。动手对比:
# hostpath-demo.yaml
apiVersion: v1
kind: Pod
metadata:
name: hostpath-test
spec:
containers:
- name: nginx
image: nginx:1.25
imagePullPolicy: IfNotPresent
command: ['sh', '-c', 'exec nginx -g "daemon off;"']
volumeMounts:
- name: host-vol
mountPath: /data
volumes:
- name: host-vol
hostPath:
path: /tmp/k8s-hostpath-demo
type: DirectoryOrCreate
# 1. 写文件 + 验证
kubectl apply -f hostpath-demo.yaml
kubectl exec hostpath-test -- sh -c 'echo "hostpath-data" > /data/msg.txt'
docker exec k8s-playground-control-plane cat /tmp/k8s-hostpath-demo/msg.txt
# hostpath-data
# 2. 删 Pod 重建——和 emptyDir 的结果相反!
kubectl delete pod hostpath-test
docker exec k8s-playground-control-plane cat /tmp/k8s-hostpath-demo/msg.txt # 删掉Pod后,文件还在
# hostpath-data
kubectl apply -f hostpath-demo.yaml # 重建同名 Pod
kubectl exec hostpath-test -- cat /data/msg.txt # hostpath-data ← 还在!数据在宿主机 /tmp/k8s-hostpath-demo/ 里
# 3. 直接在节点上验证
# /tmp/k8s-hostpath-demo/ 在 K8s 节点上(不是你的 Debian 宿主机,也不是 Pod 内)
# Kind 集群的节点是 Docker 容器,这样进去看:
docker exec k8s-playground-control-plane cat /tmp/k8s-hostpath-demo/msg.txt
# hostpath-data ← 数据在节点文件系统上,不在 Pod 里
# 进一步验证:文件在节点上,Pod 删了也不影响
docker exec k8s-playground-control-plane ls /tmp/k8s-hostpath-demo/
# msg.txt ← Pod 已删,但文件在节点文件系统上纹丝不动
清理:
kubectl delete pod hostpath-test
| 对比 | emptyDir | hostPath |
|---|---|---|
| 数据存哪 | Pod 内部 | 宿主机文件系统(docker) |
| Pod 删了 | ❌ 数据丢 | ✅ 数据在 |
| 容器重启 | ✅ 数据在 | ✅ 数据在 |
| 风险 | 无 | Pod 飘到其他节点就找不到了 |
hostPath 的风险: 数据绑定在特定节点上,Pod 被调度到其他节点就访问不到了。此外 root 容器可通过 hostPath 访问宿主机任意路径。生产环境用 Local PV 替代——功能和 hostPath 一样,但受 PVC 绑定保护。
hostPath vs
localPV: 两者都写节点本地磁盘,区别如下。你在 3.1 用 hostPath 做 PV 是因为 Kind 单节点无所谓调度。生产环境 Vertica 的 catalog/depot 用local+nodeAffinity,保证 Pod 和数据永远在同一节点:| | hostPath |
localPV ||---|---|------|
| 能用于 PV | ✅ | ✅ |
| 必须指定节点 | ❌ 不强制 | ✅ 必须配
nodeAffinity|| Scheduler 感知 | ❌ 可能调度到错误节点 | ✅ Scheduler 知道 PV 在哪个节点 |
| 动态供给 | ❌ | ✅(需 provisioner) |
| 场景 | 开发/测试 | 生产数据库 |
3. PersistentVolume 与 PersistentVolumeClaim¶
从 emptyDir 到 PV: 上面 emptyDir 实验说明:Pod 删了数据就丢。那数据库怎么办?答案是把存储从 Pod 里「提出来」,让它独立于 Pod 存在。这就是 PV/PVC 的设计思路。
PV 和 PVC 将存储的"提供"和"消费"解耦——跟租房一样:房东提供房子(PV),租客提出需求(PVC),中介(K8s)自动匹配。
3.1 动手实操:hostPath PV¶
# pv-demo.yaml
# 管理员视角:创建 PV(集群级别,不属于任何 Namespace)
apiVersion: v1
kind: PersistentVolume
metadata:
name: demo-pv
spec:
capacity:
storage: 1Gi # K8s 用二进制单位:1Gi = 1024³ ≈ 1.07GB
accessModes:
- ReadWriteOnce # 同一时刻只有一个节点能读写,本地盘、云盘(EBS),数据库数据目录、单实例应用
hostPath:
path: /tmp/k8s-demo-pv # 宿主机上的实际路径
---
# 用户视角:创建 PVC 申请存储
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: demo-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Mi
storageClassName: "" # 空字符串 = 只用静态 PV,不触发动态创建,如果不加这个,则会动态创建一个新的 PV
---
# Pod 使用 PVC
apiVersion: v1
kind: Pod
metadata:
name: pv-demo
spec:
containers:
- name: nginx
image: nginx:1.25
imagePullPolicy: IfNotPresent
command: ['sh', '-c', 'exec nginx -g "daemon off;"']
volumeMounts:
- name: my-vol
mountPath: /data
volumes:
- name: my-vol
persistentVolumeClaim:
claimName: demo-pvc
kubectl apply -f pv-demo.yaml
# 1. 看绑定状态:PVC 自动找到了 PV
kubectl get pv,pvc
# NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
# persistentvolume/demo-pv 1Gi RWO Retain Bound default/demo-pvc <unset> 3s
# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
# persistentvolumeclaim/demo-pvc Bound demo-pv 1Gi RWO <unset> 3s
# 2. 手动写入 + 验证持久
kubectl exec pv-demo -- sh -c 'echo "persistent!" > /data/msg.txt'
kubectl exec pv-demo -- cat /data/msg.txt # persistent!
kubectl delete pod pv-demo # 删 Pod
docker exec k8s-playground-control-plane cat /tmp/k8s-demo-pv/msg.txt
# persistent!
kubectl apply -f pv-demo.yaml # 重建(新 Pod,但绑定了同一个 PVC)
kubectl exec pv-demo -- cat /data/msg.txt # 还在!数据存在 PV 上,不跟 Pod 走
清理:
kubectl delete pod pv-demo && kubectl delete pvc demo-pvc && kubectl delete pv demo-pv
storage: 1Gi占用实际空间吗? 看 PV 类型。hostPath PV 的容量只做上限标记,不真正预留空间(写多少才占多少);云盘 PV(如 AWS EBS)则会真的创建一块等大的云盘并计费。你在 Kind 上用的 hostPath,不影响磁盘。
3.2 访问模式¶
PV 创建时声明的 accessModes 决定了多少个 Pod/节点能同时使用这块存储。不是所有模式能用所有存储类型:
| 模式 | 含义 | 支持的存储 | 典型场景 |
|---|---|---|---|
| RWO (ReadWriteOnce) | 同一时刻只有一个节点能读写 | 本地盘、云盘(EBS) | 数据库数据目录、单实例应用 |
| ROX (ReadOnlyMany) | 多个节点只读挂载 | NFS、CephFS、S3 | 共享配置文件、静态资源 |
| RWX (ReadWriteMany) | 多个节点同时读写 | NFS、CephFS、EFS | 多 Pod 共享缓存、日志 |
| RWOP (ReadWriteOncePod) | 同一时刻只有一个 Pod能读写(v1.22+) | 同 RWO | RWO 的加强版——即使同节点也不能两个 Pod 同时用 |
为什么本地盘不能用 RWX? 本地磁盘物理上只连着一台机器。要让多台机器同时读写同一块盘,必须用网络文件系统(NFS、Ceph)或云共享存储(AWS EFS)。
PV 可以声明多个模式(如 [RWO, RWX]),表示这块存储能提供这几种访问方式。绑定规则:PV 的模式列表必须包含 PVC 要的模式。
能声明多个模式的只有网络存储(NFS、CephFS),因为它们天然支持多节点同时访问。本地盘和云盘(EBS)只能声明
[RWO]。
3.3 Static vs Dynamic Provisioning — 两种供给方式¶
前面 3.1 的实操用的是静态供给(手动建 PV)。但还有另一种方式——让 K8s 自动建 PV。这就是「静态供给」和「动态供给」的区别。
类比: 静态供给 = 自己买硬盘装进机器(管理员手工创建 PV);动态供给 = 在云平台上选「我要 500GB SSD」→ 云平台自动创建一块盘给你(StorageClass + Provisioner 自动创建 PV)。
3.3.1 Static Provisioning - 静态供给¶
静态供给:管理员先创建 PV → 用户提交 PVC → K8s 匹配绑定。你在 3.1 做的就是这个流程。
# 管理员创建 PV(手动指定容量、路径)
apiVersion: v1
kind: PersistentVolume
metadata:
name: vertica-local-pv
spec:
capacity:
storage: 500Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain # PVC 删了后 PV 保留,数据不丢。Delete 则自动删 PV+数据
local: # 在 3.1 用 hostPath 做 PV 是为了简单(本地单节点无调度问题)。生产环境的 Vertica catalog/depot 应该用 `local` + `nodeAffinity`,确保 Pod 和数据始终在同一节点。
path: /mnt/ssd/vertica
nodeAffinity: # 限制这个 PV 只能在 node-1 上使用
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- node-1
---
# 用户创建 PVC(必须写 storageClassName: "" 才能匹配静态 PV)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: vertica-catalog-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Gi
storageClassName: "" # 空字符串 = 只用静态 PV
3.3.2 Dynamic Provisioning - 动态供给¶
动态供给:用户只提交 PVC → Provisioner 自动创建 PV → 绑定。管理员不用提前准备任何 PV。
你其实已经见过了! 3.1 实操创建 PVC 时如果不写
storageClassName: "",Kind 内置的standardStorageClass 会自动替你创建了一个 PV。那个pvc-424b7b70...就是动态供给的产物。你只提交了 PVC,PV 是自动冒出来的——这就是动态供给的核心。
# pvc.yaml
# 管理员只需创建一次 StorageClass(模板),不需为每次申请创建 PV
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com # Provisioner = 实际调云 API 创建盘的程序。不同云用不同 driver
parameters:
type: gp3
iopsPerGB: "3000"
fsType: ext4
allowVolumeExpansion: true
---
# 用户只需创建 PVC,PV 自动创建
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: vertica-depot-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd # 引用上面的 StorageClass → 触发动态创建
resources:
requests:
storage: 200Gi
动态供给的完整流程:
kubectl apply -f pvc.yaml(storageClassName: fast-ssd, storage: 200Gi)
↓
K8s 找到 fast-ssd 这个 StorageClass
↓
provisioner: ebs.csi.aws.com ← 把创建任务交给这个驱动
↓
CSI 驱动调 AWS API:
createVolume(type=gp3, iopsPerGB=3000, fsType=ext4, size=200Gi)
↑ ↑ ↑ ↑ ↑
parameters.type parameters parameters 模板写死 PVC的请求
↓
AWS 创建一块 200Gi 的 EBS 云盘 → 自动挂载到 K8s 节点
↓
PV 对象自动出现在 K8s 里 → 绑定 PVC → Pod 通过 volumeMount 读写
parameters 里的字段全部传给 Provisioner,由它决定怎么创建实际的存储。不同 Provisioner 支持不同的 parameters——EBS 有 type/iopsPerGB,Ceph 可能有 pool/imageFeatures,每个驱动自己定义。
| 静态供给 | 动态供给 | |
|---|---|---|
| 谁创建 PV | 管理员手动 | Provisioner 自动 |
| PVC 怎么写 | storageClassName: "" |
storageClassName: fast-ssd |
| 适用场景 | 本地盘、预分配硬件 | 云环境(EBS、EFS) |
| 你之前踩坑 | 不加 "" 就被 standard 动态创建了 |
Kind 内置的 standard SC |
4. StorageClass — 存储模板¶
为什么需要 StorageClass? 3.3 节动态供给的流程里,Provisioner 需要一个「说明书」才知道创建什么规格的盘。StorageClass 就是这个说明书——管理员创建一次,后面所有人创建 PVC 时引用它就能自动获得对应规格的存储。
回到第 1 节的类比:PV = 硬盘,PVC = 申请单,StorageClass = 硬盘产品目录(告诉系统用什么品牌、什么型号、什么参数)。
你集群里已经有 StorageClass 了——Kind 安装时就带了一个默认的:
kubectl get sc
# NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
# standard (default) rancher.io/local-path Delete WaitForFirstConsumer false 110m
就是这个 standard 在 3.1 帮你动态创建了 pvc-424b7b70...。
4.1 一个完整的 StorageClass¶
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com # 用哪个 CSI 驱动
parameters: # 驱动专属参数,传给 Provisioner
type: gp3 # → AWS:创建 gp3 类型
iopsPerGB: "3000" # → 每 GB 配 3000 IOPS
fsType: ext4 # → 格式化为 ext4
allowVolumeExpansion: true # 允许在线扩容(只能扩不能缩)
volumeBindingMode: WaitForFirstConsumer # 等 Pod 调度后再创建盘
reclaimPolicy: Retain # PVC 删了盘保留
4.2 关键字段详解¶
| 字段 | 作用 | 怎么选 |
|---|---|---|
provisioner |
指定用哪个 CSI 驱动 | AWS 用 ebs.csi.aws.com,Ceph 用 rbd.csi.ceph.com |
parameters |
传给驱动的创建参数 | 每个驱动不同。gp3/io1 是 EBS 的,pool/imageFeatures 是 Ceph 的 |
volumeBindingMode |
何时创建 PV | WaitForFirstConsumer = 等 Pod 调度后再建盘(保证同可用区,推荐);Immediate = PVC 创建就建盘(可能跨可用区) |
reclaimPolicy |
PVC 删了后 PV 怎么办 | Retain = 保留(数据库推荐);Delete = 连盘带数据全删 |
allowVolumeExpansion |
能否在线扩容 | true 后 PVC 的 storage 可以改大,不能改小 |
5. CSI — 容器存储接口¶
为什么需要 CSI? 前面动态供给的流程图里,Provisioner 负责调云 API 创建盘。但这个 Provisioner 具体怎么和 K8s 对接?如果没有统一标准,AWS 写一套、Ceph 写一套、NFS 又写一套——K8s 代码库会变成存储驱动的垃圾场。CSI 定义了一套标准接口,任何存储厂商只要按这个接口实现驱动,就能接入 K8s。
一句话: CSI 就是前面动态供给流程图里「Provisioner」这一环的标准实现方式。你看到的 ebs.csi.aws.com、rbd.csi.ceph.com 都是 CSI 驱动。
5.1 CSI 做了什么¶
不用深究架构图,从你已知的 PVC 创建流程来看 CSI 的两项核心工作:
工作一:创建卷(Controller)
PVC 创建 → external-provisioner 调 CSI → CSI 调云 API 创建盘(你已理解这步)
工作二:挂载卷(Node)
Pod 调度到节点 → kubelet 通知 CSI Node Plugin → CSI 把盘挂到 Pod 的 mountPath
三个关键组件:
| 组件 | 一句话角色 |
|---|---|
external-provisioner |
「建盘的人」:PVC 来了 → 调 CSI 创建后端存储 |
external-attacher |
「接线的人」:把建好的盘挂到节点,Pod 才能访问 |
node-driver-registrar |
「报到的」:告诉 kubelet「我这个 CSI 驱动可用了」 |
初学者不需要深究每个组件怎么部署。 你只需要知道:CSI = 存储厂商接入 K8s 的标准插头。当你写
provisioner: ebs.csi.aws.com时,背后就是一套 CSI 驱动在工作。
5.2 常见 CSI 驱动¶
| 存储类型 | CSI Driver | 特点 |
|---|---|---|
| AWS EBS | ebs.csi.aws.com |
RWO,gp3/io1,支持快照和扩容 |
| AWS EFS | efs.csi.aws.com |
RWX,容量自动扩展 |
| GCP Persistent Disk | pd.csi.storage.gke.io |
RWO,支持区域级 PD |
| Ceph RBD | rbd.csi.ceph.com |
RWO,高可用 |
| CephFS | cephfs.csi.ceph.com |
RWX |
| NFS | nfs.csi.k8s.io |
RWX,通用性最强 |
| Local PV | local-volume-provisioner |
RWO,高性能本地盘 |
6. StatefulSet — 有状态应用¶
为什么不能用 Deployment 部署数据库? 02 篇的 Deployment 设计目标是「无状态」——所有 Pod 完全一样,删一个立刻新建一个替代品,Pod 名叫
<deploy>-<random-hash>(每次重建名字都变)。但数据库不行:每个实例有自己的数据,不能互换;重启后必须找回原来的那卷数据;集群成员之间需要稳定的网络标识互相找到对方。StatefulSet 就是为这个场景设计的。
6.1 Deployment vs StatefulSet¶
| Deployment(02 篇) | StatefulSet(本节) | |
|---|---|---|
| Pod 身份 | 可互换、随机名 | 固定序号名,不可互换 |
| 存储 | 所有 Pod 共享或无 | 每个 Pod 独立 PVC |
| 扩缩容 | 任意顺序 | 按序号倒序删除、正序创建 |
| Pod 名 | nginx-deploy-7cd5cff77d-xxxxx |
vertica-0, vertica-1, vertica-2 |
| 典型应用 | Web 服务、API | 数据库、消息队列 |
6.2 动手实操:StatefulSet 的固定身份¶
用 nginx 演示——不需要数据库也能看懂核心机制:
# sts-demo.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-hl
spec:
clusterIP: None # Headless Service,不分配 VIP
selector:
app: nginx-sts
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nginx-sts
spec:
serviceName: nginx-hl # 绑定 Headless Service
replicas: 3
selector:
matchLabels:
app: nginx-sts
template:
metadata:
labels:
app: nginx-sts
spec:
containers:
- name: nginx
image: nginx:1.25
imagePullPolicy: IfNotPresent
volumeClaimTemplates: # 关键!每个 Pod 自动创建独立 PVC
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 100Mi
kubectl apply -f sts-demo.yaml
# 1. Pod 按 0→1→2 顺序创建,名字固定
kubectl get pods -o wide -l app=nginx-sts
# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# nginx-sts-0 1/1 Running 0 94m 10.244.0.14 k8s-playground-control-plane <none> <none> ← 名字永不变,删了重建也叫这个
# nginx-sts-1 1/1 Running 0 92m 10.244.0.19 k8s-playground-control-plane <none> <none>
# nginx-sts-2 1/1 Running 0 93m 10.244.0.18 k8s-playground-control-plane <none> <none>
# 2. 每个 Pod 有自己独立的 PVC
kubectl get pvc
# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
# data-nginx-sts-0 Bound pvc-4358aa95-18e5-42d1-93e3-820f75c47a45 100Mi RWO standard <unset> 58s
# data-nginx-sts-1 Bound pvc-bb1bd835-4b86-4555-99d9-4604ca9c0d1e 100Mi RWO standard <unset> 54s
# data-nginx-sts-2 Bound pvc-3ea7bed7-dd71-4496-b58c-5627c1f790c4 100Mi RWO standard <unset> 50s
# 3. 删 Pod,PVC 还在!Pod 重建后重新绑定同一个 PVC
kubectl delete pod nginx-sts-1
kubectl get pods -l app=nginx-sts # nginx-sts-1 自动重建,名字不变
# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# nginx-sts-0 1/1 Running 0 94m 10.244.0.14 k8s-playground-control-plane <none> <none>
# nginx-sts-1 1/1 Running 0 26s 10.244.0.25 k8s-playground-control-plane <none> <none> ← 名字永不变,删了重建也叫这个,但是 IP 会变
# nginx-sts-2 1/1 Running 0 93m 10.244.0.18 k8s-playground-control-plane <none> <none>
kubectl get pvc # data-nginx-sts-1 还在
清理:
kubectl delete sts nginx-sts && kubectl delete pvc -l app=nginx-sts && kubectl delete svc nginx-hl
6.3 volumeClaimTemplates — 每个 Pod 独立存储的关键¶
volumeClaimTemplates 是 StatefulSet 最核心的特性。它不是预先创建 PVC,而是定义了 PVC 的模板——StatefulSet 按这个模板为每个 Pod 自动创建一个 PVC:
volumeClaimTemplates:
- name: data
spec: { storage: 100Gi } ← 模板
实际创建的 PVC:
nginx-sts-0 → PVC: data-nginx-sts-0 (100Gi)
nginx-sts-1 → PVC: data-nginx-sts-1 (100Gi)
nginx-sts-2 → PVC: data-nginx-sts-2 (100Gi)
Pod 被删 → 新 Pod 用同一个名字重建 → 找到同名 PVC → 数据还在。这就是数据库能在 K8s 上跑的基础。
数据在哪?
volumeClaimTemplates只定义 PVC 模板,真正的物理路径由 PV 决定(走动态供给则是 StorageClass 的 provisioner 决定)。完整链路:volumeClaimTemplates 中 name: data → PVC: data-nginx-sts-0(storageClassName: standard) → StorageClass standard + provisioner: rancher.io/local-path → 在节点上创建目录 /var/local-path-provisioner/pvc-.../ → 挂载到 nginx-sts-0 的 mountPath实操验证每一步:
kubectl get pvc data-nginx-sts-0 -o jsonpath='{.spec.volumeName}' # 拿到 PV 名 # pvc-4358aa95-18e5-42d1-93e3-820f75c47a45 kubectl get pv pvc-4358aa95-18e5-42d1-93e3-820f75c47a45 -o yaml | grep path # 看到物理路径 # path: /var/local-path-provisioner/pvc-4358aa95-18e5-42d1-93e3-820f75c47a45_default_data-nginx-sts-0 # Kind 节点上的实际路径(不是 Debian 宿主机,不是 Pod 内) docker exec k8s-playground-control-plane ls -l /var/local-path-provisioner/pvc-4358aa95-18e5-42d1-93e3-820f75c47a45_default_data-nginx-sts-0
7. 数据库 on K8s 的存储考量¶
Vertica Eon on K8s 对存储的典型需求:
| 存储用途 | 推荐 | 不推荐 | 说明 |
|---|---|---|---|
| 公共存储(Communal) | S3 / MinIO / HDFS | hostPath | Eon 的核心存储。注意:HDFS 不能作为 K8s PV 挂载,Vertica 通过自带的 libhdfs++ 客户端直接访问,不经过 K8s 存储层 |
| 目录(Catalog) | Local SSD / EBS gp3 | NFS | 对延迟极其敏感,走 K8s PV |
| Depot(本地缓存) | Local SSD / 高性能本地盘 | 网络存储 | 读写频繁,IOPS 要求高,走 K8s PV |
7.1 性能建议¶
- Local SSD > 网络 SSD > HDD:数据库的写入性能对延迟高度敏感
- StorageClass 使用 WaitForFirstConsumer:确保 PV 与 Pod 在同一可用区/节点
- 考虑使用 Local PV:Vertica 的 depot 和 catalog 用本地 NVMe 盘可获得 IOPS 最优性能
- K8s 节点级磁盘规划:数据卷与系统卷分离,避免写入风暴影响 kubelet 和 etcd
- 文件系统选择:推荐 ext4 或 xfs,避免 ZFS 在 CSI 场景下的兼容性问题
7.2 VerticaDB Operator 的存储配置¶
关于 VerticaDB CR 中 storage 字段的详细配置,请参考 Vertica Eon on K8s 生产部署实战 的存储设计章节。
# VerticaDB CR 存储配置片段
spec:
volumes:
- name: depot-data
persistentVolumeClaim:
claimName: depot-pvc
volumeMounts:
- name: depot-data
mountPath: /opt/vertica/depot
8. 总结¶
| 概念 | 作用 | 使用频率 |
|---|---|---|
| emptyDir / hostPath | 临时数据、单节点调试 | 开发环境 |
| PV + PVC | 持久化存储的供给与消费解耦 | 所有生产环境 |
| StorageClass | 动态供给模板和策略管理 | 所有云环境 |
| CSI Driver | 存储后端的标准化接入 | 所有生产环境 |
| StatefulSet + volumeClaimTemplates | 有状态应用的存储模式 | 数据库、消息队列 |
→ 下一篇文章: Kubernetes 网络模型 — 理解 K8s 的扁平网络模型、Service 和 Ingress。