K8s动态卷供应实战_StorageClass与LocalPath_NFS双Provisioner

别再手动建 PV 了!K8s 动态卷供应从原理到落地:StorageClass + Local Path + NFS 双 Provisioner 实战

摘要:告别手动 apply PV、删应用还得上节点清目录。K8s 动态卷供应(StorageClass + PVC + PV)让存储自动创建、自动绑定、自动回收。本文讲透原理与核心配置,并落地两套主流 Provisioner:Local Path 与 NFS,每步带验证。写完这条 PVC,水(PV)就自动来了。


一、为什么需要动态卷

以前创建存储卷(PV)都需要管理员手动操作,这类卷被称为静态卷。那么 Kubernetes 能不能"智能"管理卷呢?比如需要用卷时自动创建,不用时自动清理?

答案是可以的 ------ 这就是**动态卷供应(Dynamic Volume Provisioning)**的能力。它彻底省去了集群管理员提前手动配置存储的工作,用户只要提出存储需求,系统就会自动创建对应的存储卷。

二、动态卷供应流程

三步走,理解了这个流程,后面所有 YAML 都不用背:

  1. 管理员先创建"存储模板"(StorageClass ),指定用哪个"造卷工具"(Provisioner,制备器)来创建卷;
  2. 用户创建"存储申请单"(PVC)时,指定要用哪个"存储模板";
  3. 系统收到申请后,会让模板绑定的"造卷工具"自动创建一个 PV,并且把 PV 和用户的 PVC 绑定,用户直接用 PVC 就行。

三、StorageClass 存储类别

StorageClass 就像管理员定义的"存储套餐" ------ 不同套餐对应不同的存储类型、服务质量(比如读写速度)、备份策略等。Kubernetes 不关心套餐叫什么,只负责按套餐规则造卷。

两个必须先记住的规矩:

  • 命名很重要:用户创建 PVC 时,要通过这个名字选择对应的存储套餐;
  • 创建后不能改:StorageClass 一旦创建,名字和参数就没法修改了,要改只能删掉重建。

核心配置

每个"存储套餐"都包含以下核心信息,用来指导系统创建 PV:

1. provisioner(造卷工具,必填)

指定用哪个工具创建 PV。Kubernetes 自带一些 Provisioner,名字以 kubernetes.io 开头(比如 AWS EBS、Azure Disk);也可以使用第三方提供的 Provisioner(比如 NFS 没有内置,需要自己装),按 Kubernetes 规则运行即可。

存储类型 是否内置 说明
AWSElasticBlockStore ✅ 是 亚马逊云硬盘
AzureFile/AzureDisk ✅ 是 微软云文件 / 硬盘
Ceph RBD ❌ 否 Ceph 块存储
CephFS ❌ 否 需外部制备器
iSCSI ❌ 否 需外部制备器
Glusterfs ❌ 否 分布式文件系统
NFS ❌ 否 网络文件系统,需装外部制备器
Local ❌ 否 本地文件系统,需装外部制备器
2. reclaimPolicy(PV 回收策略)
  • Delete(默认):PVC 删除后,PV 也自动删,对应的后端存储(比如云硬盘、NFS 目录)也会清掉;
  • Retain:PVC 删除后,PV 保留下来,后端存储的数据也不删,管理员可以手动处理。
3. volumeBindingMode(绑定时机)
  • Immediate(立即绑定):创建 PVC 就造卷,不管 Pod 会不会用;适合存储能被所有节点访问的场景(比如 NFS、云硬盘);
  • WaitForFirstConsumer(等使用者):先不造卷,等第一个用这个 PVC 的 Pod 创建后,根据 Pod 要跑的节点造卷;适合本地存储、只能被特定节点访问的存储。
4. mountOptions(挂载附加参数)

指定 PV 挂载时附加的"额外设置",比如权限、调试模式。

⚠️ 注意:如果存储不支持这些选项,造卷会失败;如果选项填错,PV 挂载会失败。

5. parameters

比如存储类型(SSD/HDD)、副本数等,不同工具参数不一样。

基础示例

bash 复制代码
root@master30:~# cat > storageclass-demo.yaml <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard  # 套餐名,用户PVC要填这个
provisioner: kubernetes.io/aws-ebs  # 用AWS EBS的造卷工具
parameters:
  type: gp2  # 存储类型为gp2(AWS的通用型SSD)
