本期文章来自才云科技(Caicloud)CTO 邓德源的技术原创。
1
Overview
Kubernetes 1.7 不会引入过多新功能,比较重要的几个特性包括 Priority API、CRI 的增强以及 Federation 的部分功能。此外,计划中还将提供本地存储管理,主要分为两个层面:
本地系统容量的管理。Kubernetes 的主分区主要包含 Kubelet 的根目录(/var/lib/kubelet),/var/log 目录等。此外,容器镜像,容器读写层,容器日志,Kubernetes 空数据卷(emptyDir)等都需要消耗该空间(默认情况下)。在 Kubernetes 1.7 中,社区将尝试把此类资源集中管理起来,并作为调度资源。
本地数据卷管理。本地数据卷管理的主要内容是将非主分区的其他分区全部作为本地持久化数据卷供 Kubernetes 调度使用,遵循 Kubernetes 的 PV/PVC 模型。
2
Local Volume API
在本文中,我们集中介绍本地数据卷管理,一起思考如何设计本地持久化数据卷,后续再介绍 Kubernetes 准备如何进行容量管理。
首先,在设计之初,我们需要思考本地数据卷的场景和用例,社区认为的两个主要应用场景是分布式文件系统和数据库,因为两者都需要高性能和可靠性,比如在云的环境里,本地 SSD 比挂载的数据盘性能高出许多;另外,本地磁盘在多数情况下成本更低,适合数据量大的场景。
了解场景之后,开始进入 API 设计阶段。本地数据卷的管理是一个比较复杂的工程,需要支持文件系统和块存储。不同的文件系统具有不同的特性(比如 xfs 支持 quota,而目前 ext4 并不支持 quota),挂载选项也大相径庭。
并且,本地存储卷是目前为止唯一一个涉及到同时支持文件系统和块存储的插件方案(ceph 可以算一个,但是 cephfs 和 RBD 目前是两个插件)。为了兼容各种可能的情况,经过长时间讨论,目前 API 已经基本定型:
有了上述定义,我们可以创建相对应的 Persistent Volume:
这里有几个注意事项:
spec.local.path 可以是任意的路径,但是 Kubernetes 推荐把一块磁盘或者分区挂载到该分区。使用一块磁盘除了可以做到容量隔离之外,还可以做到性能隔离;如果性能隔离对应用场景不重要,那么可以把磁盘划分为多个分区,因此可以创建多个 PV。Kubernetes 的这种方式将控制权全部交给了用户,用户可以更加灵活的配置系统。
spec.local.path 目前只能是路径,后续 Kubernetes 会支持块设备,这样一来 spec.local.path 会被重用,即当 PV 是一个挂载点时,path 是一个路径;当 PV 是一个块设备时,path 代表设备路径。另一种方案是采用更深层次的键值,例如 spec.local.fs.path, spec.local.block.path,这样的好处是可以支持多重属性,比如 spec.local.fs.fsType,但是社区目前并没有这么去做,主要原因是考虑适配 CSI (container storage interface)。
spec.capacity 目前是 informational。如果我们采用的是磁盘或者分区的方式,那么该值是磁盘或分区的大小,也就做到了容量隔离。但如果我们采用的是任意的目录,目前 Kubernetes 并不会限制该目录可以使用的大小。
上述 API 仅仅定义了 local PV 的 path 属性,但是不同于其他存储插件,local PV 的一大特点就是它一定会与本地所绑定,因此,我们需要给 PV 再加一个属性,告知系统 PV 属于哪个节点。我们可以简简单单地在 PV 内加上 NodeName 的属性,如下所示:
这里最大的问题在于,我们将 PV 的拓扑结构限定在节点层面,但是“本地”的含义并不是说存储一定是分配在某台机器上,我们可以说:这个存储是属于该 rack 的本地存储。那么对于 rack 内的机器而言,该存储就是他们的本地存储。鉴于此,local volume 的 API 需要设计得更加通用:
上述 Yaml 与最开始介绍的配置表达的相同的内容,但是提供了更加灵活的表达方式,同时也复用了 Kubernetes 中已经存在的 node affinity 的概念。
3
Local Volume Scheduling
Kubernetes PV/PVC 在设计时,忽略了与调度器的交互部分,导致 PV/PVC 的绑定与 Pod 调度毫无关系。在 “remote volume“ 的情况下问题并不大,因为 Pod 被调度后,可以动态挂载数据卷,但是在本地数据卷的情况下,问题已经凸显出来。当用户创建 PV 和 PVC 之后,由于本地数据卷的特性,任何要求某个特定 PVC 的 Pod 实际上已经被调度了。
例如,管理员创建一个名为 super-pv 的数据卷,该数据卷存在于节点 Kube-node-1 上;当用户创建一个名为 claim-it 的 PVC 并被 Kubernetes 绑定在 super-pv 上之后,任何使用 PVC 的 Pod 实际上已经被调度到了 Kube-node-1 上。
假设此时,Kube-node-1 的资源无法满足 Pod 的需求(例如 CPU 足够),那么 Pod 会一直处于 Pending 状态,即使另外一台机器 Kube-node-2 有足够的资源,也有满足需求的本地数据卷。
在 Kubernetes 1.7 中,本地数据卷管理暂时不会处理该问题(会设计一个外部控制器来定期查询该错误状态,并根据一定的策略重新调度)。后续的解决方案暂无定论,其中一个可行的方案是将绑定逻辑放在调度器中:
PV/PVC 的绑定被延迟到调度时刻,即当真有 Pod 使用某个 PVC 时再进行绑定
调度器通过 PVC.storageclass 找到可以使用的 PV,并拿到每个 PV 的 affinity
调度器综合考虑 Pod 的调度需求和 PV 的 affinity 进行调度
如果没有满足需求的 Node,调度器通过和 PV provisioner 交互,动态创建 PV
如果我们从整体的设计上来看,该问题并非只会出现在本地存储上,任何需要调度器和 PV/PVC 绑定控制器交互的地方都会有问题。
例如,在多 zone 的情况下,PV/PVC 控制器并不会考虑一个 PV 需要创建在哪个 zone 里面;因此,很有可能控制器选择了一个 zone,但是该 zone 并不满足 Pod 的需求(例如 affinity 需求,或者所有节点资源都不够)。
由于一个 zone 可能包含多个机器,因此问题被缩小,但仍然存在。如果我们仔细思考可以看出,实际上 node 就是一个 ”tiny zone“,问题的本质实际上是一样的。
4
Local Volume Static Provisioner
本地数据卷的创建由新的 Kubernetes 组件处理(local volume provisioner addon),该组件将以 DaemonSet 的方式部署。
Provisioner 有两个核心组件:
Discovery: discovery 组件的功能是接收用户配置的信息,然后创建 PV。例如,当用户将 “/var/lib/kubelet/localstorages” 作为本地数据卷的目录后,provisioner 会为该目录下的每一个目录创建一个新的 PV,并添加正确的拓扑信息。
Deleter: deleter 负责处理 PV 状态改变。当 deleter 发现其管理的 PV 进入了 Released 状态,deleter 会清理数据并将 PV 从 Kubernetes API 中删除。此时,Discovery 发现 PV 被删除,会重新创建新的 PV,从而达到回收的效果。
Provisioner 后续还可以完成各种错误汇报,容量检查等功能,在 kubernetes 1.7 中会提供 alpha 版本。
5
Future
尽管 Kubernetes 1.7 中本地存储卷只能说处于基本可以试一试的状态,社区后续会为这个功能投入大量精力,具体实现内容已经涉及到 1.8、1.9 版,包括 dynamic provisioner, fsGroup support, SELinux support, taints/toleration, local PV monitoring, block storage support 等,相信后续会有更多进展。
相关阅读:
https://github.com/kubernetes...
https://github.com/kubernetes...