ljzsdut
GitHubToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeBack to homepage
Edit page

4.4 K8s存储之本地持久化存储 Local Pv

一、Local PV的设计

LocalPV:Kubernetes直接使用宿主机的本地磁盘目录 ,来持久化存储容器的数据。它的读写性能相比于大多数远程存储来说,要好得多,尤其是SSD盘。

1. Local PV 使用场景

Local Persistent Volume 并不适用于所有应用。它的适用范围非常固定,比如:高优先级的系统应用,需要在多个不同节点上存储数据,而且对 I/O 要求较高。

典型的应用包括:分布式数据存储比如 MongoDB,分布式文件系统比如 GlusterFS、Ceph 等,以及需要在本地磁盘上进行大量数据缓存的分布式应用,其次使用 Local Persistent Volume 的应用必须具备数据备份和恢复的能力,允许你把这些数据定时备份在其他位置。

2. Local PV的实现

LocalPV的实现可以理解为我们前面使用的hostpath加上nodeAffinity,比如:在宿主机NodeA上提前创建好目录 ,然后在定义Pod时添加nodeAffinity=NodeA,指定Pod在我们提前创建好目录的主机上运行。

3. Local PV 和常规PV的区别

对于常规的 PV,Kubernetes 都是先调度 Pod 到某个节点上,然后再持久化”这台机器上的 Volume 目录。而 Local PV,则需要运维人员提前准备好节点的磁盘。它们在不同节点上的挂载情况可以完全不同,甚至有的节点可以没这种磁盘。所以调度器就必须能够知道所有节点与 Local Persistent Volume 对应的磁盘的关联关系,然后根据这个信息来调度 Pod。也就是在调度的时候考虑Volume 分布。

Both use local disks available on a machine. But! Imagine you have a cluster of three machines and have a Deployment with a replica of 1. If your pod is scheduled on node A, writes to a host path, then the pod is destroyed. At this point the scheduler will need to create a new pod, and this pod might be scheduled to node C which doesn’t have the data. Oops!

Local volumes fix this by ensuring a pod is scheduled to the machine where the data exists.

二、创建Local PV

创建Local PV 其实应该给宿主机挂载并格式化一个可用的磁盘,这里我们就在宿主机上挂载几个 RAM Disk(内存盘)来模拟本地磁盘。

$ mkdir /mnt/disks
$ for vol in vol1 vol2 vol3; do mkdir /mnt/disks/$vol; mount -t tmpfs $vol /mnt/disks/$vol; done

如果希望其他节点也能支持 Local Persistent Volume 的话,那就需要为它们也执行上述操作,并且确保这些磁盘的名字(vol1、vol2 等)不重复。

创建Local PV

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-pv
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:  #表示该pv为 Local Persistent Volume
    path: /mnt/disks/vol1   #Local PV对应的本地磁盘路径
  nodeAffinity:  #表示该PV位于哪个node上
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - k8s-node01

上面定义的local 字段,指定了它是一个 Local Persistent Volume;而 path 字段,指定的正是这个 PV 对应的本地磁盘的路径,即:/mnt/disks/vol1。而这个磁盘存在与k8s-node01节点上,也就意味着 Pod使用这个 PV就必须运行在 node-1 上。所以nodeAffinity 字段就指定 node-1 这个节点的名字,声明PV与节点的对应关系。这正是 Kubernetes 实现“在调度的时候就考虑 Volume 分布”的主要方法。

创建这个PV

$ kubectl  create -f localpv.yaml 
persistentvolume/example-pv created
$ kubectl  get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM               STORAGECLASS    REASON   AGE
example-pv                                 5Gi        RWO            Delete           Available                       local-storage            12s

三、StorageClass的延迟绑定机制

PV 与PVC 的最佳实践,需要创建一个 StorageClass 来描述这个 PV,如下所示:

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
  • provisioner 字段定义为no-provisioner,这是因为 Local Persistent Volume 目前尚不支持 Dynamic Provisioning动态生成PV,所以我们需要提前手动创建PV

  • volumeBindingMode字段定义为WaitForFirstConsumer,它是 Local Persistent Volume 里一个非常重要的特性,即:延迟绑定。延迟绑定就是在我们提交PVC文件时,StorageClass为我们延迟绑定PV与PVC的对应关系。

延迟绑定的原因是:比如我们在当前集群上有两个相同属性的PV,它们分布在不同的节点Node1和Node2上,而我们定义的Pod需要运行在Node1节点上 ,但是StorageClass已经为Pod声明的PVC绑定了在Node2上的PV,这样的话,Pod调度就会失败,所以我们要延迟StorageClass的绑定操作。

