目录

Kubernetes-06 存储详解

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 绑定:

  1. accessModes 匹配
  2. storage 容量 >= PVC 请求量
  3. storageClassName 匹配
  4. 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