reclaimPolicy: Retain  # PVC 不用了保留,不删除
allowVolumeExpansion: true  # 允许扩容
mountOptions:
  - debug  # 挂载时开启调试模式
volumeBindingMode: Immediate  # 立即创建并绑定PV
EOF

四、实战:部署 Local Path Provisioner

Local Path Provisioner 是 Rancher 开源、K8s 官方推荐的本地存储动态卷插件,适合需要使用节点本地磁盘的场景。

1. 部署制备器

bash 复制代码
root@master30:~# wget https://ghfast.top/https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml

官方模板 local-path-storage.yaml 核心由 5 类资源 组成,每部分各司其职:

资源类型 名称 核心作用
Namespace local-path-storage 隔离 Local Path Provisioner 相关资源
ConfigMap local-path-config 定义本地存储路径 规则(默认 /opt/local-path-provisioner)
ServiceAccount + RBAC local-path-provisioner 赋予 Provisioner 操作 PV/PVC 的权限
Deployment local-path-provisioner 运行 Local Path Provisioner 核心程序(动态创建本地 PV)
StorageClass local-path 提供给 PVC 调用的「本地存储模板」

核心内容如下(部署前建议先 vim 修改镜像仓库):

bash 复制代码
root@master30:~# vim local-path-storage.yaml
bash 复制代码
apiVersion: v1                              # K8s 核心 API 版本
kind: Namespace                             # 资源类型:命名空间(隔离用的大盒子)
metadata:                                   # 元数据
  name: local-path-storage                  # 命名空间名字,Provisioner 全家桶都住这

---
apiVersion: v1                              # 核心 API 版本
kind: ServiceAccount                        # 资源类型:服务账号(Pod 在 K8s 里的"身份证")
metadata:
  name: local-path-provisioner-service-account   # SA 名字
  namespace: local-path-storage             # 放在上面那个命名空间里

---
apiVersion: rbac.authorization.k8s.io/v1    # RBAC 权限管理 API 版本
kind: Role                                  # 资源类型:角色(只管一个命名空间的权限)
metadata:
  name: local-path-provisioner-role         # Role 名字
  namespace: local-path-storage             # 只在这个命名空间生效
rules:                                      # 权限规则列表
  - apiGroups: [""]                         # 空字符串 = 核心 API 组(pods/nodes/pv 这些基础资源)
    resources: ["pods"]                     # 能操作的资源:Pod
    verbs: ["get", "list", "watch", "create", "patch", "update", "delete"]  # 增删改查全包

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole                           # 资源类型:集群角色(全集群范围的权限)
metadata:
  name: local-path-provisioner-role         # ClusterRole 名字(和上面 Role 同名,但级别不同)
rules:
  - apiGroups: [""]                         # 核心 API 组
    resources: ["nodes", "persistentvolumeclaims", "configmaps", "pods", "pods/log"]  # 能看节点/PVC/配置表/Pod/Pod日志
    verbs: ["get", "list", "watch"]        # 只能查,不能改
  - apiGroups: [""]
    resources: ["persistentvolumes"]       # 能操作 PV(动态卷的核心!自动建/删 PV)
    verbs: ["get", "list", "watch", "create", "patch", "update", "delete"]  # 增删改查全包
  - apiGroups: [""]
    resources: ["events"]                   # 事件(记录 PVC 绑定/解绑事件给用户看)
    verbs: ["create", "patch"]             # 只能写事件
  - apiGroups: ["storage.k8s.io"]          # 存储专属 API 组
    resources: ["storageclasses"]           # 能看 StorageClass
    verbs: ["get", "list", "watch"]        # 只读

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding                           # 资源类型:角色绑定(把 Role 的权限发给 SA)
metadata:
  name: local-path-provisioner-bind
  namespace: local-path-storage
roleRef:                                    # ← 左边:从哪个 Role 拿权限
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: local-path-provisioner-role
subjects:                                   # ← 右边:权限发给谁
  - kind: ServiceAccount
    name: local-path-provisioner-service-account
    namespace: local-path-storage

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding                    # 资源类型:集群角色绑定(把 ClusterRole 发给 SA)
metadata:
  name: local-path-provisioner-bind
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole                         # 绑定的是 ClusterRole(全集群级)
  name: local-path-provisioner-role
