【K8S存储管理:PV/PVC动态供应及NFS动态供给实战】

提示:本文原创作品,良心制作,干货为主,简洁清晰,一看就会

文章目录

  • 前言
  • 一、K8S存储管理
    • [1.1 底层存储](#1.1 底层存储)
    • [1.2 PV/PVC](#1.2 PV/PVC)
    • [1.3 StorageClass](#1.3 StorageClass)
  • 二、K8S中基于NFS的动态存储实战
    • [2.1 基于 NFS 的动态存储供给流程](#2.1 基于 NFS 的动态存储供给流程)
    • [2.2 部署NFS](#2.2 部署NFS)
    • [2.3 定义storage并授权](#2.3 定义storage并授权)
    • [2.4 部署自动创建PV的pod服务](#2.4 部署自动创建PV的pod服务)
    • [2.5 测试无状态服务](#2.5 测试无状态服务)
    • [2.6 测试有状态服务](#2.6 测试有状态服务)

前言

一、K8S存储管理

K8s存储管理的核心是「把存储和容器解耦」,通过一套分层抽象,解决容器数据临时化、跨节点共享和持久化的问题

1.1 底层存储

底层存储也就是数据真正存在的地方

  • 本地存储(hostPath/emptyDir):存在节点本地磁盘
  • 云厂商存储(AWS EBS/阿里云盘):存在云厂商的块存储
  • 开源分布式存储(Ceph/NFS):存在集群化的存储服务
  • K8s对象存储(ConfigMap/Secret):存在etcd里的配置数据

1.2 PV/PVC

PV相当于存储的"货架",PVC相当于"申请单"

  • PV(持久化卷):管理员在底层存储上划出来的"可用货架",比如100G的Ceph卷、20G的云盘,定义好容量、读写模式等
  • PVC(持久化卷声明):应用侧提交的"领用申请",声明自己需要多大容量、什么读写模式的存储
  • 绑定机制:K8s会自动把PVC和匹配的PV绑定,Pod只需要挂载PVC,不用关心底层用的是什么存储

1.3 StorageClass

StorageClass动态创建PV的"自动化货架"

  • 解决手动创建PV的麻烦:管理员定义好一个存储模板(比如"Ceph 10G 标准卷"),交给StorageClass
  • 应用侧PVC只要指定这个StorageClass,K8s就会自动调用底层存储驱动,动态创建对应的PV并绑定给PVC,不用管理员提前准备

总的来说:数据存在底层存储里 → 管理员用PV把它包装成可用资源 → 应用用PVC申请资源 → 用StorageClass实现动态创建,Pod只需要挂载PVC就能用持久化存储

二、K8S中基于NFS的动态存储实战

2.1 基于 NFS 的动态存储供给流程

1,kubectl 创建了一个 StatefulSet,它的 volumeClaimTemplates 指定了 storageClassName: managed-nfs-storage

2,K8s 控制器发现 PVC 没有对应的 PV,就去问 managed-nfs-storage 这个 StorageClass 定义的 provisioner 是谁

3,发现是 nfs-client-provisioner,于是把创建 PV 的请求发给它

4,nfs-client-provisioner Pod 收到请求后,去 NFS 服务器 192.168.13.137 上创建一个目录,然后创建一个 PV 并和 PVC 绑定

5,StatefulSet 的 Pod 启动后,成功挂载这个 PV,数据就持久化到 NFS 上了

2.2 部署NFS

yaml 复制代码
## 准备一台机器安装NFS,后续的PV/PVC会到NFS中拿空间
root@NFS:~# apt -y install nfs-kernel-server
root@NFS:~# systemctl start nfs-kernel-server
root@NFS:~# mkdir /data
root@NFS:~# vim /etc/exports
/data *(rw,sync,no_root_squash,no_subtree_check)
root@NFS:~# chmod 777 /data
root@NFS:~# systemctl restart nfs-kernel-server
root@NFS:~# systemctl enable nfs-kernel-server

2.3 定义storage并授权

yaml 复制代码
## 1. 创建sc
root@k8s-master1:~# vim storageclass-nfs.yaml 
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
        name: managed-nfs-storage
## 指定外部NFS供给器的名字
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
root@k8s-master1:~# kubectl apply -f storageclass-nfs.yaml 
root@k8s-master1:~# kubectl get sc
NAME                  PROVISIONER                                   RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
managed-nfs-storage   k8s-sigs.io/nfs-subdir-external-provisioner   Delete          Immediate           false                  38s

因为storage自动创建pv需要经过kube-apiserver,所以要进行授权

yaml 复制代码
## 2. 创建RBAC
root@k8s-master1:~# vim rbac.yaml 
## ServiceAccount:NFS provisioner pod 使用的身份
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
  # 需要指定命名空间
  namespace: default
---
## ClusterRole:定义集群维度的权限
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: nfs-client-provisioner-ClusterRole
rules:
  - apiGroups: [""]
    resources: ["nodes"]       # 需要获取节点信息,用于拓扑感知
    verbs: ["get","list","watch"]
  - apiGroups: [""]
    resources: ["persistentvolumes"]   # 需要全量操作PV
    verbs: ["get", "list", "watch", "create", "delete"]
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]      # 需要监控PVC,并更新其状态
    verbs: ["get", "list", "watch", "update"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]   # 需要获取storageclasses信息
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["events"]  # 需要记录事件
    verbs: ["create", "update", "patch"]
---
## ClusterRoleBinding:将ServiceAccount绑定到ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: nfs-clinet-provisioner-ClusterRoleBinding
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    # 必须添加:ServiceAccount所在命名空间
    namespace: default
roleRef:
  kind: ClusterRole
  name: nfs-clinet-provisioner-ClusterRole
  apiGroup: rbac.authorization.k8s.io
---
## Role: 命名空间维度的权限,可以和clusterrole合并
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: nfs-clinet-provisioner-Role
  namespace: default 
rules:
  - apiGroups: [""]
    resources: ["endpoints"]  # 某些provisioner实现会使用endpoints做leader election
    verbs: ["get","list","watch","create","update","patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: nfs-clinet-provisioner-RoleBinding
  namespace: default
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    # 必须添加:ServiceAccount所在命名空间
    namespace: default
roleRef:
  kind: Role
  name: nfs-clinet-provisioner-Role
  apiGroup: rbac.authorization.k8s.io
root@k8s-master1:~# kubectl apply -f rbac.yaml 

2.4 部署自动创建PV的pod服务

yaml 复制代码
root@k8s-master1:~# vim deploy-nfs.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-client-provisioner
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nfs-client-provisioner
  strategy:  # 升级策略
    type: Recreate  # 当需要更新Pod时,K8S会先删除现有的Pod,再创建新的Pod;这适用于不能同时运行多个副本的有状态服务
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      # 指定pod运行时使用的sa名称,其绑定的RBAC权限决定了provisioner能够访问哪些K8S API资源
      serviceAccountName: nfs-client-provisioner
      containers:
        - name: nfs-client-provisioner
          # 这里使用阿里云镜像仓库中的nfs-subdir-external-provisioner镜像,版本 v4.0.0;该镜像实现了nfs动态供给器
          image: registry.cn-beijing.aliyuncs.com/pylixm/nfs-subdir-external-provisioner:v4.0.0
          volumeMounts:
            - name: nfs-root
            # 将nfs共享挂载到容器内的 /persistentvolumes 路径,不建议改;provisioner 会在该目录下为每个pvc创建子目录
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              # 必须与StorageClass中的 provisioner 字段完全一致,这样StorageClass才能调用该供给器
              value: k8s-sigs.io/nfs-subdir-external-provisioner
            - name: NFS_SERVER
              value: 192.168.13.137 # nfs服务器的ip地址
            - name: NFS_PATH
              value: /data  # nfs服务器上导出的共享目录路径
      volumes:
        - name: nfs-root
          nfs:
            server: 192.168.13.137
            path: /data
yaml 复制代码
root@k8s-master1:~# kubectl apply -f deploy-nfs.yaml 
root@k8s-master1:~# kubectl get pod
NAME                                      READY   STATUS    RESTARTS   AGE
nfs-client-provisioner-65ddc6f649-7zmv8   1/1     Running   0          4s

至此,基于动态 PV/PVC 供给的存储架构已搭建完成。当前已部署的核心组件包括:

  • StorageClass:定义存储类型与供给策略;
  • ServiceAccount 及配套的 RBAC 资源:为自动供给程序授予必要的操作权限;
  • NFS Provisioner Pod:负责动态创建 PV/PVC,该 Pod 已绑定上述 ServiceAccount,并通过 StorageClass 声明后端存储。

后续在创建工作负载(如 Pod、Deployment、StatefulSet)时,只需在 PVC 中通过 storageClassName 字段引用该 StorageClass,Kubernetes 便会自动完成 PV 的动态创建与 PVC 的绑定,无需手动干预

2.5 测试无状态服务

无状态 Deployment 模式:需要提前申请 PVC,然后Pod 再去引用 这个PVC------ 适合共享存储

yaml 复制代码
root@k8s-master1:~# mkdir -p /k8s-test/sc/
root@k8s-master1:~# cd /k8s-test/sc/

## 创建一个PVC,绑定storageClass,让其自动生成pv
root@k8s-master1:/k8s-test/sc# vim pvc-1.yaml 
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
        name: pvc-stateless
spec:
        storageClassName: managed-nfs-storage
        accessModes:
                - ReadWriteMany
        resources:
                requests:
                        storage: 100Mi
root@k8s-master1:/k8s-test/sc# kubectl apply -f pvc-1.yaml
root@k8s-master1:/k8s-test/sc# kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                   STORAGECLASS          REASON   AGE
pvc-1e0871ac-8a6a-4864-b8e2-30e339a2887a   100Mi      RWX            Delete           Bound    default/pvc-stateless   managed-nfs-storage            4s
root@k8s-master1:/k8s-test/sc# kubectl get pvc
NAME            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS          AGE
pvc-stateless   Bound    pvc-1e0871ac-8a6a-4864-b8e2-30e339a2887a   100Mi      RWX            managed-nfs-storage   7s
yaml 复制代码
## 创建nginx的无状态控制器,副本为2,挂载网页到nfs上
root@k8s-master1:/k8s-test/sc# vim pod-1.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: new
spec:
  replicas: 2  # 副本数,根据需要调整
  selector:
    matchLabels:
      app: new
  template:
    metadata:
      labels:
        app: new
    spec:
      containers:
      - name: nginx
        image: nginx:1.24
        ports:
        - containerPort: 80
        volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: nginx-data
      volumes:
      - name: nginx-data
        persistentVolumeClaim:
          claimName: pvc-stateless
root@k8s-master1:/k8s-test/sc# kubectl apply -f pod-1.yaml 
root@k8s-master1:/k8s-test/sc# kubectl get pod -o wide
NAME                                      READY   STATUS    RESTARTS   AGE    IP               NODE        NOMINATED NODE   READINESS GATES
nfs-client-provisioner-65ddc6f649-7zmv8   1/1     Running   0          2m   10.244.36.104    k8s-node1   <none>           <none>
nginx-6d7f475986-7zn6n                    1/1     Running   0          1m   10.244.169.181   k8s-node2   <none>           <none>
nginx-6d7f475986-jmbt8                    1/1     Running   0          1m   10.244.36.105    k8s-node1   <none>           <none>
yaml 复制代码
##  NFS 是网络文件系统,多个客户端可以同时挂载同一个远程目录,并且看到的内容完全一致
## 两个pod共享了同一个网络磁盘。就像两个人同时打开同一个网络共享文件夹,其中一人修改了文件,另一人刷新后就能看到变化
## 进入其中一个pod中,并添加页面
root@k8s-master1:/k8s-test/sc# kubectl exec -it nginx-6d7f475986-jmbt8 /bin/bash
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
root@nginx-6d7f475986-jmbt8:/# ls /usr/share/nginx/html/
root@nginx-6d7f475986-jmbt8:/# echo 11111 > /usr/share/nginx/html/index.html
root@nginx-6d7f475986-jmbt8:/# exit
exit
root@k8s-master1:/k8s-test/sc# curl 10.244.169.181
11111
## 访问另一个pod可以看到相同的页面
root@k8s-master1:/k8s-test/sc# curl 10.244.36.105
11111

# 在nfs机器上能看到刚才创建出的文件
root@NFS:~# ls /data/
default-pvc-stateless-pvc-1e0871ac-8a6a-4864-b8e2-30e339a2887a
root@NFS:~# ls /data/default-pvc-stateless-pvc-1e0871ac-8a6a-4864-b8e2-30e339a2887a/
index.html

2.6 测试有状态服务

StatefulSet 通常会配合 volumeClaimTemplates 使用。每个 Pod 启动时,会自动创建一个专属的 PVC,进而绑定一个专属的 PV不共享,每个 Pod 拥有独立存储

StatefulSet 使用 volumeClaimTemplates 时,不需要提前创建 PVC。它会为每个 Pod 自动生成 一个独立的 PVC

yaml 复制代码
## 创建一个mysql的有状态控制器,副本为2,并且设置svc为Headless Service类型,挂载mysql的数据存放目录到nfs
root@k8s-master1:/k8s-test/sc# vim mysql.yaml 
apiVersion: apps/v1
kind: StatefulSet
metadata:
        name: mysql
spec:
        # 关联的Headless Service名称,用于生成稳定DNS
        serviceName: "mysql"  
        replicas: 2
        selector:
                matchLabels:
                        app: mysql
        template:
                metadata:
                        labels:
                                app: mysql
                spec:
                        containers:
                                - name: mysql
                                  image: mysql:5.7
                                  ports:
                                          - containerPort: 3306
                                            name: mysql
                                  volumeMounts:
                                          - name: test-mysql
                                            # 挂载到MySQL数据目录
                                            mountPath: /var/lib/mysql 
                                  env:
                                          - name: MYSQL_ROOT_PASSWORD
                                            value: "Admin@123456"
        ## 动态PVC模板(StatefulSet特有)
        volumeClaimTemplates: 
                - metadata:
                        name: test-mysql
                  spec:
                          # 访问模式:单节点读写
                          accessModes: ["ReadWriteOnce"] 
                          # 使用已创建的StorageClass动态供给
                          storageClassName: "managed-nfs-storage" 
                          resources:
                                  requests:
                                          storage: 2Gi
---
apiVersion: v1
kind: Service
metadata:
        name: mysql
        labels:
                app: mysql
spec:
        ports:
                - port: 3306
                  name: database
        # Headless Service(不分配ClusterIP)
        clusterIP: None
        selector:
                app: mysql
yaml 复制代码
## 创建mysql.yaml
root@k8s-master1:/k8s-test/sc# kubectl apply -f mysql.yaml
root@k8s-master1:/k8s-test/sc# kubectl get svc
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP    37d
mysql        ClusterIP   None         <none>        3306/TCP   17s

## 由创建时间可以看到mysql的pod不是同时启动的,mysql-0先running后mysql-1再启动
root@k8s-master1:/k8s-test/sc# kubectl get pod -o wide| grep mysql  
mysql-0                                   1/1     Running   0          3m38s   10.244.36.106    k8s-node1   <none>           <none>
mysql-1                                   1/1     Running   0          2m44s   10.244.169.182   k8s-node2   <none>           <none>

## 自动创建pv/pvc
root@k8s-master1:/k8s-test/sc# kubectl get pv  
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                        STORAGECLASS          REASON   AGE
pvc-1e0871ac-8a6a-4864-b8e2-30e339a2887a   100Mi      RWX            Delete           Bound    default/pvc-stateless        managed-nfs-storage            176m
pvc-b21d9215-7128-4aaf-8d45-073c1b9b8083   2Gi        RWO            Delete           Bound    default/test-mysql-mysql-0   managed-nfs-storage            4m54s
pvc-d4ec3851-3021-478a-91d2-0af9995b00dc   2Gi        RWO            Delete           Bound    default/test-mysql-mysql-1   managed-nfs-storage            4m
root@k8s-master1:/k8s-test/sc# kubectl get pvc
NAME                 STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS          AGE
pvc-stateless        Bound    pvc-1e0871ac-8a6a-4864-b8e2-30e339a2887a   100Mi      RWX            managed-nfs-storage   176m
test-mysql-mysql-0   Bound    pvc-b21d9215-7128-4aaf-8d45-073c1b9b8083   2Gi        RWO            managed-nfs-storage   4m56s
test-mysql-mysql-1   Bound    pvc-d4ec3851-3021-478a-91d2-0af9995b00dc   2Gi        RWO            managed-nfs-storage   4m2s

## 查看nfs上的/data目录,可以看到数据挂载成功
root@NFS:~# ls /data/
default-pvc-stateless-pvc-1e0871ac-8a6a-4864-b8e2-30e339a2887a       default-test-mysql-mysql-1-pvc-d4ec3851-3021-478a-91d2-0af9995b00dc
default-test-mysql-mysql-0-pvc-b21d9215-7128-4aaf-8d45-073c1b9b8083
root@NFS:~# ls /data/default-test-mysql-mysql-0-pvc-b21d9215-7128-4aaf-8d45-073c1b9b8083/
auto.cnf    ca.pem           client-key.pem  ibdata1      ib_logfile1  mysql       performance_schema  public_key.pem   server-key.pem
ca-key.pem  client-cert.pem  ib_buffer_pool  ib_logfile0  ibtmp1       mysql.sock  private_key.pem     server-cert.pem  sys
yaml 复制代码
root@k8s-master1:/k8s-test/sc# kubectl exec -it mysql-0 /bin/bash
bash-4.2# mysql -h mysql-0.mysql.default.svc.cluster.local -P 3306 -u root -p'Admin@123456'

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> exit
Bye
bash-4.2# exit
exit
root@k8s-master1:/k8s-test/sc# 

StatefulSet 中的每个 Pod 会获得形如 <pod-name>.<service-name>.<namespace>.svc.cluster.local 的稳定 DNS 名称

例如,mysql-0.mysql.default.svc.cluster.local,即使 Pod 重新调度,该域名也不会改变

特性 手动创建 PVC + Deployment StatefulSet + volumeClaimTemplates
是否需要预先创建 PVC ✅ 必须提前手动创建 ❌ 自动创建
PVC 与 Pod 的对应关系 多个 Pod 可共享一个 PVC 每个 Pod 独占一个 PVC(名称含 Pod 序号)
存储是否共享 取决于是否共用同一 PVC 默认不共享,相互隔离
Pod 删除后 PVC 是否保留 保留(除非手动删除) 保留(可配置保留策略)
典型场景 静态文件、配置中心、共享缓存 数据库、ZooKeeper、Redis 集群

注:

文中若有疏漏,欢迎大家指正赐教。

本文为100%原创,转载请务必标注原创作者,尊重劳动成果。

求赞、求关注、求评论!你的支持是我更新的最大动力,评论区等你~

相关推荐
容器魔方1 小时前
“驾驭工程”下一跳?JiuwenClaw AgentTeam开启“协同工程”全新范式
人工智能·云原生·容器·架构·开源
YuanDaima20482 小时前
Docker 核心架构与底层技术原理解析
运维·人工智能·docker·微服务·容器·架构·个人开发
liux35282 小时前
Kubernetes v1.27.16 部署 Prometheus + Grafana + Alertmanager 监控体系
kubernetes
sbjdhjd2 小时前
02(上)| K8s 资源管理全流程:命令、配置、生产避坑
linux·运维·云原生·kubernetes·云计算·podman·kubelet
零壹AI实验室3 小时前
云原生微服务踩坑记:187个服务降到23个,故障率降低90%
微服务·云原生·架构
珂玥c3 小时前
k8s集群切换master
云原生·容器·kubernetes
殇尘3 小时前
Docker + VSCode 搭建开发环境沙箱
vscode·docker·容器
颯沓如流星4 小时前
ZKube:优雅易用的 ZooKeeper 可视化管理工具
分布式·zookeeper·云原生
汪汪大队u4 小时前
从 Docker Compose 到 Kubernetes:物联网管理系统迁移实战(3)—— 两个运维坑
运维·docker·kubernetes