什么是Local Persistent Volumes
在kubernetes 1.14版本中, Local Persistent Volumes
(以下简称LPV)已变为正式版本(GA),LPV的概念在1.7中被首次提出(alpha),并在1.10版本中升级到beat版本。现在用户终于可以在生产环境中使用LPV的功能和API了。
首先:Local Persistent Volumes
代表了直接绑定在计算节点上的一块本地磁盘。
kubernetes提供了一套卷插件(volume plugin)标准,使得k8s集群的工作负载可以使用多种块存储和文件存储。大部分磁盘插件都使用了远程存储,这是为了让持久化的数据与计算节点彼此独立,但远程存储通常无法提供本地存储那么强的读写性能。有了LPV 插件,kubernetes负载现在可以用同样的volume api,在容器中使用本地磁盘。
这跟hostPath有什么区别
hostPath是一种volume,可以让pod挂载宿主机上的一个文件或目录(如果挂载路径不存在,则创建为目录或文件并挂载)。
最大的不同在于调度器是否能理解磁盘和node的对应关系,一个使用hostPath的pod,当他被重新调度时,很有可能被调度到与原先不同的node上,这就导致pod内数据丢失了。而使用LPV的pod,总会被调度到同一个node上(否则就调度失败)。
如何使用LPV
首先 需要创建StorageClass
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
注意到这里volumeBindingMode
字段的值是WaitForFirstConsumer
。这种bindingmode意味着:
kubernetes的pv控制器会将这类pv的binding延迟,直到有一个使用了对应pvc的pod被创建出来且该pod被调度完毕。这时候才会将pv和pvc进行binding,并且这时候pv的选择会结合调度的node和pv的nodeaffinity。
接下来,提前准备好的provisioner会动态创建PV。
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
local-pv-27c0f084 368Gi RWO Delete Available local-storage 8s
local-pv-3796b049 368Gi RWO Delete Available local-storage 7s
local-pv-3ddecaea 368Gi RWO Delete Available local-storage 7s
LPV的详细内容如下:
$ kubectl describe pv local-pv-ce05be60
Name: local-pv-ce05be60
Labels: <none>
Annotations: pv.kubernetes.io/provisioned-by=local-volume-provisioner-minikube-18f57fb2-a186-11e7-b543-080027d51893
StorageClass: local-fast
Status: Available
Claim:
Reclaim Policy: Delete
Access Modes: RWO
Capacity: 1024220Ki
NodeAffinity:
Required Terms:
Term 0: kubernetes.io/hostname in [my-node]
Message:
Source:
Type: LocalVolume (a persistent volume backed by local storage on a node)
Path: /mnt/disks/vol1
Events: <none>
当然,也可以不使用provisioner,而是手动创建PV。但是必须要注意的是,LPV必须要填写nodeAffinity。 (1.10前k8s是将nodeAffinity作为annotation记录到PV中,1.10起将其独立为一个字段)
apiVersion: v1
kind: PersistentVolume
metadata:
name: example-pv
spec:
capacity:
storage: 100Gi
# volumeMode field requires BlockVolume Alpha feature gate to be enabled.
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
接下来可以创建各种workload,记得要在workload的模板中声明volumeClaimTemplates。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: local-test
spec:
serviceName: "local-service"
replicas: 3
selector:
matchLabels:
app: local-test
template:
metadata:
labels:
app: local-test
spec:
containers:
- name: test-container
image: k8s.gcr.io/busybox
command:
- "/bin/sh"
args:
- "-c"
- "sleep 100000"
volumeMounts:
- name: local-vol
mountPath: /usr/test-pod
volumeClaimTemplates:
- metadata:
name: local-vol
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "local-storage"
resources:
requests:
storage: 368Gi
注意到这里volumeClaimTemplates.spec.storageClassName
是local-storage
,即我们一开始创建的storageclass实例的名字。
使用LPV的pod的调度流程
上面这个statefulset创建后,控制器会为其创建对应的PVC,并且会为PVC查找符合条件的PV,但是由于我们在local-storage
中配置了WaitForFirstConsumer
,所以控制器不会处理pvc和pv的bind;
同时,调度器在调度该pod时,predicate算法中也会根据PVC的要求去找到可用的PV,并且会过滤掉“与LPV的affinity”不匹配的node。最终,调度器发现:
- pv:
example-pv
满足了pvc的要求; - node:
example-node
满足了pv:example-pv
的nodeAffinity要求。
于是乎调度器尝试将pv和pvc bind起来,并且对pod进行重新调度。
重新调度pod时调度器发现pod的pvc资源得到了满足(都bound了pv),且bound的pv的nodeAffinity与node:example-node
匹配。于是将pod调度到node:example-node
上。完成调度。
如何创建LPV
- 在机器上创建目录:
mkdir -p /mnt/disks/ssd1
- 在机器上执行命令,将某个卷挂载到该目录:
mount -t /dev/vdc /mnt/disks/ssd1
- 在集群中创建对应的storageClass. 参见上文。
- 手动创建本地卷的PV,或者通过provisioner去自动创建。手动创建的模板见上文。
如何删除LPV
对于已经被bind并被pod使用的LPV,删除一定要按照流程来 , 要不然会删除失败:
- 删除使用这个pv的pod
- 从node上移除这个磁盘(按照一个pv一块盘)
- 删除pvc
- 删除pv
LPV延迟绑定部分的代码解读
所有的关键在于volumeBinder
这个结构,它继承了SchedulerVolumeBinder
接口,包括:
type SchedulerVolumeBinder interface {
FindPodVolumes(pod *v1.Pod, node *v1.Node)
AssumePodVolumes(assumedPod *v1.Pod, nodeName string)
BindPodVolumes(assumedPod *v1.Pod) error
GetBindingsCache() PodBindingCache
}
FindPodVolumes
了解调度器原理的应该知道,调度器的predicate算法,在调度pod时,会逐个node的去进行predicate,以确认这个node是否可以调度。我们称之为预选阶段。
VolumeBindingChecker
是一个检查器,在调度器的算法工厂初始化的最后一步,会向工厂中注册检查算法,这样调度器在进行predicate时,最后一步会执行对volumeBinding的检查。我们看func (c *VolumeBindingChecker) predicate
方法就能看到,这里面执行了FindPodVolumes
,并且判断返回的几个值是否为true,或err是否为空:
unboundSatisfied, boundSatisfied, err := c.binder.Binder.FindPodVolumes(pod, node)
boundSatisfied 为false表示pod绑定的pv 与当前计算的node亲和性不过关。
unboundSatisfied 为false表示pod中申明的未bound的pvc,在集群内的pv中找不到可以匹配的。
就这样,调度器会反复去重试调度,反复执行FindPodVolumes
,直到我们(或者provisoner)创建出了PV,比如这时新建的PV,其nodeAffinity对应到了node A。这次调度,在对node A进行predicate计算时,发现pod中申明的、未bound的pvc,在集群中有合适的pv,且该pv的nodeAffinity就是node A,于是返回的unboundSatisfied
为 true, 调度器最终找到了一个合适的node。
那么,调度器接下来要对pod执行assume,在对pod assume之前,调度器要先对pod中bind的volume进行assume。见func (sched *Scheduler) assumeAndBindVolumes(assumed *v1.Pod, host string) error
。这个函数里,我们调用了volumeBinder
的AssumePodVolumes
方法。
AssumePodVolumes
assume是假设的意思,顾名思义,这个方法会先在调度器的缓存中,假定pod已经调度到node A上,对缓存中的pv、pvc、binding等资源进行更新,看是否能成功,它会返回一些讯息:
allBound, bindingRequired, err := sched.config.VolumeBinder.Binder.AssumePodVolumes(assumed, host)
allBound 为true表示所有的pv、pvc,在缓存中已经是bind。如果为false,会最终导致本次调度失败。
bindingRequired 为true表示有一些pv需要和pvc bind起来。如果为true,调度器会向volumeBinder
的BindQueue
中写入一个用例。这个队列会被一个worker轮询,并进行对应的工作。
什么工作呢? BindPodVolumes
BindPodVolumes
调度器在Run起来的时候,会启动一个协程,反复执行bindVolumesWorker
。在这个worker中我们可以看到,他尝试从volumeBinder
的BindQueue
中取出任务,进行BindPodVolumes
,成功则该任务Done,失败则报错重试。
阅读BindPodVolumes
这个方法,很简单,从缓存中找到对应的pod、pv、pvc等内容,更新到APIserver中。
由于我们在AssumePodVolumes
中已经更新了缓存,所以这里更新到apiserver的操作,会真正地将pv和pvc bind起来。
之后呢?
在worker中我们看到,如果BindPodVolumes
成功,依然会构造一个pod调度失败的事件,并更新pod的状态为PodScheduled
,这么做是为了将pod放回调度队列,让调度器再去调度一次。
我们假设pod中只申明了一个LPV,在刚刚描述的这次BindPodVolumes
操作中已经在apiserver中对这个LPV,和pod中的pvc进行了bind。那么,下一次调度器调度pod时,在AssumePodVolumes
时会发现已经allBound
,调度器会继续后续的操作,最终pod被成功地调度(创建出Binding资源,apiserver将pod的nodeName更新)。
pv控制器不管吗?
创建PVC后,pv控制器会有一个worker:syncUnboundClaim
去管理未bind的pvc。这个worker中,对于spec.VolumeName
不为空的pvc,会去进行bind操作,确保pv和pvc绑定起来;对于spec.VolumeName
为空的pvc,会去检查是否延迟绑定,并查找集群中适合该pvc的pv(这里没有node的概念,所以在查找时更多地是根据selector和AccessModes去过滤)。可以在
func findMatchingVolume(
claim *v1.PersistentVolumeClaim,
volumes []*v1.PersistentVolume,
node *v1.Node,
excludedVolumes map[string]*v1.PersistentVolume,
delayBinding bool) (*v1.PersistentVolume, error)
中找到过滤的逻辑。这里我们只要知道:对于延迟绑定的pvc,我们会过滤掉所有的pv,并最后发出一个WaitForFirstConsumer
的event结束worker。
可见,pv控制器对于延迟调度的pvc放任自流了。我们在findMatchingVolume
方法中也可以看到官方的一段注释:
if node == nil && delayBinding {
// PV controller does not bind this claim.
// Scheduler will handle binding unbound volumes
// Scheduler path will have node != nil
continue
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。