Kubernetes支持几十种类型的后端存储卷,其中有几种存储卷总是给人一种分不清楚它们之间有什么区别的感觉,尤其是local与hostPath这两种存储卷类型,看上去都像是node本地存储方案嘛。当然,还另有一种volume类型是emptyDir,也有相近之处。
在Docker容器时代,我们就对Volume很熟悉了,一般来说我们是通过创建Volume数据卷,然后挂载到指定容器的指定路径下,以实现容器数据的持久化存储或者是多容器间的数据共享,当然这里说的都是单机版的容器解决方案。
进入到容器集群时代后,我们看到Kubernetes按时间顺序先后提供了emptyDir、hostPath和local的本地磁盘存储卷解决方案。emptyDir、hostPath都是Kubernetes很早就实现和支持了的技术,local volume方式则是从k8s v1.7才刚刚发布的alpha版本,目前在k8s v1.10中发布了local volume的beta版本,部分功能在早期版本中并不支持。
在展开之前,我们先讨论一个问题,就是既然都已经实现容器云平台了,我们为什么还要关注这几款本地存储卷的货呢?粗略归纳了下,有以下几个原因:
我们并不是说分布式存储服务不好,很多公司在云平台建设的实践中,往往是需要结合使用几种通用的与专用的存储解决方案,才能最终满足大部分的使用需求。
所以,如果这里有一款场景适合你的话,不妨了解一下这几款本地存储卷的功能特点、使用技巧与异同。
emptyDir类型的Volume在Pod分配到Node上时被创建,Kubernetes会在Node上自动分配一个目录,因此无需指定宿主机Node上对应的目录文件。 这个目录的初始内容为空,当Pod从Node上移除时,emptyDir中的数据会被永久删除。
注:容器的crashing事件并不会导致emptyDir中的数据被删除。
根据官方给出的最佳使用实践的建议,emptyDir可以在以下几种场景下使用:
默认情况下,emptyDir可以使用任何类型的由node节点提供的后端存储。如果你有特殊的场景,需要使用tmpfs作为emptyDir的可用存储资源也是可以的,只需要在创建emptyDir卷时增加一个emptyDir.medium字段的定义,并赋值为"Memory"即可。
注:在使用tmpfs文件系统作为emptyDir的存储后端时,如果遇到node节点重启,则emptyDir中的数据也会全部丢失。同时,你编写的任何文件也都将计入Container的内存使用限制。
我们在测试k8s环境中创建一个emptyDir volume的使用示例。
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
containers:
- image: busybox
name: test-emptydir
command: [ "sleep", "3600" ]
volumeMounts:
- mountPath: /data
name: data-volume
volumes:
- name: data-volume
emptyDir: {}
查看下创建出来的pod,这里只截取了与volume有关的部分,其他无关内容直接省略:
# kubectl describe pod test-pod
Name: test-pod
Namespace: default
Node: kube-node2/172.16.10.102
......
Environment: <none>
Mounts:
/data from data-volume (rw)
......
Volumes:
data-volume:
Type: EmptyDir (a temporary directory that shares a pod's lifetime)
Medium:
......
可以进入到容器中查看下实际的卷挂载结果:
# kubectl exec -it test-pod -c test-emptydir /bin/sh
hostPath类型则是映射node文件系统中的文件或者目录到pod里。在使用hostPath类型的存储卷时,也可以设置type字段,支持的类型有文件、目录、File、Socket、CharDevice和BlockDevice。
使用场景:
注意事项:
下面我们在测试k8s环境中创建一个hostPath volume使用示例。
apiVersion: v1
kind: Pod
metadata:
name: test-pod2
spec:
containers:
- image: busybox
name: test-hostpath
command: [ "sleep", "3600" ]
volumeMounts:
- mountPath: /test-data
name: test-volume
volumes:
- name: test-volume
hostPath:
# directory location on host
path: /data
# this field is optional
type: Directory
查看下pod创建结果,观察volumes部分:
# kubectl describe pod test-pod2
Name: test-pod2
Namespace: default
Node: kube-node2/172.16.10.102
......
Mounts:
/test-data from test-volume (rw)
......
Volumes:
test-volume:
Type: HostPath (bare host directory volume)
Path: /data
HostPathType: Directory
......
我们登录到容器中,进入挂载的/test-data目录中,创建个测试文件。
# kubectl exec -it test-pod2 -c test-hostpath /bin/sh
/ # echo 'testtesttest' > /test-data/test.log
/ # exit
我们在运行该pod的node节点上,可以看到如下的文件和内容。
[root@kube-node2 test-data]# cat /test-data/test.log
testtesttest
现在,我们把该pod删除掉,再看看node节点上的hostPath使用的目录与数据会有什么变化。
[root@kube-node1 ~]# kubectl delete pod test-pod2
pod "test-pod2" deleted
到运行原pod的node节点上查看如下。
[root@kube-node2 test-data]# ls -l
total 4
-rw-r--r-- 1 root root 13 Nov 14 00:25 test.log
[root@kube-node2 test-data]# cat /test-data/test.log
testtesttest
有时我们需要搭建一个单节点的k8s测试环境,就利用到hostPath作为后端的存储卷,模拟真实环境提供PV、StorageClass和PVC的管理功能支持。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
namespace: kube-system
name: standard
annotations:
storageclass.beta.kubernetes.io/is-default-class: "true"
labels:
addonmanager.kubernetes.io/mode: Reconcile
provisioner: kubernetes.io/host-path
这是一个很新的存储类型,建议在k8s v1.10+以上的版本中使用。该local volume类型目前还只是beta版。
Local volume 允许用户通过标准PVC接口以简单且可移植的方式访问node节点的本地存储。 PV的定义中需要包含描述节点亲和性的信息,k8s系统则使用该信息将容器调度到正确的node节点。
官方推荐在使用local volumes时,创建一个StorageClass并把volumeBindingMode字段设置为“WaitForFirstConsumer”。虽然local volumes还不支持动态的provisioning管理功能,但我们仍然可以创建一个StorageClass并使用延迟卷绑定的功能,将volume binding延迟至pod scheduling阶段执行。
这样可以确保PersistentVolumeClaim绑定策略将Pod可能具有的任何其他node节点约束也进行评估,例如节点资源要求、节点选择器、Pod亲和性和Pod反亲和性。
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
配置local volume后,可以使用一个外部的静态配置器来帮助简化本地存储的管理。 Provisioner 配置程序将通过为每个卷创建和清理PersistentVolumes来管理发现目录下的卷。
Local storage provisioner要求管理员在每个节点上预配置好local volumes,并指明该local volume是属于以下哪种类型:
一个local volume,可以是挂载到node本地的磁盘、磁盘分区或目录。
Local volumes虽然可以支持创建静态PersistentVolume,但到目前为止仍不支持动态的PV资源管理。
这意味着,你需要自己手动去处理部分PV管理的工作,但考虑到至少省去了在创建pod时手写PV YAML配置文件的工作,这个功能还是很值得的。
apiVersion: v1
kind: PersistentVolume
metadata:
name: example-pv
spec:
capacity:
storage: 100Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
storageClassName: local-storage
local:
path: /mnt/disks/ssd1
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- example-node
local volume仍受node节点可用性方面的限制,因此并不适用于所有应用程序。 如果node节点变得不健康,则local volume也将变得不可访问,使用这个local volume的Pod也将无法运行。 使用local voluems的应用程序必须能够容忍这种降低的可用性以及潜在的数据丢失,是否会真得导致这个后果将取决于node节点底层磁盘存储与数据保护的具体实现了。
local-volume项目及地址:https://github.com/kubernetes-incubator/external-storage/tree/master/local-volume
如果使用block块设备,则需要启用Alpha的功能特性:k8s v1.10+
$ export KUBE_FEATURE_GATES="BlockVolume=true"
注:如果是已经部署好的k8s v1.10+集群,需要为几个主要组件均开启对该特性的支持后,才能使用block块设备功能。如果k8s是低于1.10版本,则还需要启用其它的几个功能特性,因为在低版本中这些功能特性还都是alpha版本的。
根据大家搭建k8s的方法的不同,下面提供了四种情况下的配置说明。
使用kube-up.sh启动的GCE集群将自动格式化并挂载所请求的Local SSDs,因此您可以使用预先生成的部署规范部署配置器并跳至步骤4,除非您要自定义配置器规范或存储类。
$ NODE_LOCAL_SSDS_EXT=<n>,<scsi|nvme>,fs cluster/kube-up.sh
$ kubectl create -f provisioner/deployment/kubernetes/gce/class-local-ssds.yaml
$ kubectl create -f provisioner/deployment/kubernetes/gce/provisioner_generated_gce_ssd_volumes.yaml
GKE集群将自动格式化并挂载所请求的Local SSDs。在GKE documentation中有更详细的说明。
然后,跳至步骤4。
说明:在我们使用测试环境中,是一套3节点的k8s测试环境,为了模拟测试local volume功能,直接结合使用了下面option4中提供的ram disks测试方法,创建了3个tmpfs格式的文件系统挂载资源。
(1)创建/mnt/disks目录,并在该目录下挂载几个子目录。下面是使用三个ram disks做一个真实存储卷的模拟测试。
$ mkdir /mnt/fast-disks
$ for vol in vol1 vol2 vol3;
do
mkdir -p /mnt/fast-disks/$vol
mount -t tmpfs $vol /mnt/fast-disks/$vol
done
(2)创建单机k8s本地测试集群
$ ALLOW_PRIVILEGED=true LOG_LEVEL=5 FEATURE_GATES=$KUBE_FEATURE_GATES hack/local-up-cluster.sh
要延迟卷绑定直到pod调度并处理单个pod中的多个本地PV,必须创建StorageClass并将volumeBindingMode设置为WaitForFirstConsumer。
# 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
$ kubectl create -f provisioner/deployment/kubernetes/example/default_example_storageclass.yaml
配置一个外部的静态配置器。
(1)生成Provisioner的ServiceAccount,Roles,DaemonSet和ConfigMap规范,并对其进行自定义配置。
此步骤使用helm模板生成需要的配置规格。 有关设置说明,请参阅helm README。
使用默认值生成Provisioner的配置规格,请运行:
helm template ./helm/provisioner > ./provisioner/deployment/kubernetes/provisioner_generated.yaml
如果是使用自定义的配置文件的话:
helm template ./helm/provisioner --values custom-values.yaml > ./provisioner/deployment/kubernetes/provisioner_generated.yaml
(2)部署Provisioner
如果用户对Provisioner的yaml文件的内容感到满意,就可以使用kubectl创建Provisioner的DaemonSet和ConfigMap了。
# kubectl create -f ./provisioner/deployment/kubernetes/provisioner_generated.yaml
configmap "local-provisioner-config" created
daemonset.extensions "local-volume-provisioner" created
serviceaccount "local-storage-admin" created
clusterrolebinding.rbac.authorization.k8s.io "local-storage-provisioner-pv-binding" created
clusterrole.rbac.authorization.k8s.io "local-storage-provisioner-node-clusterrole" created
clusterrolebinding.rbac.authorization.k8s.io "local-storage-provisioner-node-binding" created
(3)检查已自动发现的local volumes
一旦启动,外部static provisioner将发现并自动创建出 local-volume PVs。
我们查看下上面测试中创建出的PVs有哪些:
# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
local-pv-436f0527 495Mi RWO Delete Available fast-disks 2m
local-pv-77a4ffb0 495Mi RWO Delete Available fast-disks 2m
local-pv-97f7ec5c 495Mi RWO Delete Available fast-disks 2m
local-pv-9f0ddba3 495Mi RWO Delete Available fast-disks 2m
local-pv-a0dfdc91 495Mi RWO Delete Available fast-disks 2m
local-pv-a52333e3 495Mi RWO Delete Available fast-disks 2m
local-pv-bed86926 495Mi RWO Delete Available fast-disks 2m
local-pv-d037a0d1 495Mi RWO Delete Available fast-disks 2m
local-pv-d26c3252 495Mi RWO Delete Available fast-disks 2m
查看某一个PV的详细描述信息:
# kubectl describe pv local-pv-436f0527
Name: local-pv-436f0527
Labels: <none>
Annotations: pv.kubernetes.io/provisioned-by=local-volume-provisioner-kube-node2-c3733876-b56f-11e8-990b-080027395360
Finalizers: [kubernetes.io/pv-protection]
StorageClass: fast-disks
Status: Available
Claim:
Reclaim Policy: Delete
Access Modes: RWO
Capacity: 495Mi
Node Affinity:
Required Terms:
Term 0: kubernetes.io/hostname in [kube-node2]
Message:
Source:
Type: LocalVolume (a persistent volume backed by local storage on a node)
Path: /mnt/fast-disks/vol2
Events: <none>
参照前文介绍local volume概念的章节中已经讲解过的PersistentVolume使用示例。
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: example-local-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Mi
storageClassName: fast-disks
# 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
# kubectl describe pvc example-local-claim
Name: example-local-claim
Namespace: default
StorageClass: fast-disks
Status: Pending
Volume:
Labels: <none>
Annotations: <none>
Finalizers: [kubernetes.io/pvc-protection]
Capacity:
Access Modes:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal WaitForFirstConsumer 6s (x6 over 59s) persistentvolume-controller waiting for first consumer to be created before binding
apiVersion: v1
kind: Pod
metadata:
name: local-pvc-pod
spec:
containers:
- image: busybox
name: test-local-pvc
command: [ "sleep", "3600" ]
volumeMounts:
- mountPath: /data
name: data-volume
volumes:
- name: data-volume
persistentVolumeClaim:
claimName: example-local-claim
创建并查看:
# kubectl create -f example-local-pvc-pod.yaml
pod "local-pvc-pod" created
# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
client1 1/1 Running 67 64d 172.30.80.2 kube-node3
local-pvc-pod 1/1 Running 0 2m 172.30.48.6 kube-node1
查看pod中容器挂载PVC的配置详情,这里只截取了部分信息:
# kubectl describe pod local-pvc-pod
Name: local-pvc-pod
Namespace: default
Node: kube-node1/172.16.10.101
Start Time: Thu, 15 Nov 2018 16:39:30 +0800
Labels: <none>
Annotations: <none>
Status: Running
IP: 172.30.48.6
Containers:
test-local-pvc:
......
Mounts:
/data from data-volume (rw)
/var/run/secrets/kubernetes.io/serviceaccount from default-token-qkhcf (ro)
Conditions:
Type Status
Initialized True
Ready True
PodScheduled True
Volumes:
data-volume:
Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
ClaimName: example-local-claim
ReadOnly: false
......
进入容器中查看挂载的数据卷:
[root@kube-node1 ~]# kubectl exec -it local-pvc-pod -c test-local-pvc /bin/sh
/ # ls
bin data dev etc home proc root sys tmp usr var
/ # df -h
Filesystem Size Used Available Use% Mounted on
overlay 41.0G 8.1G 32.8G 20% /
tmpfs 64.0M 0 64.0M 0% /dev
tmpfs 495.8M 0 495.8M 0% /sys/fs/cgroup
vol3 495.8M 0 495.8M 0% /data
再回过头来看下PVC的状态,已经变成了Bound:
# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
example-local-claim Bound local-pv-a0dfdc91 495Mi RWO fast-disks 1h
在上面的实验过程中不知道大家有没有发现一处问题,就是我们在定义PVC时是指定的申请50Mi的空间,而实际挂载到测试容器上的存储空间是495.8M,刚好是我们在某个node节点上挂载的一个文件系统的全部空间。
为什么会这样呢?这就是我们所使用的这个local persistent volume外部静态配置器的功能局限性所在了。它不支持动态的PV空间申请管理。
也就是说,虽然通过这个静态PV配置器,我们省去了手写PV YAML文件的痛苦,但仍然需要手工处理这项工作:
那如果以前给某容器分配的一个存储空间不够用了怎么办?
给大家的一个建议是使用Linux下的LVM(逻辑分区管理)来管理每个node节点上的本地磁盘存储空间。
有几点会与上面的配置方法上不同。
首先,是要在k8s主要的组件上均开启用于支持block块设备的特性。
KUBE_FEATURE_GATES="BlockVolume=true"
其次是,定义一个"Block"类型的volumeMode PVC,为容器申请一个"Block"类型的PV。
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: example-block-local-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Mi
volumeMode: Block
storageClassName: fast-disks
当您想要停用本地卷时,这是一个可行的工作流程。
参考资料:
https://github.com/kubernetes-incubator/external-storage/tree/master/local-volume
https://kubernetes.io/docs/concepts/storage/volumes/