K8S 基于本地存储的持久卷

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

k8s31master 是控制节点

k8s31node1、k8s31node2 是工作节点

容器运行时是 containerd

一、背景分析

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

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

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

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

1)创建 StorageClass

javascript 复制代码
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)创建挂载目录

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

bash 复制代码
# node1 创建
[root@k8s31node1 ~]# mkdir -p /data/local_storage
# node2 创建
[root@k8s31node2 ~]# mkdir -p /data/local_storage

3)创建 PV

javascript 复制代码
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

javascript 复制代码
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)创建部署

javascript 复制代码
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 上,看看会发生什么?

javascript 复制代码
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 创建不成功。

  • 查看描述
bash 复制代码
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 本地目录创建文件

bash 复制代码
[root@k8s31node1 ~]# echo "LocalStorage" > /data/local_storage/index.html

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

  • 删除部署再重新执行
bash 复制代码
kubectl delete -f deploy.yaml
kubectl apply -f deploy.yaml

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

8)Delete 自动回收验证

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

bash 复制代码
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)手工回收

bash 复制代码
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)

bash 复制代码
# 创建发现目录和其子目录 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

bash 复制代码
kubectl apply -f default_example_storageclass.yaml
javascript 复制代码
# 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)镜像准备

bash 复制代码
# 找个国内好访问的镜像站,拉取镜像
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

javascript 复制代码
---
# 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、创建集群角色、创建账号角色绑定。

bash 复制代码
kubectl apply -f default_example_provisioner_generated.yaml

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

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

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

6)创建 PVC

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

storageClassName: fast-disks

bash 复制代码
kubectl apply -f pvc1.yaml

7)创建 Pod,使用 PVC

javascript 复制代码
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 自动回收验证

bash 复制代码
# 删除 pod
kubectl delete -f pod1.yaml
# 删除 PVC
kubectl delete -f pvc1.yaml

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

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

9)后续运维

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

bash 复制代码
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

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

相关推荐
hwj运维之路19 分钟前
基于k8s的Jenkins CI/CD平台部署实践(一):Jenkins部署详解
ci/cd·kubernetes·jenkins
一个天蝎座 白勺 程序猿29 分钟前
Python爬虫(14)Python爬虫数据存储新范式:云原生NoSQL服务实战与运维成本革命
爬虫·python·云原生
灵雀云2 小时前
证券行业数字化转型:灵雀云架设云原生“数字高速路”
云原生·paas
AllData公司负责人2 小时前
【能力比对】K8S数据平台VS数据平台
云原生·容器·kubernetes
识途老码2 小时前
k8s部署OpenELB
云原生·容器·kubernetes·eip
sunshine_sean4 小时前
docker 部署kafka命令
docker·容器·kafka
hwj运维之路4 小时前
基于k8s的Jenkins CI/CD平台部署实践(二):流水线构建与自动部署全流程
ci/cd·kubernetes·jenkins
Super_man541884 小时前
k8s之ingress解释以及k8s创建业务的流程定义
云原生·容器·kubernetes
Python测试之道7 小时前
K8s ConfigMap实战:像设置手机一样管理配置!
容器·智能手机·kubernetes
开心码农1号7 小时前
如何理解k8s中的controller
docker·容器·kubernetes