Kubernetes-06 存储详解
Kubernetes 存储详解
系列第六篇。容器是无状态的,但现实世界的应用往往需要持久化数据。本篇讲解 K8s 的存储体系。
目录
- 存储概览
- Volume(临时存储)
- PersistentVolume(PV)
- PersistentVolumeClaim(PVC)
- StorageClass(动态供给)
- 常用存储类型实战
- Volume 快照
- Go 应用存储实战
- 小结
1. 存储概览
K8s 存储架构:
应用 (Pod)
↓ volumeMounts
Volume(挂载点)
↓
PersistentVolumeClaim(存储申请)
↓
StorageClass(动态创建存储)或 PersistentVolume(静态供给)
↓
实际存储后端
├── 云存储: AWS EBS, GCE PD, Azure Disk
├── 分布式存储: Ceph, GlusterFS, Longhorn
├── 网络存储: NFS
└── 本地存储: local, hostPath
存储分类
| 类别 | 描述 | 适用场景 |
|---|---|---|
| 临时存储 | Pod 生命周期内有效,Pod 删除后消失 | 临时文件、缓存、容器间共享 |
| 持久化存储 | 独立于 Pod 生命周期 | 数据库、用户上传文件、日志 |
2. Volume(临时存储)
K8s Volume 有多种类型,生命周期与 Pod 绑定(不是容器)。
2.1 emptyDir
最简单的 Volume,Pod 创建时分配,Pod 删除时清除。
spec:
volumes:
- name: shared-data
emptyDir: {} # 使用磁盘
# emptyDir:
# medium: Memory # 使用内存(tmpfs),速度更快但占用内存
# sizeLimit: 500Mi # 限制大小
containers:
- name: main-app
image: go-app:v1
volumeMounts:
- name: shared-data
mountPath: /data
- name: log-agent
image: fluent-bit:latest
volumeMounts:
- name: shared-data
mountPath: /data
readOnly: true
适用场景:
- 同一 Pod 内多容器共享数据
- 临时计算文件
- 作为写缓冲区
2.2 hostPath
挂载宿主机(Node)的目录到 Pod。
spec:
volumes:
- name: host-log
hostPath:
path: /var/log/app
type: DirectoryOrCreate # 不存在则创建目录
containers:
- name: go-app
volumeMounts:
- name: host-log
mountPath: /var/log/app
hostPath.type 选项:
| type | 描述 |
|---|---|
"" |
不检查(默认) |
Directory |
目录必须存在 |
DirectoryOrCreate |
不存在则创建(权限 0755) |
File |
文件必须存在 |
FileOrCreate |
不存在则创建(权限 0644) |
Socket |
Unix Socket 必须存在 |
CharDevice |
字符设备必须存在 |
BlockDevice |
块设备必须存在 |
注意:
- hostPath 依赖于 Node,Pod 迁移到其他 Node 后数据不可用
- 安全风险较高(可以访问宿主机文件系统)
- 适用于:DaemonSet 采集节点日志、访问宿主机特殊设备
2.3 configMap / secret
将 ConfigMap 或 Secret 挂载为文件。
spec:
volumes:
- name: app-config
configMap:
name: my-config
items: # 只挂载指定的 key
- key: app.yaml
path: app.yaml # 挂载后的文件名
mode: 0444 # 文件权限
- name: tls-certs
secret:
secretName: my-tls-secret
defaultMode: 0400 # 默认权限(比较严格,key 文件)
containers:
- name: go-app
volumeMounts:
- name: app-config
mountPath: /etc/app
readOnly: true
- name: tls-certs
mountPath: /etc/ssl/app
readOnly: true
2.4 projected
将多个 Volume 合并到同一个挂载点:
spec:
volumes:
- name: all-in-one
projected:
sources:
- configMap:
name: my-config
- secret:
name: my-secret
- serviceAccountToken: # 注入 ServiceAccount Token
path: token
expirationSeconds: 3600
containers:
- name: go-app
volumeMounts:
- name: all-in-one
mountPath: /etc/app
3. PersistentVolume(PV)
PV 是集群级别的存储资源,由管理员预先创建(静态供给)或 StorageClass 自动创建(动态供给)。
3.1 PV 的关键属性
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-mysql-data
spec:
capacity:
storage: 100Gi # 存储容量
accessModes:
- ReadWriteOnce # 访问模式
persistentVolumeReclaimPolicy: Retain # 回收策略
storageClassName: "" # 空字符串表示不属于任何 StorageClass
volumeMode: Filesystem # Filesystem(默认)| Block
# 后端存储类型(以 NFS 为例)
nfs:
server: 192.168.1.100
path: /exports/mysql
3.2 访问模式(accessModes)
| 模式 | 简写 | 描述 |
|---|---|---|
ReadWriteOnce |
RWO | 单个节点可读写(大多数块存储) |
ReadOnlyMany |
ROX | 多节点只读 |
ReadWriteMany |
RWX | 多节点可读写(NFS、CephFS) |
ReadWriteOncePod |
RWOP | 单个 Pod 可读写(K8s 1.22+) |
注意: accessModes 是存储能力声明,并不强制限制同时访问。实际的并发访问限制由存储后端决定。
3.3 回收策略(reclaimPolicy)
| 策略 | 描述 |
|---|---|
Retain |
PVC 删除后 PV 保留(需手动清理和重新绑定) |
Delete |
PVC 删除后 PV 和底层存储一起删除(动态供给默认) |
Recycle |
已弃用(曾经是 rm -rf,然后允许重新绑定) |
生产建议: 重要数据用 Retain,测试环境用 Delete。
3.4 PV 的生命周期
Available → 可用,未被绑定
Bound → 已被某个 PVC 绑定
Released → PVC 已删除,PV 正在清理(Retain 策略时 PV 停留在此状态)
Failed → 自动清理失败
4. PersistentVolumeClaim(PVC)
PVC 是用户对存储的申请,类比于 Pod 申请 CPU/内存。
4.1 基本 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-data-pvc
namespace: production
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi # 申请 50Gi
storageClassName: fast-ssd # 指定 StorageClass(为空则绑定无 StorageClass 的 PV)
volumeMode: Filesystem
kubectl get pvc
# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
# mysql-data-pvc Bound pvc-xxx-yyy-zzz 50Gi RWO fast-ssd 5m
4.2 在 Pod 中使用 PVC
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: root-password
ports:
- containerPort: 3306
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
volumes:
- name: mysql-data
persistentVolumeClaim:
claimName: mysql-data-pvc # 引用 PVC
4.3 PV 与 PVC 的绑定规则
K8s 会自动寻找满足以下条件的 PV 与 PVC 绑定:
accessModes匹配storage容量 >= PVC 请求量storageClassName匹配volumeMode匹配
如果没有合适的 PV,PVC 会一直处于 Pending 状态。
5. StorageClass(动态供给)
StorageClass 让 K8s 在 PVC 创建时自动创建 PV,无需管理员手动预创建。
5.1 StorageClass 示例
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
annotations:
storageclass.kubernetes.io/is-default-class: "true" # 设为默认 StorageClass
provisioner: ebs.csi.aws.com # CSI 驱动(根据云厂商不同而不同)
parameters:
type: gp3 # EBS 卷类型
iops: "3000"
throughput: "125"
encrypted: "true"
kmsKeyId: "arn:aws:kms:..."
reclaimPolicy: Delete # PVC 删除时自动删除 EBS 卷
allowVolumeExpansion: true # 允许 PVC 扩容
volumeBindingMode: WaitForFirstConsumer # 等 Pod 调度后再创建 PV(保证同可用区)
# 或 Immediate:PVC 创建时立即创建 PV
5.2 常用 StorageClass 配置
AWS EBS(gp3):
provisioner: ebs.csi.aws.com
parameters:
type: gp3
iops: "3000"
throughput: "125"
GCE Persistent Disk:
provisioner: pd.csi.storage.gke.io
parameters:
type: pd-ssd
Azure Disk:
provisioner: disk.csi.azure.com
parameters:
storageaccounttype: Premium_LRS
本地存储(Longhorn,适合自建集群):
provisioner: driver.longhorn.io
parameters:
numberOfReplicas: "3"
staleReplicaTimeout: "2880"
NFS(自建 NFS Server):
provisioner: nfs.csi.k8s.io
parameters:
server: 192.168.1.100
share: /exports
5.3 PVC 扩容
# 修改 PVC 的 storage 大小
kubectl edit pvc mysql-data-pvc
# 将 storage 从 50Gi 改为 100Gi
# 或
kubectl patch pvc mysql-data-pvc -p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}'
kubectl get pvc mysql-data-pvc
# STATUS: Bound(扩容中可能短暂变为 Resizing)
注意: 只能扩容,不能缩容。需要 StorageClass 设置 allowVolumeExpansion: true。
6. 常用存储类型实战
6.1 本地存储(local)
适合需要高 I/O 性能的场景(如数据库):
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-node1
spec:
capacity:
storage: 500Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
volumeMode: Filesystem
local:
path: /mnt/disks/ssd1 # Node 上的本地路径
nodeAffinity: # 必须指定节点亲和性
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- node-1 # 只在 node-1 上有效
# 对应的 StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner # 不自动供给,需手动创建 PV
volumeBindingMode: WaitForFirstConsumer # 等 Pod 调度后才绑定
6.2 多副本共享存储(NFS)
适合多个 Pod 需要共享同一存储(ReadWriteMany):
# NFS StorageClass(使用 nfs-subdir-external-provisioner)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-storage
provisioner: cluster.local/nfs-subdir-external-provisioner
parameters:
archiveOnDelete: "false" # PVC 删除时不归档
---
# PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: shared-files-pvc
spec:
accessModes:
- ReadWriteMany # 多 Pod 可同时读写
storageClassName: nfs-storage
resources:
requests:
storage: 100Gi
6.3 StatefulSet 使用 PVC(volumeClaimTemplates)
StatefulSet 自动为每个 Pod 创建独立 PVC:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgresql
spec:
replicas: 3
serviceName: postgresql
selector:
matchLabels:
app: postgresql
template:
metadata:
labels:
app: postgresql
spec:
containers:
- name: postgresql
image: postgres:16
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: pg-secret
key: password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: pg-data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates: # 为每个 Pod 自动创建 PVC
- metadata:
name: pg-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 200Gi
创建后自动生成:
PVC: pg-data-postgresql-0 → 绑定到 postgresql-0
PVC: pg-data-postgresql-1 → 绑定到 postgresql-1
PVC: pg-data-postgresql-2 → 绑定到 postgresql-2
7. Volume 快照
K8s 1.20+ 稳定支持 Volume 快照(VolumeSnapshot),用于备份。
# 创建快照
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: mysql-snapshot-20260316
spec:
volumeSnapshotClassName: csi-aws-vsc
source:
persistentVolumeClaimName: mysql-data-pvc # 对哪个 PVC 做快照
---
# 从快照恢复(创建新 PVC)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-data-restored
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd
resources:
requests:
storage: 50Gi
dataSource:
name: mysql-snapshot-20260316
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
8. Go 应用存储实战
8.1 场景:Go 文件服务
一个 Go 应用提供文件上传/下载功能,需要持久化存储。
package main
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
)
const uploadDir = "/data/uploads" // 对应 PVC 挂载点
func main() {
// 确保目录存在
if err := os.MkdirAll(uploadDir, 0755); err != nil {
panic(err)
}
http.HandleFunc("/upload", uploadHandler)
http.HandleFunc("/files/", downloadHandler)
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
})
fmt.Println("File service starting on :8080")
http.ListenAndServe(":8080", nil)
}
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
r.ParseMultipartForm(32 << 20) // 32MB 内存限制
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
dst, err := os.Create(filepath.Join(uploadDir, header.Filename))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer dst.Close()
if _, err := io.Copy(dst, file); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, "File %s uploaded successfully", header.Filename)
}
func downloadHandler(w http.ResponseWriter, r *http.Request) {
filename := filepath.Base(r.URL.Path)
http.ServeFile(w, r, filepath.Join(uploadDir, filename))
}
8.2 对应的 K8s 配置
# PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: file-service-pvc
namespace: production
spec:
accessModes:
- ReadWriteOnce # 如果只有一个 Pod 副本,RWO 即可
# - ReadWriteMany # 如果多副本,需要 NFS 等支持 RWX 的存储
storageClassName: fast-ssd
resources:
requests:
storage: 100Gi
---
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: file-service
namespace: production
spec:
replicas: 1 # RWO 只支持单副本(如需多副本改用 NFS + RWX)
selector:
matchLabels:
app: file-service
template:
metadata:
labels:
app: file-service
spec:
containers:
- name: file-service
image: myregistry.io/file-service:v1.0.0
ports:
- containerPort: 8080
volumeMounts:
- name: upload-data
mountPath: /data/uploads # 与 Go 代码中的 uploadDir 一致
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "1"
memory: "1Gi"
volumes:
- name: upload-data
persistentVolumeClaim:
claimName: file-service-pvc
8.3 数据库备份 CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
name: mysql-backup
namespace: production
spec:
schedule: "0 2 * * *" # 每天凌晨 2 点
timeZone: "Asia/Shanghai"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: mysql:8.0
command:
- sh
- -c
- |
DATE=$(date +%Y%m%d_%H%M%S)
mysqldump -h mysql-service -u root -p"${MYSQL_ROOT_PASSWORD}" \
--all-databases > /backup/full_backup_${DATE}.sql
# 删除 7 天前的备份
find /backup -name "*.sql" -mtime +7 -delete
echo "Backup completed: full_backup_${DATE}.sql"
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: root-password
volumeMounts:
- name: backup-storage
mountPath: /backup
volumes:
- name: backup-storage
persistentVolumeClaim:
claimName: mysql-backup-pvc
9. 小结
存储选型指南
需要持久化数据?
├── 否 → emptyDir(临时)或不需要 Volume
└── 是
├── 单 Pod(或 StatefulSet 每 Pod 独立)?
│ └── PVC with RWO(EBS/Local Disk/Ceph RBD)
├── 多 Pod 共享?
│ └── PVC with RWX(NFS/CephFS)
└── 每个节点需要本地文件?
└── hostPath 或 local PV
常用命令速查
# 查看 PV
kubectl get pv
kubectl describe pv <name>
# 查看 PVC
kubectl get pvc
kubectl get pvc -n production
# 查看 StorageClass
kubectl get storageclass
# 查看 Pod 的 Volume 挂载情况
kubectl describe pod <name> | grep -A 10 "Volumes:"
# PVC 扩容
kubectl patch pvc <name> -p '{"spec":{"resources":{"requests":{"storage":"200Gi"}}}}'
# 查看 Volume 快照
kubectl get volumesnapshot
xingliuhua