subjects:
  - kind: ServiceAccount
    name: local-path-provisioner-service-account
    namespace: local-path-storage

---
apiVersion: apps/v1                         # 应用工作负载 API 版本
kind: Deployment                            # 资源类型:无状态工作负载(管 Pod 副本)
metadata:
  name: local-path-provisioner
  namespace: local-path-storage
spec:
  replicas: 1                               # 只跑 1 个副本(Provisioner 单点足够)
  selector:
    matchLabels:
      app: local-path-provisioner           # 管带这个标签的 Pod
  template:                                 # Pod 模板(照着这个样子建 Pod)
    metadata:
      labels:
        app: local-path-provisioner
    spec:
      serviceAccountName: local-path-provisioner-service-account  # Pod 用哪张"身份证"去调 API
      containers:
        - name: local-path-provisioner
          image: hub.laoma.cloud/rancher/local-path-provisioner:v0.0.37  # 镜像(已换自己私有加速仓库)
          imagePullPolicy: IfNotPresent     # 本地有就不重新拉,没有才拉
          command:                          # 容器启动命令
            - local-path-provisioner
            - --debug                       # 开 debug 日志,方便排错
            - start
            - --config
            - /etc/config/config.json       # 指定配置文件路径
          volumeMounts:                     # 把卷挂载进容器里
            - name: config-volume
              mountPath: /etc/config/       # 配置文件挂到容器这个目录
          env:                              # 环境变量
            - name: POD_NAMESPACE           # 把当前命名空间注入进 Pod
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace   # 自动取 Pod 自己的命名空间名
            - name: CONFIG_MOUNT_PATH
              value: /etc/config/             # 配置文件在容器里的路径
      volumes:                                # 定义 Pod 能用的卷
        - name: config-volume
          configMap:
            name: local-path-config           # 从下面那个 ConfigMap 挂配置进来

---
apiVersion: storage.k8s.io/v1               # 存储 API 版本
kind: StorageClass                          # 资源类型:存储类(动态卷的"模板")
metadata:
  name: local-path                          # SC 名字,PVC 里 storageClassName 就填这个
provisioner: rancher.io/local-path          # 用哪个 Provisioner 来造 PV(和 Provisioner Pod 里的对应)
volumeBindingMode: WaitForFirstConsumer     # 等 Pod 真正要挂载了再绑 PV(调度感知,Pod 调到哪 PV 就在哪)
reclaimPolicy: Delete                       # PVC 删了之后 PV 跟着删(数据也一起删)

---
kind: ConfigMap                             # 资源类型:配置字典(存配置文件用)
apiVersion: v1
metadata:
  name: local-path-config
  namespace: local-path-storage
data:
  config.json: |-                           # 主配置 JSON(告诉 Provisioner 数据放哪)
    {
            "nodePathMap":[                 # 节点路径映射表
            {
                    "node":"DEFAULT_PATH_FOR_NON_LISTED_NODES",   # 没单独列的节点都用默认路径
                    "paths":["/opt/local-path-provisioner"]       # 数据存在节点的这个目录下
            }
            ]
    }
  setup: |-                                 # 【建卷前】执行的脚本(建目录)
    #!/bin/sh
    set -eu                                 # 出错就退出
    mkdir -m 0777 -p "$VOL_DIR"             # 创建卷目录,权限 777(谁都能写)
  teardown: |-                              # 【删卷后】执行的脚本(删目录)
    #!/bin/sh
    set -eu
    rm -rf "$VOL_DIR"                       # 递归删掉整个卷目录
  helperPod.yaml: |-                        # 辅助 Pod 模板(在目标节点上跑上面两个脚本的"干活小工")
    apiVersion: v1
    kind: Pod
    metadata:
      name: helper-pod
    spec:
      priorityClassName: system-node-critical     # 高优先级,节点紧张时也不被驱逐
      tolerations:                                # 容忍污点
        - key: node.kubernetes.io/disk-pressure   # 磁盘紧张节点也能调度
          operator: Exists
          effect: NoSchedule
      containers:
      - name: helper-pod
        image: busybox                            # 用超轻量的 busybox 跑脚本
        imagePullPolicy: IfNotPresent