也就是延迟到到第一个声明使用该 PVC 的 Pod 出现在调度器之后,调度器再综合考虑所有的调度规则,当然也包括每个 PV 所在的节点位置,来统一决定,这个 Pod 声明的 PVC,到底应该跟哪个 PV 进行绑定。

比如上面的Pod需要运行在node1节点上,StorageClass发现可以绑定的PV后,先不为Pod中的PVC绑定PV,而是等到Pod调度到node1节点后,再为PVC绑定当前节点运行的PV。

所以,通过这个延迟绑定机制,原本实时发生的 PVC 和 PV 的绑定过程,就被延迟到了 Pod 第一次调度的时候在调度器中进行,从而保证了这个绑定结果不会影响 Pod 的正常调度。

现在我们创建StoragClass

$ kubectl  create -f localpv-storageclass.yaml 
storageclass.storage.k8s.io/local-storage created

创建PVC

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: example-local-claim
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: local-storage

上面定义的StorageClass为“local-storage”,也就是StorageClass看到这个PVC并不会立即为它绑定PV。

创建资源

$ kubectl   create -f  local-pvc.yaml 
persistentvolumeclaim/example-local-claim created
$ kubectl  get  pvc
NAME                  STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS    AGE
example-local-claim   Pending                                                                        local-storage   103s
$ kubectl  get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM               STORAGECLASS    REASON   AGE
example-pv                                 5Gi        RWO            Delete           Available                       local-storage            51m

可以看到当前PVC的状态为Pending,PV与 PVC也没有建立绑定关系。

现在我们创建使用这个PVC的Pod

apiVersion: v1
kind: Pod
metadata:
  name: localpv-pod
spec:
  containers:
    - name: localpv-po-container
      image: nginx
      ports: 
        - containerPort: 80
          name: "http-server"
      volumeMounts: 
        - mountPath: "/usr/share/nginx/html"
          name: example-pv-storage
  volumes:
    - name: example-pv-storage
      persistentVolumeClaim:
        claimName: example-local-claim

创建这个Pod

$ kubectl  create   -f localpv-pod.yaml 
pod/localpv-pod created
$ kubectl  get pods -o wide
NAME                                      READY   STATUS    RESTARTS   AGE   IP            NODE         NOMINATED NODE   READINESS GATES
localpv-pod                               1/1     Running   0          16h   10.244.2.51   k8s-node01   <none>           <none>
$ kubectl  get pvc 
NAME                  STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS    AGE
example-local-claim   Bound    example-pv                                 5Gi        RWO            local-storage   16h
$ kubectl  get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                         STORAGECLASS    REASON   AGE
example-pv                                 5Gi        RWO            Delete           Bound    default/example-local-claim   local-storage            17h

可以看到Pod调度在“k8s-node01”节点并成功运行后,PVC和PV的状态已经Bound绑定。

现在验证文件是否可以持久化存储,进入当前这个Pod的挂载目录新建一个测试文件

$ kubectl  exec -it  localpv-pod  -- /bin/bash
$ cd /usr/share/nginx/html/
$ touch test.html
$ ls
test.html

在宿主机的挂载目录上查看是否创建

$ ls /mnt/disks/vol1/
test.html

现在我们删除或者重建这个Pod,查看宿主机上是否还存在这个测试文件

$ kubectl  delete pod localpv-pod
pod "localpv-pod" deleted
$ ls /mnt/disks/vol1/
test.html

可以看到文件是依旧存在的,这也说明,像 Kubernetes 这样构建出来的、基于本地存储的 Volume,完全可以提供容器持久化存储的功能。所以,像 StatefulSet 这样的有状态编排工具,也完全可以通过声明 Local 类型的 PV 和 PVC,来管理应用的存储状态。

删除Local PV

需要注意的是,我们上面手动创建 PV 的方式,在删除 PV 时需要按如下流程执行操作:

  • 删除使用这个 PV 的 Pod;
  • 从宿主机移除本地磁盘(比如,umount 它);
  • 删除 PVC;
  • 删除 PV。

如果不按照这个流程的话,这个 PV 的删除就会失败。

四、实战:为Prometheus创建local pv

参考资料

创建StorageClass

创建Prometheus的storageclass配置文件

# cat prometheus-data-storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: prometheus-lpv
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

创建Local PV

创建Prometheus的sc的pv配置文件,同时指定了调度节点。

使用1个StorageClass对应3个Local PV。

# 在需要调度的Prometheus的node上创建目录与赋权
mkdir /data/prometheus
chown -R 65534:65534 /data/prometheus

