K8S 基于本地存储的持久卷

发布于:2025-05-08 ⋅ 阅读:(18) ⋅ 点赞:(0)

假设有如下三个节点的 K8S 集群:

k8s31master 是控制节点

k8s31node1、k8s31node2 是工作节点

容器运行时是 containerd

 一、背景分析

阅读本文,默认您有 PV-PVChostPath 相关知识。

由于安全方面的考虑,K8S 官方并不推荐 hostPath + 节点选择器 来作为 Pod 的持久化方案。

转而提倡 local 持久卷的方式。我们今天就来实践一下 local 持久卷

二、no-provisioner 方式(无供应商方式)

1)创建 StorageClass

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage  # 名称需与 PV 中的 storageClassName 对应
provisioner: kubernetes.io/no-provisioner  # 关键:本地卷不支持动态供应
volumeBindingMode: WaitForFirstConsumer  # 关键:延迟绑定直到 Pod 调度
reclaimPolicy: Delete  # 卷回收策略(Retain/Delete)

provisioner=kubernetes.io/no-provisioner:本地卷还不支持动态制备; 然而还是需要创建 StorageClass 以延迟卷绑定,直到 Pod 被实际调度到合适的节点。

volumeBindingMode=WaitForFirstConsumer:延迟 PVC 与 PV 的绑定。延迟卷绑定使得调度器在为 PersistentVolumeClaim 选择一个合适的 PersistentVolume 时能考虑到所有 Pod 的调度限制。

 2)创建挂载目录

因为本地卷不支持动态制备,所以需要在每个工作节点上手工创建挂载目录。

# node1 创建
[root@k8s31node1 ~]# mkdir -p /data/local_storage
# node2 创建
[root@k8s31node2 ~]# mkdir -p /data/local_storage

 3)创建 PV

apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-rwo-pv
  labels:
    type: local
spec:
  capacity:
    storage: 1Gi  # 定义存储容量
  volumeMode: Filesystem  # 可选:Filesystem 或 Block
  accessModes:
    - ReadWriteOnce  # 关键:仅允许单节点读写挂载
  persistentVolumeReclaimPolicy: Delete # 可选:Retain/Recycle/Delete
  storageClassName: local-storage  # 引用上面创建的存储类
  local:
    path: /data/local_storage  # 节点上的实际路径
  nodeAffinity:  # 限制卷只能被特定节点使用
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - k8s31node1 # 替换为你的节点名

accessModes=ReadWriteOnce:因为是本地存储,所以只允许单节点挂载。

nodeAffinity:节点亲和性,限制卷只能被具有标签 kubernetes.io/hostname 且值为 k8s31node1 的节点使用。可以通过命令 kubectl get node --show-labels 查看节点标签。

  •  查看 pv

 STATUS=Available:实现可用状态。

 4)创建 PVC

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: local-rwo-pvc
spec:  
  storageClassName: local-storage
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  •  查看 pv,pvc

因为是延迟绑定,所以 PVC 显示为 Pending 状态。只有当 Pod 挂载了这个 PVC,K8S 才会寻找合适的 PV 进行绑定。

 5)创建部署

apiVersion: apps/v1
kind: Deployment
metadata:
  name: local-rwo-deploy
spec:
  replicas: 2
  selector:
    matchLabels:
      app: local-rwo-pod
  template:
    metadata:
      labels:
        app: local-rwo-pod
    spec:
      volumes:
      - name: local-rwo-volumes
        persistentVolumeClaim:
          claimName: local-rwo-pvc
      containers:
      - name: nginx
        image: nginx:1.14.2
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - name: local-rwo-volumes
          mountPath: /usr/share/nginx/html
          readOnly: true
        ports:
        - containerPort: 80
          protocol: TCP
  • 查看部署的 Pod 

 可以看到,因为 PV 节点亲和性的设置,所有的 Pod 会被调度到同一个节点 node1 上。

这个时候的 PV、PVC 显示绑定状态。

6)RWO 验证

 此时,我们再创建一个 Pod 挂载这个 PVC,然后通过节点选择器,让它被调度到 node2 上,看看会发生什么?