一键部署:

bash 复制代码
root@master30:~# kubectl apply -f local-path-storage.yaml

root@master30:~# kubectl get sc local-path
NAME         PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
local-path   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  31s

root@master30:~# kubectl get all -n local-path-storage
NAME                                          READY   STATUS    RESTARTS   AGE
pod/local-path-provisioner-74b5c4bf98-955sk   1/1     Running   0          63s

NAME                                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/local-path-provisioner   1/1     1            1           63s

NAME                                                DESIRED   CURRENT   READY   AGE
replicaset.apps/local-path-provisioner-74b5c4bf98   1         1         1       63s

2. 验证部署

bash 复制代码
root@master30:~# kubectl config set-context --current --namespace default
步骤 1:创建 PVC
bash 复制代码
root@master30:~# cat > local-path-pvc.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: local-path-pvc
  namespace: default
spec:
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 5Gi
  storageClassName: local-path  # 指定 Local Path 的 StorageClass
EOF

root@master30:~# kubectl apply -f local-path-pvc.yaml
root@master30:~# kubectl get pvc
NAME             STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
local-path-pvc   Pending                                      local-path     <unset>                 6s

注意:此时 PVC 处于 Pending 状态 ------ 因为 StorageClass 是 WaitForFirstConsumer,要等 Pod 关联才会真正创建本地目录。

步骤 2:创建 Deployment
bash 复制代码
root@master30:~# cat > local-path-deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: webapp
  name: webapp
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      containers:
      - image: hub.laoma.cloud/library/nginx
        name: nginx
        volumeMounts:
        - name: webapp-data
          mountPath: /usr/share/nginx/html
      volumes:
      - name: webapp-data
        persistentVolumeClaim:
          claimName: local-path-pvc
EOF

root@master30:~# kubectl apply -f local-path-deployment.yaml

查看 Pod 与 PVC 状态:

bash 复制代码
root@master30:~# kubectl get pods -o wide
NAME                      READY   STATUS    RESTARTS   AGE    IP             NODE                   NOMINATED NODE   READINESS GATES
webapp-6d46b54487-6nqmh   1/1     Running   0          114s   10.224.19.21   worker31.laoma.cloud   <none>           <none>
webapp-6d46b54487-kzkd5   1/1     Running   0          114s   10.224.19.22   worker31.laoma.cloud   <none>           <none>

root@master30:~# kubectl get pvc
NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
local-path-pvc   Bound    pvc-e992833c-af6a-4d4d-aff5-fc544394db6d   5Gi        RWO            local-path     <unset>                 4m41s

写入测试数据并验证:

bash 复制代码
root@master30:~# kubectl exec webapp-6d46b54487-6nqmh -- bash -c 'echo Hello World From Nginx > /usr/share/nginx/html/index.html'

root@master30:~# ls /opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/
index.html

root@master30:~# cat /opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/index.html
Hello World From Nginx
步骤 3:清理资源
bash 复制代码
root@master30:~# kubectl delete deployment webapp

root@master30:~# kubectl delete pvc local-path-pvc

root@master30:~# ls /opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/
ls: cannot access '/opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/': No such file or directory

五、实战:部署 NFS Provisioner

Local Path 的缺点是数据绑死在某个节点 ,Pod 漂移到别的节点就读不到数据。生产/测试环境更常用的方案是 NFS 共享存储。

1. 部署 NFS 服务

bash 复制代码
# master节点部署nfs服务
root@master30:~# apt install -y nfs-kernel-server
root@master30:~# mkdir -m 777 /shares/
root@master30:~# cat << EOF > /etc/exports
/shares *(rw,sync,no_root_squash,no_all_squash,insecure)
EOF
root@master30:~# systemctl restart nfs-server.service

# work节点安装nfs客户端
root@master31和32:~# apt install -y nfs-common

2. 部署制备器

NFS 没有内置制备器,这里我们使用社区经典的外部制备器 nfs-subdir-external-provisioner。

