目录

Kubernetes-06 存储详解

Kubernetes 存储详解

系列第六篇。容器是无状态的,但现实世界的应用往往需要持久化数据。本篇讲解 K8s 的存储体系。


目录

  1. 存储概览
  2. Volume(临时存储)
  3. PersistentVolume(PV)
  4. PersistentVolumeClaim(PVC)
  5. StorageClass(动态供给)
  6. 常用存储类型实战
  7. Volume 快照
  8. Go 应用存储实战
  9. 小结

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