apiVersion: v1
kind: Pod
metadata:
  name: local-node2-pod
spec:
  nodeName: k8s31node2 # 节点选择器
  volumes:
  - name: storage
    persistentVolumeClaim:
      claimName: local-rwo-pvc # 需提前创建对应的 PVC
  containers:
  - name: nginx
    image: nginx:1.14.2
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - name: storage
      mountPath: /usr/share/nginx/html
      readOnly: true
    ports:
    - containerPort: 80
      protocol: TCP

会发现 Pod 创建不成功。 

  • 查看描述
kubectl describe pod local-node2-pod

  Warning  FailedMount  9s (x7 over 40s)  kubelet  MountVolume.NodeAffinity check failed for volume "local-rwo-pv" : no matching NodeSelectorTerms

从这也能看出,本地存储实现了严格的 RWO。

7)数据持久性验证 

在 node1 本地目录创建文件

[root@k8s31node1 ~]# echo "LocalStorage" > /data/local_storage/index.html

 数据在 Pod1 跟 Pod2 中是共享的。

  •  删除部署再重新执行
kubectl delete -f deploy.yaml
kubectl apply -f deploy.yaml

 pod 被删除后重新部署,数据还在。

 8)Delete 自动回收验证

删除部署、PVC 看看有没有帮我们自动删除 PV

kubectl delete -f deploy.yaml
kubectl delete -f pvc.yaml
kubectl get pv

 会发现 PV 的状态是 Failed。说明自动回收失败了。

  •  查看 PV 描述

 Warning  VolumeFailedDelete  2m51s  persistentvolume-controller  error getting deleter volume plugin for volume "local-rwo-pv": no volume plugin matched

卷的自动删除失败,当前的卷插件并不支持 自动回收。

 9)手工回收

kubectl delete -f pv.yaml

 在 node1 上删除数据文件

三、provisioner 方式(静态供应商方式)

1)介绍

kubernetes-sigs/sig-storage-local-static-provisioner 是 K8S 本地存储的静态外部供应商。

所谓静态,是说,虽然它可以帮我们创建 PV 与清理 PV 上的数据,但是真实存储目录的创建,还是需要集群管理员来做。本质上,它还是一种 PV 的静态供应方式。

英文原文:

There is one provisioner instance on each node in the cluster. Each instance is responsible for monitoring and managing the local volumes on its node.

The basic components of the provisioner are as follows:

  • Discovery: The discovery routine periodically reads the configured discovery directories and looks for new mount points that don't have a PV, and creates a PV for it.

  • Deleter: The deleter routine is invoked by the Informer when a PV phase changes. If the phase is Released, then it cleans up the volume and deletes the PV API object.

  • Cache: A central cache stores all the Local PersistentVolumes that the provisioner has created. It is populated by a PV informer that filters out the PVs that belong to this node and have been created by this provisioner. It is used by the Discovery and Deleter routines to get the existing PVs.

  • Controller: The controller runs a sync loop that coordinates the other components. The discovery and deleter run serially to simplify synchronization with the cache and create/delete operations.

中文翻译:

  在每个集群节点上都有一个供应者实例。每个实例负责监控和管理其节点上的本地卷。供应者的基本组件如下:

  1. 发现模块
    • 发现程序会定期读取配置好的发现目录,寻找没有 PV 的新挂载点,并为它们创建 PV。
  2. 删除模块
    • 当 PV 状态发生变化时,Informer 会调用删除程序。
    • 如果状态是"已释放",它会清理卷并删除 PV API 对象。
  3. 缓存
    • 中央缓存存储供应者创建的所有本地持久卷。
    • 它由一个 PV Informer 填充,该 Informer 过滤出属于该节点且由该供应者创建的 PV。
    • 发现和删除程序使用它来获取现有 PV。
  4. 控制器
    • 控制器运行一个同步循环来协调其他组件。
    • 发现和删除程序串行运行,以简化与缓存的同步以及创建/删除操作。

 简单来说:

