4.4 K8s存储之本地持久化存储 Local Pv
LocalPV:Kubernetes直接使用宿主机的本地磁盘目录 ,来持久化存储容器的数据。它的读写性能相比于大多数远程存储来说,要好得多,尤其是SSD盘。
Local Persistent Volume 并不适用于所有应用。它的适用范围非常固定,比如:高优先级的系统应用,需要在多个不同节点上存储数据,而且对 I/O 要求较高。
典型的应用包括:分布式数据存储比如 MongoDB,分布式文件系统比如 GlusterFS、Ceph 等,以及需要在本地磁盘上进行大量数据缓存的分布式应用,其次使用 Local Persistent Volume 的应用必须具备数据备份和恢复的能力,允许你把这些数据定时备份在其他位置。
LocalPV的实现可以理解为我们前面使用的hostpath加上nodeAffinity,比如:在宿主机NodeA上提前创建好目录 ,然后在定义Pod时添加nodeAffinity=NodeA,指定Pod在我们提前创建好目录的主机上运行。
对于常规的 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 其实应该给宿主机挂载并格式化一个可用的磁盘,这里我们就在宿主机上挂载几个 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
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的storageclass配置文件
# cat prometheus-data-storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: prometheus-lpv
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
创建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
# 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
---
# 存储类:定义了存储的属性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 #注意主机名
上面提到,在使用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