Files

626 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [k8s, storage, pv, pvc, statefulset, devops]
create time: 2026-07-07 15:48
---
# Kubernetes 存储 — PV、PVC 与 StatefulSet
## 概述
容器的本质是短暂的(ephemeral):Pod 被删除后,其内部所有文件随之消散。这对无状态服务无关紧要,但对数据库、消息队列等有状态应用来说是致命的。Kubernetes 通过分层存储抽象 —— **Volume → PersistentVolume(PV)→ PersistentVolumeClaim(PVC)→ StorageClass** —— 将"存储供给"与"存储消费"解耦,让开发者无需关心底层基础设施,只需声明"我需要多大、什么访问模式的存储",即可自动获得持久化能力。配合 StatefulSet,K8s 还为有状态应用提供了稳定的网络标识和有序的生命周期管理。
## 卷(Volume)基础
Volume 是 K8s 最早引入的存储概念,与 Pod 同生命周期(除非是持久卷类型)。它定义在 Pod spec 中,可被同一 Pod 内的多个容器共享。
### 常见卷类型
| 卷类型 | 生命周期 | 典型用途 | 是否持久 |
|--------|----------|----------|----------|
| `emptyDir` | Pod 存续期间 | 容器间共享临时文件、缓存 | 否 |
| `hostPath` | 节点存续期间 | 访问宿主机文件(如 Docker socket) | 节点级持久 |
| `nfs` | 独立于 Pod | 多节点共享存储 | 是 |
| `configMap` / `secret` | Pod 存续期间 | 注入配置和敏感信息 | 否 |
| `persistentVolumeClaim` | 独立于 Pod | 绑定 PV,正式的持久化方案 | 是 |
### emptyDir 示例
```yaml
apiVersion: v1
kind: Pod
metadata:
name: shared-data
spec:
containers:
- name: writer
image: busybox
command: ["sh", "-c", "echo hello > /data/msg && sleep 3600"]
volumeMounts:
- name: cache
mountPath: /data
- name: reader
image: busybox
command: ["sh", "-c", "cat /data/msg && sleep 3600"]
volumeMounts:
- name: cache
mountPath: /data
volumes:
- name: cache
emptyDir: {}
```
> [!tip] emptyDir 的 `medium: Memory` 配置可将其挂载为 tmpfs(RAM 磁盘),适用于对 I/O 延迟极敏感的场景,但会计入容器内存用量。
### hostPath 注意事项
hostPath 直接暴露宿主机目录,存在安全风险。生产环境应优先使用 PVC。如果必须使用,建议设置 `type` 字段限制行为:
```yaml
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
type: Socket # 仅允许已存在的 socket 文件
```
hostPath 的 `type` 可选值:`DirectoryOrCreate`、`Directory`、`FileOrCreate`、`File`、`Socket`、`CharDevice`、`BlockDevice`。
## 持久卷(PersistentVolume)
PersistentVolume(PV)是集群级别的存储资源,由管理员预先创建或由 StorageClass 动态供给。它与 Pod 完全解耦 —— Pod 删除后 PV 仍然存在。
### PV 详解
```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nfs-data
spec:
capacity:
storage: 10Gi # 存储容量
accessModes:
- ReadWriteMany # 访问模式
persistentVolumeReclaimPolicy: Retain # 回收策略
storageClassName: slow # 存储类名称
nfs:
server: 192.168.1.100
path: /exports/data
```
关键字段说明:
| 字段 | 说明 |
|------|------|
| `capacity.storage` | PV 的存储容量,只能声明一种资源(目前仅支持 storage) |
| `accessModes` | 定义卷的挂载方式,可声明多种,但实际只能使用一种 |
| `persistentVolumeReclaimPolicy` | PVC 释放后 PV 的处理策略:Retain / Delete / Recycle |
| `storageClassName` | 关联的 StorageClass 名称,为空字符串表示不关联任何类 |
| `volumeMode` | `Filesystem`(默认)或 `Block`(原始块设备) |
| `mountOptions` | 挂载选项,如 `["hard", "nfsvers=4.1"]` |
> [!info] PV 的状态包括 `Available`(未绑定)、`Bound`(已绑定到 PVC)、`Released`(PVC 已删除但 PV 未回收)、`Failed`(自动回收失败)。
### 回收策略(Reclaim Policy)
当 PVC 被删除后,PV 的回收策略决定了其后续行为。
| 策略 | 行为 | 适用场景 |
|------|------|----------|
| **Retain** | PV 状态变为 `Released`,数据保留,需管理员手动处理 | 有保留价值的数据 |
| **Delete** | PV 及底层存储资源一并删除 | 动态供给的临时存储 |
| **Recycle** | 执行 `rm -rf /volume/*`,PV 回到 `Available`(已废弃) | 仅兼容简单存储后端 |
```mermaid
stateDiagram-v2
[*] --> Available: PV 创建
Available --> Bound: PVC 绑定
Bound --> Released: PVC 删除
Released --> Available: Recycle (rm -rf)
Released --> [*]: Delete (删除底层存储)
Released --> Released: Retain (等待手动处理)
state "管理员手动操作" as manual
Released --> manual: 需人工介入
manual --> Available: 修复后重新上线
manual --> [*]: 彻底删除
```
> [!warning] Recycle 策略已被废弃,且仅对 NFS 和 hostPath 有效。生产环境应优先使用动态供给 + Delete 策略,或 Retain 策略配合手动运维。
### 访问模式(Access Modes)
| 模式 | 缩写 | 含义 |
|------|------|------|
| ReadWriteOnce | RWO | 单节点读写挂载 |
| ReadOnlyMany | ROX | 多节点只读挂载 |
| ReadWriteMany | RWX | 多节点读写挂载 |
| ReadWriteOncePod | RWOP | 单 Pod 读写(K8s 1.22+) |
各存储后端对访问模式的支持情况:
| 存储后端 | RWO | ROX | RWX | RWOP |
|----------|-----|-----|-----|------|
| 本地磁盘 (local) | 支持 | - | - | 支持 |
| AWS EBS | 支持 | - | - | 支持 |
| GCE PD | 支持 | 支持 | - | 支持 |
| Azure Disk | 支持 | - | - | 支持 |
| Azure File | 支持 | 支持 | 支持 | - |
| Ceph RBD | 支持 | 支持 | - | - |
| CephFS | 支持 | 支持 | 支持 | - |
| NFS | 支持 | 支持 | 支持 | - |
| Longhorn | 支持 | 支持 | 支持 | - |
> [!tip] 为什么不是所有存储都支持 RWX?因为多节点同时写入需要分布式文件系统级别的并发控制(如分布式锁、一致性协议)。块存储(EBS、PD)天然不支持多写,必须通过 NFS/文件系统层封装才能实现 RWX。
## 持久卷声明(PersistentVolumeClaim)
PVC 是用户对存储资源的"请求"。用户不直接创建或管理 PV,而是通过 PVC 声明需求(容量、访问模式、存储类),K8s 负责将合适的 PV 绑定到 PVC。
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
storageClassName: fast-ssd
```
### PVC 绑定机制
PVC 与 PV 的绑定有两种模式:
#### 静态供给(Static Provisioning)
管理员手动创建 PV 资源,用户创建 PVC 后,K8s 控制面中的 PV controller 根据容量、访问模式、存储类等条件进行匹配。
#### 动态供给(Dynamic Provisioning)
用户创建 PVC 后,若集群中没有匹配的 PV,且 PVC 指定了 StorageClass,K8s 会调用对应的 provisioner 自动创建 PV 并完成绑定。这是生产环境中最常用的方式。
```mermaid
flowchart TD
A[用户创建 PVC] --> B{是否有匹配的 Available PV?}
B -->|是| C[绑定 PV 到 PVC]
B -->|否| D{PVC 指定了 StorageClass?}
D -->|否| E[PVC 持续 Pending]
D -->|是| F[StorageClass Provisioner 创建 PV]
F --> C
C --> G[Pod 挂载 PVC 使用]
style E fill:#f96,stroke:#333
style G fill:#6f9,stroke:#333
```
绑定匹配规则:
1. PV 与 PVC 的 `storageClassName` 必须一致
2. PV 的 `accessModes` 必须包含 PVC 请求的模式
3. PV 的 `capacity.storage` 必须 >= PVC 请求的容量
4. PV 的 `volumeMode` 必须匹配
5. 若存在多个匹配 PV,优先选择容量最小的满足者(最小匹配原则)
6. 可通过标签选择器(`selector`)进一步筛选
> [!info] 一对一绑定:一个 PV 只能绑定一个 PVC,一个 PVC 也只能绑定一个 PV。绑定后即使 PV 的标签被修改,绑定关系也不会解除。
### StorageClass
StorageClass 定义了存储的"类别",是动态供给的核心。它告诉 K8s 使用哪个 provisioner、以什么参数创建存储。
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: kubernetes.io/aws-ebs # 存储驱动
parameters:
type: gp3 # 传递给 provisioner 的参数
fsType: ext4
iopsPerGB: "50"
reclaimPolicy: Delete # PV 回收策略
volumeBindingMode: WaitForFirstConsumer # 绑定时机
allowVolumeExpansion: true # 允许扩容
mountOptions:
- debug
```
关键字段详解:
| 字段 | 说明 | 常见值 |
|------|------|--------|
| `provisioner` | 存储驱动标识 | `kubernetes.io/aws-ebs`、`rancher.io/local-path`、`csi.hetzner.cloud` 等 |
| `parameters` | 传递给 provisioner 的键值对,不同驱动参数不同 | EBS: `type`/`fsType`;Ceph: `pool`/`clusterID` |
| `reclaimPolicy` | 动态创建的 PV 的回收策略,默认 `Delete` | `Retain`、`Delete` |
| `volumeBindingMode` | PV 创建和绑定的时机 | `Immediate`(默认)、`WaitForFirstConsumer` |
| `allowVolumeExpansion` | 是否允许 PVC 扩容,默认 `false` | `true`、`false` |
| `mountOptions` | 动态创建的 PV 的挂载选项 | NFS: `["hard","nfsvers=4.1"]` |
> [!warning] `volumeBindingMode: WaitForFirstConsumer` 的重要性:默认的 `Immediate` 模式会在 PVC 创建时立即绑定/创建 PV,这可能导致 PV 创建在与 Pod 不同的可用区(尤其在多可用区集群中),最终 Pod 调度失败。`WaitForFirstConsumer` 会延迟到 Pod 调度时才创建 PV,确保存储与计算在同一拓扑域。
```mermaid
sequenceDiagram
participant User as 用户
participant API as API Server
participant SC as StorageClass
participant PV Controller
participant Provisioner
Note over User,Provisioner: Immediate 模式
User->>API: 创建 PVC
API->>PV Controller: 协调 PVC
PV Controller->>Provisioner: 创建 PV
Provisioner-->>PV Controller: PV Ready
PV Controller->>API: 绑定 PV ↔ PVC
Note over User,Provisioner: WaitForFirstConsumer 模式
User->>API: 创建 PVC (Pending)
User->>API: 创建 Pod
API->>PV Controller: Pod 调度完成,触发绑定
PV Controller->>Provisioner: 在 Pod 所在节点/可用区创建 PV
Provisioner-->>PV Controller: PV Ready
PV Controller->>API: 绑定 PV ↔ PVC
```
## StatefulSet
StatefulSet 是 K8s 为有状态应用设计的工作负载控制器,解决了 Deployment 无法满足的三个核心需求:
1. **稳定的网络标识**:Pod 名称固定(`pod-0`、`pod-1`),通过 Headless Service 提供 DNS 记录(`pod-0.service-name.namespace.svc.cluster.local`)
2. **稳定的持久存储**:每个 Pod 拥有独立的 PVC,Pod 重建后 PVC 不变
3. **有序的部署/扩缩/删除**:Pod 按序号顺序操作,前一个就绪后才启动下一个
### StatefulSet vs Deployment
| 特性 | Deployment | StatefulSet |
|------|-----------|-------------|
| Pod 名称 | 随机后缀 | 有序编号(`-0`、`-1`、`-2`) |
| 部署顺序 | 并行 | 顺序(前一个 Ready 后才启动下一个) |
| 删除顺序 | 并行 | 逆序 |
| 网络标识 | 不稳定 | 稳定,配合 Headless Service |
| 存储 | 共享 PVC 或无存储 | 每个 Pod 独立 PVC(通过 `volumeClaimTemplates`) |
| 缩容 | 随机删除 | 从最大序号开始删除 |
### volumeClaimTemplates
`volumeClaimTemplates` 是 StatefulSet 的核心特性 —— 它会为每个 Pod 自动创建独立的 PVC。
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql # 必须关联 Headless Service
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
volumeMounts:
- name: data
mountPath: /var/lib/mysql
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 20Gi
```
上例中,K8s 会自动创建三个 PVC:`data-mysql-0`、`data-mysql-1`、`data-mysql-2`,每个绑定独立的 PV。
> [!info] StatefulSet 缩容时,PVC 不会被删除。这意味着缩容后再扩容,新 Pod 会重新挂载原来的 PVC,数据不丢失。这是 StatefulSet 最重要的特性之一。
## CSI(Container Storage Interface)
CSI 是 K8s 存储插件的标准化接口规范(自 K8s 1.13 GA),取代了早期的 in-tree volume plugin 方式。
### 为什么需要 CSI
早期 K8s 的存储驱动代码直接存在于 K8s 主仓库(in-tree),导致:
- 存储供应商必须将代码合入 K8s 仓库,发布周期受限于 K8s
- K8s 仓库臃肿,维护负担重
- 驱动 bug 可能影响 K8s 核心组件稳定性
CSI 将存储驱动移出 K8s 核心(out-of-tree),通过 gRPC 接口标准化通信。
### CSI 架构
```mermaid
graph TD
subgraph "Kubernetes 组件"
AK[API Server]
KC[kube-controller-manager]
KM[kubelet]
end
subgraph "CSI 组件(集群级别)"
CP[CSI Controller Plugin]
CS[CSI Driver 注册]
end
subgraph "CSI 组件(节点级别)"
NP[CSI Node Plugin]
end
subgraph "存储后端"
S[Cloud Provider / SAN / NFS]
end
AK --> KC
KC -->|创建/删除/扩容| CP
CP --> S
KM -->|挂载/卸载| NP
NP --> S
CS -.->|注册| AK
```
CSI 的三个核心组件:
- **CSI Controller Plugin**:集群级别,负责卷的创建、删除、快照、扩容等控制面操作
- **CSI Node Plugin**:节点级别,负责卷的挂载(NodePublishVolume)、卸载(NodeUnpublishVolume)等数据面操作
- **CSIDriver 对象**:在 K8s 中注册驱动,声明能力(如 `attachRequired`、`podInfoOnMount`)
### 常见 CSI 驱动
| 存储后端 | CSI 驱动名称 | 说明 |
|----------|-------------|------|
| AWS EBS | `ebs.csi.aws.com` | AWS 官方,替代 in-tree aws-ebs |
| GCE PD | `pd.csi.storage.gke.io` | GCP 官方 |
| Azure Disk | `disk.csi.azure.com` | Azure 官方 |
| Ceph RBD | `rbd.csi.ceph.com` | Ceph 官方 |
| Longhorn | `driver.longhorn.io` | Rancher 开源分布式存储 |
| OpenEBS | `openebs.io/local` / `openebs.io/zfs-localpv` | 开源容器原生存储 |
| Hetzner Cloud | `csi.hetzner.cloud` | Hetzner 云盘 |
| 本地路径 | `rancher.io/local-path` | Local Path Provisioner,开发测试常用 |
### CSI Driver 部署示例
以 AWS EBS CSI Driver 为例,通常通过 Helm 安装:
```bash
helm repo add aws-ebs-csi-driver https://kubernetes-sigs.github.io/aws-ebs-csi-driver
helm install aws-ebs-csi-driver aws-ebs-csi-driver/aws-ebs-csi-driver \
--namespace kube-system
```
安装后会在集群中创建 `ebs.csi.aws.com` CSIDriver 对象和对应的 DaemonSet(Node Plugin)+ Deployment(Controller Plugin)。
## 实战示例
以下完整示例演示如何部署一个带持久化存储的 MySQL StatefulSet。
### 1. StorageClass
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: mysql-ssd
provisioner: rancher.io/local-path # 本地开发环境使用 local-path
reclaimPolicy: Retain # 数据库场景用 Retain 更安全
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
```
### 2. Headless Service
```yaml
apiVersion: v1
kind: Service
metadata:
name: mysql
labels:
app: mysql
spec:
ports:
- port: 3306
targetPort: 3306
name: mysql
clusterIP: None # Headless Service,StatefulSet 必需
selector:
app: mysql
```
### 3. StatefulSet
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql
replicas: 1 # 单节点 MySQL,生产建议配合 Operator 做主从
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: ROOT_PASSWORD
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
readinessProbe:
exec:
command:
- mysqladmin
- ping
- -h
- localhost
initialDelaySeconds: 10
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: mysql-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: mysql-ssd
resources:
requests:
storage: 10Gi
```
### 4. Secret(MySQL root 密码)
```yaml
apiVersion: v1
kind: Secret
metadata:
name: mysql-secret
type: Opaque
stringData:
ROOT_PASSWORD: "MyS3cur3P@ssw0rd"
```
### 5. 客户端 Pod 测试连接
```yaml
apiVersion: v1
kind: Pod
metadata:
name: mysql-client
spec:
containers:
- name: mysql-client
image: mysql:8.0
command: ["sleep", "3600"]
```
进入客户端 Pod 后,使用 StatefulSet 的稳定 DNS 名称连接:
```bash
kubectl exec -it mysql-client -- bash
mysql -h mysql-0.mysql.default.svc.cluster.local -u root -p
```
> [!tip] DNS 解析规则:`<pod-name>.<headless-service-name>.<namespace>.svc.cluster.local`。`mysql-0.mysql` 即可解析,完整 FQDN 为 `mysql-0.mysql.default.svc.cluster.local`。
## 常见陷阱与最佳实践
### PVC 持续 Pending
**现象**:PVC 长时间处于 Pending 状态,Pod 无法启动。
**排查步骤**:
```bash
# 查看 PVC 事件
kubectl describe pvc <pvc-name>
# 查看是否有匹配的 PV
kubectl get pv
# 查看 StorageClass 是否存在
kubectl get storageclass
```
**常见原因**:
- 没有匹配的 Available PV(静态供给场景下)
- StorageClass 的 provisioner 未安装或配置错误
- `volumeBindingMode: WaitForFirstConsumer` 下,没有 Pod 消费该 PVC
- 底层存储配额不足或权限不足
### 动态供给配置错误
**现象**:PVC Pending,事件中显示 `waiting for first consumer to be created before binding`。
**排查**:确认 `volumeBindingMode` 设置是否符合预期。如果 Pod 已经存在但仍未绑定,检查:
- Pod 是否成功调度到节点(Pod 事件)
- CSI Node Plugin 是否在目标节点正常运行(DaemonSet Pod 状态)
- Provisioner 的参数是否正确(如 `parameters.type` 是否为合法值)
### RWX 不支持的存储类型
**现象**:PVC 声明 `ReadWriteMany` 但一直 Pending。
**原因**:块存储(EBS、GCE PD、Azure Disk)不支持 RWX。解决方案:
- 使用支持 RWX 的存储后端:NFS、CephFS、Azure File、Longhorn、OpenEBS
- 使用 ReadWriteOnce(如果应用本身不支持多写,如 MySQL 单实例)
- 引入分布式锁机制或应用层复制
### PVC 扩容
扩容需要 StorageClass 设置 `allowVolumeExpansion: true`,且 CSI 驱动支持在线扩容:
```bash
# 编辑 PVC 请求的容量
kubectl patch pvc mysql-data-mysql-0 -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
# 查看扩容进度
kubectl describe pvc mysql-data-mysql-0
```
> [!warning] PVC 扩容只能增大不能缩小。部分存储后端支持在线扩容(无需重启 Pod),部分需要 unmount 后才能扩容,具体取决于 CSI 驱动实现。
### 数据备份策略
PV 中的数据没有内置的备份机制,需要额外配置:
- **VolumeSnapshot**:K8s 1.20+ GA,通过 CSI 驱动创建卷快照
- **Velero**:开源的 K8s 备份恢复工具,支持卷快照 + 资源对象的完整备份
- **应用层备份**:对于数据库,使用 `mysqldump`、`pg_dump` 等原生工具定期备份
- **存储层快照**:利用云平台的 EBS Snapshot、Azure Snapshot 等定期快照
```yaml
# VolumeSnapshot 示例
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: mysql-snapshot
spec:
volumeSnapshotClassName: csi-aws-ebs-snapclass
source:
persistentVolumeClaimName: mysql-data-mysql-0
```
### 最佳实践总结
1. **始终使用 `volumeBindingMode: WaitForFirstConsumer`**,避免多可用区集群中存储与计算分离
2. **数据库场景使用 `reclaimPolicy: Retain`**,防止误删 PVC 导致数据丢失
3. **为 StatefulSet 配置 Readiness Probe**,确保 K8s 在 Pod 真正就绪后才启动下一个
4. **生产环境使用 CSI 驱动替代 in-tree plugin**,获得更好的维护性和功能支持
5. **定期备份 PV 数据**,不要依赖 K8s 的存储抽象作为唯一的持久化保障
6. **监控 PV 使用率**,设置 PVC 扩容告警,避免磁盘满导致服务中断
7. **避免使用 `hostPath`** 作为生产存储方案,除非有明确的安全和隔离措施
## 延伸阅读
- [[k8s-03-pod]] — Pod 基础与生命周期
- [[k8s-02-architecture]] — Kubernetes 整体架构