sig-storage-local-static-provisioner 会用 DaemonSet 控制器的方式,在每个集群工作节点上,都创建出一个 pod,pod 名字叫 provisioner,它会负责监听发现目录(默认是 /mnt/fast-disks)有没有新的挂载点进来,有的话,就以这个挂载点创建一个新的 PV。


我们将基于 开始文档 来进行配置

2)创建并挂载发现目录

在所有工作节点上创建发现目录(/mnt/fast-disks)和存储目录(/data/local_storage/fast-disks)

# 创建发现目录和其子目录 v1
[root@k8s31node1 ~]# mkdir -p /mnt/fast-disks/v1
# 创建实际存储目录
[root@k8s31node1 ~]# mkdir -p /data/local_storage/fast-disks/v1
# 执行绑定挂载
[root@k8s31node1 ~]# mount --bind /data/local_storage/fast-disks/v1 /mnt/fast-disks/v1

# 创建发现目录和其子目录 v1
[root@k8s31node2 ~]# mkdir -p /mnt/fast-disks/v1
# 创建实际存储目录
[root@k8s31node2 ~]# mkdir -p /data/local_storage/fast-disks/v1
# 执行绑定挂载
[root@k8s31node2 ~]# mount --bind /data/local_storage/fast-disks/v1 /mnt/fast-disks/v1

mount 命令常用来将外部设备如磁盘、U盘等挂载进 Linux 系统中,我手头没有多余的硬盘来演示,所以我把文件系统目录 /data/local_storage/fast-disks/v1 通过绑定挂载的方式,挂载到 /mnt/fast-disks/v1。 

3)创建 StorageClass

执行 sig-storage-local-static-provisioner\deployment\kubernetes\example\default_example_storageclass.yaml

kubectl apply -f default_example_storageclass.yaml
# Only create this for K8s 1.9+
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-disks
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
# Supported policies: Delete, Retain
reclaimPolicy: Delete
  • 创建名为 fast-disks 的 StorageClass:

4)镜像准备

# 找个国内好访问的镜像站,拉取镜像
docker pull swr.cn-north-4.myhuaweicloud.com/ddn-k8s/registry.k8s.io/sig-storage/local-volume-provisioner:v2.6.0
# 名字太长,改短一点
docker tag 镜像ID registry.k8s.io/sig-storage/local-volume-provisioner:v2.6.0
# 导出镜像
docker save -o local-volume-provisioner.tar.gz registry.k8s.io/sig-storage/local-volume-provisioner:v2.6.0
# 上传到工作节点
scp local-volume-provisioner.tar.gz 192.168.40.20:/root
scp local-volume-provisioner.tar.gz 192.168.40.30:/root
在工作节点导入镜像
[root@k8s31node1 ~]# ctr -n=k8s.io images import local-volume-provisioner.tar.gz
[root@k8s31node1 ~]# ctr -n=k8s.io images ls | grep local

[root@k8s31node2 ~]# ctr -n=k8s.io images import local-volume-provisioner.tar.gz
[root@k8s31node2 ~]# ctr -n=k8s.io images ls | grep local

 5)部署 provisioner

修改 sig-storage-local-static-provisioner\deployment\kubernetes\example\default_example_provisioner_generated.yaml

---
# Source: provisioner/templates/provisioner.yaml
 
apiVersion: v1
kind: ConfigMap
metadata:
  name: local-provisioner-config 
  namespace: default 
data:
  storageClassMap: |     
    fast-disks:
       hostDir: /mnt/fast-disks
       mountDir:  /mnt/fast-disks 
       blockCleanerCommand:
         - "/scripts/shred.sh"
         - "2"
       volumeMode: Filesystem
       fsType: ext4
       namePattern: "*"
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: local-volume-provisioner
  namespace: default
  labels:
    app: local-volume-provisioner