# cat prometheus-federate-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: prometheus-lpv-0  #第1个lpv,对应sealos-k8s-node1本地路径为/data/prometheus
spec:
  capacity:
    storage: 10Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: prometheus-lpv
  local:
    path: /data/prometheus
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - sealos-k8s-node1
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: prometheus-lpv-1  #第2个lpv,对应sealos-k8s-node2本地路径为/data/prometheus
spec:
  capacity:
    storage: 20Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: prometheus-lpv
  local:
    path: /data/prometheus
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - sealos-k8s-node2
---          
apiVersion: v1
kind: PersistentVolume
metadata:
  name: prometheus-lpv-2  #第3个lpv,对应sealos-k8s-node3本地路径为/data/prometheus
spec:
  capacity:
    storage: 10Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: prometheus-lpv
  local:
    path: /data/prometheus
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - sealos-k8s-node3

Pod使用lpv

# cat prometheus-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: prometheus
  namespace: kube-system
  labels:
    k8s-app: prometheus
    kubernetes.io/cluster-service: "true"
spec:
  serviceName: "prometheus"
  podManagementPolicy: "Parallel"
  replicas: 3
  selector:
    matchLabels:
      k8s-app: prometheus
  template:
    metadata:
      labels:
        k8s-app: prometheus
      annotations:
        scheduler.alpha.kubernetes.io/critical-pod: ''
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: k8s-app
                operator: In
                values:
                - prometheus
            topologyKey: "kubernetes.io/hostname"
      priorityClassName: system-cluster-critical
      hostNetwork: true
      dnsPolicy: ClusterFirstWithHostNet
      containers:
      - name: prometheus-server-configmap-reload
        image: "jimmidyson/configmap-reload:v0.4.0"
        imagePullPolicy: "IfNotPresent"
        args:
          - --volume-dir=/etc/config
          - --webhook-url=http://localhost:9090/-/reload
        volumeMounts:
          - name: config-volume
            mountPath: /etc/config
            readOnly: true
        resources:
          limits:
            cpu: 10m
            memory: 10Mi
          requests:
            cpu: 10m
            memory: 10Mi
      - image: prom/prometheus:v2.20.0
        imagePullPolicy: IfNotPresent
        name: prometheus
        command:
          - "/bin/prometheus"
        args:
          - "--config.file=/etc/prometheus/prometheus.yml"
          - "--storage.tsdb.path=/prometheus"
          - "--storage.tsdb.retention=24h"
          - "--web.console.libraries=/etc/prometheus/console_libraries"
          - "--web.console.templates=/etc/prometheus/consoles"
          - "--web.enable-lifecycle"
        ports:
          - containerPort: 9090
            protocol: TCP
        volumeMounts:
          - mountPath: "/prometheus"
            name: prometheus-data
          - mountPath: "/etc/prometheus"
            name: config-volume
        readinessProbe:
          httpGet:
            path: /-/ready
            port: 9090
          initialDelaySeconds: 30
          timeoutSeconds: 30
        livenessProbe:
          httpGet:
            path: /-/healthy
            port: 9090
          initialDelaySeconds: 30
          timeoutSeconds: 30
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
          limits:
            cpu: 1000m
            memory: 2500Mi
        securityContext:
            runAsUser: 65534
            privileged: true
      serviceAccountName: prometheus
      volumes:
        - name: config-volume
          configMap:
            name: prometheus-config
  volumeClaimTemplates:
    - metadata:
        name: prometheus-data
      spec:
        accessModes: [ "ReadWriteOnce" ]
        storageClassName: "prometheus-lpv"  #使用Local PV
        resources:
          requests:
            storage: 5Gi

实战:jenkins分配lpv

---
# 存储类:定义了存储的属性WaitForFirstConsumer。一个存储类可以对应多个PV
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: k8s-lpv
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

---
# PV:预备给存储类创建的PVC进行绑定
apiVersion: v1
kind: PersistentVolume
metadata:
  name: jenkins-lpv-0
spec:
  capacity:
    storage: 50Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: k8s-lpv
  local:
    path: /u01/lpv-data  #需要实现创建目录mkdir -pv /u01/lpv-data
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - knode-22-17  #注意主机名

hostpath-provisioner

上面提到,在使用Local PV时,需要手动创建PV。而hostpath-provisioner就是为了解决手动创建PV的。

hostpath-provisioner 是一种在单节点 Kubernetes 集群中动态供应 Kubernetes HostPath Volumes 的工具。基于 kubernetes-sigs/sig-storage-lib-external-provisioner/hostpath-provisioner 示例项目。提供官方 Helm Chart,部署十分方便。

  • Github 官网:https://github.com/rimusz/hostpath-provisioner
  • artifacthub 地址:https://artifacthub.io/packages/helm/rimusz/hostpath-provisioner