步骤 1:创建命名空间
bash 复制代码
root@master30:~# kubectl create ns kube-storage
root@master30:~# kubectl config set-context --current --namespace kube-storage
步骤 2:创建 RBAC 权限
bash 复制代码
root@master30:~# cat > nfs-rbac.yaml <<'EOF'
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
  namespace: kube-storage
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: nfs-client-provisioner-runner
rules:
  - apiGroups: [""]
    resources: ["persistentvolumes"]
    verbs: ["get", "list", "watch", "create", "delete"]
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["get", "list", "watch", "update"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["create", "update", "patch"]
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: run-nfs-client-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: kube-storage
roleRef:
  kind: ClusterRole
  name: nfs-client-provisioner-runner
  apiGroup: rbac.authorization.k8s.io
EOF

root@master30:~# kubectl apply -f nfs-rbac.yaml
步骤 3:部署 NFS Provisioner 应用
bash 复制代码
root@master30:~# cat > nfs-provisioner.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-client-provisioner
  namespace: kube-storage
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nfs-client-provisioner
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccountName: nfs-client-provisioner
      volumes:
      - name: nfs-client-root
        nfs:
          server: 10.1.8.30   # NFS 服务器 IP
          path: /shares       # NFS 共享路径
      containers:
      - name: nfs-client-provisioner
        image: hub.laoma.cloud/sig-storage/nfs-subdir-external-provisioner:v4.0.2
        volumeMounts:
        - name: nfs-client-root
          mountPath: /persistentvolumes
        env:
        - name: PROVISIONER_NAME
          value: fuseim.pri/ifs
        - name: NFS_SERVER
          value: 10.1.8.30    # 替换为你的 NFS 服务器 IP
        - name: NFS_PATH
          value: /shares      # 替换为你的共享路径
EOF

root@master30:~# kubectl apply -f nfs-provisioner.yaml

root@master30:~# kubectl get deployments.apps -n kube-storage
NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
nfs-client-provisioner   1/1     1            1           1m
步骤 4:创建 NFS StorageClass
bash 复制代码
root@master30:~# cat > nfs-storageclass.yaml <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-storage
  namespace: kube-storage
provisioner: fuseim.pri/ifs  # 必须与上面 Provisioner 的 PROVISIONER_NAME 一致
parameters:
  onDelete: retain       # 删除 PVC 保留 NFS 数据
  archiveOnDelete: "false"
#reclaimPolicy: Retain
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: Immediate
EOF

root@master30:~# kubectl apply -f nfs-storageclass.yaml

root@master30:~# kubectl get sc
NAME          PROVISIONER      RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
nfs-storage   fuseim.pri/ifs   Delete          Immediate           true                   2s

3. 验证部署

bash 复制代码
root@master30:~# kubectl config set-context --current --namespace default
步骤 1:创建 PVC
bash 复制代码
root@master30:~# cat > nfs-pvc.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: webclaim
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: "nfs-storage"
EOF

root@master30:~# kubectl apply -f nfs-pvc.yaml

root@master30:~# kubectl get pvc
NAME       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
webclaim   Bound    pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc   5Gi        RWO           nfs-storage    <unset>                 3s

root@master30:~# kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                   STORAGECLASS   AGE
pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc   5Gi        RWO            Delete           Bound    default/webclaim        nfs-storage    65s

对比 Local Path:NFS 由于是 Immediate 模式,PVC 一创建就立刻造卷并 Bound,不需要等 Pod。

查看 NFS 共享目录并准备主页:

bash 复制代码
root@master30:~# ls /shares/
default-webclaim-pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc

root@master30:~# echo Hello World From Nginx > /shares/default-webclaim-pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc/index.html
步骤 2:创建 Deployment 并暴露服务
bash 复制代码
root@master30:~# cat > nfs-deploy.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: webapp
  name: webapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      containers:
      - image: hub.laoma.cloud/library/nginx
        name: nginx
        volumeMounts:
        - name: webapp-data
          mountPath: /usr/share/nginx/html
      volumes:
      - name: webapp-data
        persistentVolumeClaim:
          claimName: webclaim
EOF

root@master30:~# kubectl apply -f nfs-deploy.yaml

root@master30:~# kubectl expose deployment webapp --target-port 80 --port 80 --protocol TCP
root@master30:~# kubectl get svc webapp
NAME     TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
webapp   ClusterIP   10.109.140.205   <none>        80/TCP    5m11s

访问测试:

bash 复制代码
curl 10.109.140.205
Hello World From Nginx
curl 10.109.140.205
Hello World From Nginx
步骤 3:清理资源
bash 复制代码
root@master30:~# kubectl delete deployments.apps webapp

root@master30:~# kubectl delete pvc webclaim
persistentvolumeclaim "webclaim" deleted

root@master30:~# kubectl get pv
No resources found

root@master30:~# ls /shares/default-webclaim-pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc/
index.html

✅ 保留 NFS provisioner,下一章 StatefulSet 还要用。


Local Path 还是 NFS?怎么选

对比项 Local Path Provisioner NFS Provisioner
存储位置 节点本地磁盘 独立 NFS 服务器共享目录
绑定模式 WaitForFirstConsumer(必须) Immediate(推荐)
Pod 能否漂移 ❌ 不能,重建后可能被调度到没数据的节点 ✅ 能,任意节点都能挂载
性能 ⭐⭐⭐⭐⭐ 本地磁盘 IO ⭐⭐⭐ 受网络影响
可用性 ❌ 节点宕机数据不可访问 ⚠️ NFS 单点(生产需做高可用)
适用场景 单节点测试、对 IO 要求极高的缓存类应用 通用测试环境、需要跨节点共享数据的场景
生产建议 仅限特定场景 用高可用 NFS 集群或换 Ceph / Rook

一句话:玩单机/追求极致 IO 用 Local Path;要 Pod 自由漂移,用 NFS。

三个高频排错命令

bash 复制代码
# PVC 一直 Pending,第一件事就是 describe
root@master30:~# kubectl describe pvc <pvc-name>
# 常见错误:storageclass 名不匹配 / provisioner pod 没起来 / WaitForFirstConsumer 还没被 Pod 引用

# 看 provisioner 是不是在正常工作
root@master30:~# kubectl logs -n kube-storage deployment/nfs-client-provisioner
root@master30:~# kubectl logs -n local-path-storage deployment/local-path-provisioner

# 看有没有生成对应的 PV
root@master30:~# kubectl get pv | grep <pvc-name>

六、总结

动态卷供应的本质,就是把"管理员手动创建 PV"这件事抽象成一个可复用的模板:

  1. StorageClass 定义套餐(用谁造、怎么造、怎么回收、什么时候绑);
  2. Provisioner 是干活的工人(内置或外部安装);
  3. PVC 是用户提的需求单;
  4. 需求单一交,工人按套餐自动把 PV 造出来并绑定。

记住 provisioner 名字要对得上、volumeBindingMode 别选错、reclaimPolicy 想清楚要不要留数据 ------ 这三点搞定,存储这块基本不会翻车。

相关推荐
大囚长1 小时前
Kubernetes 的资源开销、适用边界,以及游戏与有状态负载的工程现实
游戏·容器·kubernetes
RuoyiOffice1 小时前
Hyper-V+Kubernetes 搭建实战:保留 Windows,构建三节点 Linux 容器集群
linux·spring boot·kubernetes·containerd·hyper-v·微服务部署·ruoyi office
fb_123451 小时前
K8s StatefulSet五大有状态集群实战_Nginx_Etcd_Redis_Cassandra_MySQL
nginx·kubernetes·etcd
时间售卖铺2 小时前
Docker私有仓库搭建与实战指南
运维·docker·云原生·容器
分布式存储与RustFS3 小时前
Kafka 分层存储落地指南:冷 Segment 自动落到 RustFS 对象存储
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
Z21-X3 小时前
云原生运维实战笔记:华为云计算与网络类服务全景解析
运维·笔记·学习·云原生·华为云
mengge.cloud3 小时前
10-29-Kubernetes动态卷供应StatefulSet与ETCD
容器·kubernetes·etcd
小哈里3 小时前
【推理】NVIDIA DLI培训:大规模部署与优化 AI 推理(推理原理,K8S,网关层,pd分离,MoE,Dynamo)
人工智能·容器·kubernetes·nvidia·推理
孔令飞3 小时前
AI 岗位类别、要求、技能及学习路线
云原生·go·ai编程