spec:
  selector:
    matchLabels:
      app: local-volume-provisioner 
  template:
    metadata:
      labels:
        app: local-volume-provisioner
    spec:
      serviceAccountName: local-storage-admin
      containers:
        - image: "registry.k8s.io/sig-storage/local-volume-provisioner:v2.6.0" # 修改镜像版本
          imagePullPolicy: "IfNotPresent" # 修改镜像拉取策略
          name: provisioner 
          securityContext:
            privileged: true
          env:
          - name: MY_NODE_NAME
            valueFrom:
              fieldRef:
                fieldPath: spec.nodeName
          volumeMounts:
            - mountPath: /etc/provisioner/config 
              name: provisioner-config
              readOnly: true             
            - mountPath:  /mnt/fast-disks 
              name: fast-disks
              mountPropagation: "HostToContainer" 
      volumes:
        - name: provisioner-config
          configMap:
            name: local-provisioner-config         
        - name: fast-disks
          hostPath:
            path: /mnt/fast-disks 

---
# Source: provisioner/templates/provisioner-service-account.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: local-storage-admin
  namespace: default

---
# Source: provisioner/templates/provisioner-cluster-role-binding.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: local-storage-provisioner-pv-binding
  namespace: default
subjects:
- kind: ServiceAccount
  name: local-storage-admin
  namespace: default
roleRef:
  kind: ClusterRole
  name: system:persistent-volume-provisioner
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: local-storage-provisioner-node-clusterrole
rules:
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: local-storage-provisioner-node-binding
  namespace: default
subjects:
- kind: ServiceAccount
  name: local-storage-admin
  namespace: default
roleRef:
  kind: ClusterRole
  name: local-storage-provisioner-node-clusterrole
  apiGroup: rbac.authorization.k8s.io

修改镜像版本与镜像拉取策略。

provisioner 利用 DaemonSet 控制器在每个工作节点上,都创建出一个 pod,监听发现目录(默认是 /mnt/fast-disks)挂载点,创建对应 PV。pod 要操作 node,要调用 K8S API Server,所以脚本的内容,就是创建 ServiceAccount、创建集群角色、创建账号角色绑定。

kubectl apply -f default_example_provisioner_generated.yaml

 执行成功后,会创建一个 local-volume-provisioner DaemonSet,并在每个节点上创建一个 Pod。

 这些 Pod 会监视每个节点发现目录的挂载点,然后创建相应的 PV:

 这些 PV 都配置对应节点的节点亲和性,后续我们的业务 Pod 使用这些 PV 存储的时候,就会被调度到 PV 对应的节点上——哪个 Pod 使用了 PV,就会被调度到 PV 对应的节点上。

 6)创建 PVC

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc1
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi
  storageClassName: fast-disks

storageClassName: fast-disks

kubectl apply -f pvc1.yaml

 7)创建 Pod,使用 PVC

apiVersion: v1
kind: Pod
metadata:
  name: local-pod1
spec:
  volumes:
  - name: storage
    persistentVolumeClaim:
      claimName: pvc1 # 使用上面的 PVC
  containers:
  - name: nginx
    image: nginx:1.14.2
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - name: storage
      mountPath: /usr/share/nginx/html
      readOnly: true
    ports:
    - containerPort: 80
      protocol: TCP

 此时,PV local-pv-4df7cdd7 已经被绑定给了 pvc1。

local-pv-4df7cdd7 位于 node2,pod 也被调度到 node2。

此时,在 node2 存储目录 /data/local_storage/fast-disks/v1 创建文件 index.html。

是可以被挂载进我们的 Pod local-pod1 的。

8)Delete 自动回收验证

# 删除 pod
kubectl delete -f pod1.yaml
# 删除 PVC
kubectl delete -f pvc1.yaml

此时可以看到,local-pv-4df7cdd7 PV 又被释放了出来。

 node2 上的数据,也被清理掉了。

 

 9)后续运维

后续运维,就只需要在每个节点上创建更多的挂载点,然后再创建对应数量的 PVC,以供开发建 Pod 使用。可以使用脚本,提高创建效率。

for i in $(seq 1 5); do
  mkdir -p /mnt/fast-disks/v${i} 
  mkdir -p /data/local_storage/fast-disks/v${i}
  mount --bind /data/local_storage/fast-disks/v${i} /mnt/fast-disks/v${i} 
done

 后续运维,还可以把临时挂载改为永久挂载,以防节点重启,挂载丢失。


网站公告

今日签到

点亮在社区的每一天
去签到