当前位置:主页 > 查看内容

实现 Kubernetes 动态LocalVolume挂载本地磁盘

发布时间:2021-07-07 00:00| 位朋友查看

简介:实现 Kubernetes 动态LocalVolume挂载本地磁盘前言 在 Kubernetes 体系中,存在大量的存储插件用于实现基于网络的存储系统挂载,比如NFS、GFS、Ceph和云厂商的云盘设备。但在某些用户的环境中,可能无法或没有必要搭建复杂的网络存储系统,需要一个更简单的……
实现 Kubernetes 动态LocalVolume挂载本地磁盘前言

在 Kubernetes 体系中,存在大量的存储插件用于实现基于网络的存储系统挂载,比如NFS、GFS、Ceph和云厂商的云盘设备。但在某些用户的环境中,可能无法或没有必要搭建复杂的网络存储系统,需要一个更简单的存储方案。另外网络存储系统避免不了性能的损耗,然而对于一些分布式数据库,其在应用层已经实现了数据同步和冗余,在存储层只需要一个高性能的存储方案

在这些情况下如何实现Kubernetes 应用的数据持久化呢?

HostPath Volume

对 Kubernetes 有一定使用经验的伙伴首先会想到HostPath Volume,这是一种可以直接挂载宿主机磁盘的Volume实现,将容器中需要持久化的数据挂载到宿主机上,它当然可以实现数据持久化。然而会有以下几个问题:

(1)HostPath Volume与节点无关,意味着在多节点的集群中,Pod的重新创建不会保障调度到原来的节点,这就意味着数据丢失。于是我们需要搭配设置调度属性使Pod始终处于某一个节点,这在带来配置复杂性的同时还破坏了Kubernetes的调度均衡度。

(2)HostPath Volume的数据不易管理,当Volume不需要使用时数据无法自动完成清理从而形成较多的磁盘浪费。

Local Persistent Volume

Local Persistent Volume 在 Kubernetes 1.14中完成GA。相对于HostPath Volume,Local Persistent Volume 首先考虑解决调度问题。使用了Local Persistent Volume 的Pod调度器将使其始终运行于同一个节点。用户不需要在额外设置调度属性。并且它在第一次调度时遵循其他调度算法,一定层面上保持了均衡度。

遗憾的是 Local Persistent Volume 默认不支持动态配置。在社区方案中有提供一个静态PV配置器sig-storage-local-static-provisioner,其可以达成的效果是管理节点上的磁盘生命周期和PV的创建和回收。虽然可以实现PV的创建,但它的工作模式与常规的Provisioners,它不能根据PVC的需要动态提供PV,需要在节点上预先准备好磁盘和PV资源。

如何在此方案的基础上进一步简化,在节点上基于指定的数据目录,实现动态的LocalVolume挂载呢?

技术方案

需要达成的效果如下:

(1)基于Local Persistent Volume 实现的基础思路;

(2)实现各节点的数据目录的管理;

(3)实现动态 PV 分配;

StorageClass定义
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
 name: rainbondslsc
provisioner: rainbond.io/provisioner-sslc
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer

其中有一个关键性参数volumeBindingMode,该参数有两个取值,分别是Immediate 和 WaitForFirstConsumer

Immediate 模式下PVC与PV立即绑定,主要是不等待相关Pod调度完成,不关心其运行节点,直接完成绑定。相反的 WaitForFirstConsumer模式下需要等待Pod调度完成后进行PV绑定。因此PV创建时可以获取到Pod的运行节点。

我们需要实现的 provisioner 工作在 WaitForFirstConsumer 模式下,在创建PV时获取到Pod的运行节点,调用该节点的驱动服务创建磁盘路径进行初始化,进而完成PV的创建。

Provisioner的实现

Provisioner分为两个部分,一个是控制器部分,负责PV的创建和生命周期,另一部分是节点驱动服务,负责管理节点上的磁盘和数据。

PV控制器部分

控制器部分实现的代码参考: Rainbond 本地存储控制器

控制器部分的主要逻辑是从 Kube-API 监听 PersistentVolumeClaim 资源的变更,基于spec.storageClassName字段判断资源是否应该由当前控制器管理。如果是走以下流程:
(1)基于PersistentVolumeClaim获取到StorageClass资源,例如上面提到的rainbondslsc。

(2)基于StorageClass的provisioner值判定处理流程。

(3)从PersistentVolumeClaim资源中的Annotations配置 volume.kubernetes.io/selected-node 获取PVC所属Pod的运行节点。该值是由调度器设置的,这是一个关键信息获取。

if ctrl.kubeVersion.AtLeast(utilversion.MustParseSemantic("v1.11.0")) {
 // Get SelectedNode
 if nodeName, ok := claim.Annotations[annSelectedNode]; ok {
 selectedNode, err = ctrl.client.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) // TODO (verult) cache Nodes
 if err != nil {
 err = fmt.Errorf("failed to get target node: %v", err)
 ctrl.eventRecorder.Event(claim, v1.EventTypeWarning, "ProvisioningFailed", err.Error())
 return err
 // Get AllowedTopologies
 allowedTopologies, err = ctrl.fetchAllowedTopologies(claimClass)
 if err != nil {
 err = fmt.Errorf("failed to get AllowedTopologies from StorageClass: %v", err)
 ctrl.eventRecorder.Event(claim, v1.EventTypeWarning, "ProvisioningFailed", err.Error())
 return err
 }

(4)调用节点服务创建对应存储目录或独立磁盘。

path, err := p.createPath(options)
 if err != nil {
 if err == dao.ErrVolumeNotFound {
 return nil, err
 return nil, fmt.Errorf("create local volume from node %s failure %s", options.SelectedNode.Name, err.Error())
 if path == "" {
 return nil, fmt.Errorf("create local volume failure,local path is not create")
 }

(5) 创建对应的PV资源。

pv := v1.PersistentVolume{
 ObjectMeta: metav1.ObjectMeta{
 Name: options.PVName,
 Labels: options.PVC.Labels,
 Spec: v1.PersistentVolumeSpec{
 PersistentVolumeReclaimPolicy: options.PersistentVolumeReclaimPolicy,
 AccessModes: options.PVC.Spec.AccessModes,
 Capacity: v1.ResourceList{
 v1.ResourceName(v1.ResourceStorage): options.PVC.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)],
 PersistentVolumeSource: v1.PersistentVolumeSource{
 HostPath: v1.HostPathVolumeSource{
 Path: path,
 MountOptions: options.MountOptions,
 NodeAffinity: v1.VolumeNodeAffinity{
 Required: v1.NodeSelector{
 NodeSelectorTerms: []v1.NodeSelectorTerm{
 MatchExpressions: []v1.NodeSelectorRequirement{
 Key: "kubernetes.io/hostname",
 Operator: v1.NodeSelectorOpIn,
 Values: []string{options.SelectedNode.Labels["kubernetes.io/hostname"]},
 }

其中关键性参数是设置PV的NodeAffinity参数,使其绑定在选定的节点。然后使用 HostPath 类型的PersistentVolumeSource指定挂载的路径。

当PV资源删除时,根据PV绑定的节点进行磁盘资源的释放:

nodeIP := func() string {
 for _, me := range pv.Spec.NodeAffinity.Required.NodeSelectorTerms[0].MatchExpressions {
 if me.Key != "kubernetes.io/hostname" {
 continue
 return me.Values[0]
 return ""
 if nodeIP == "" {
 logrus.Errorf("storage class: rainbondslsc; name: %s; node ip not found", pv.Name)
 return
 if err := deletePath(nodeIP, path); err != nil {
 logrus.Errorf("delete path: %v", err)
 return
 }
节点驱动服务

节点驱动服务主要提供两个API,分配磁盘空间和释放磁盘空间。在实现上,简化方案则是直接在指定路径下创建子路径和释放子路径。较详细的方案可以像 sig-storage-local-static-provisioner 一样,实现对节点上存储设备的管理,包括发现、初始化、分配、回收等等。

使用方式

Rainbond中,使用者仅需指定挂载路径和选择本地存储即可。

对应的翻译为 Kubernetes 资源后PVC配置如下:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
 labels:
 app_id: 6f67c68fc3ee493ea7d1705a17c0744b
 creater_id: "1614686043101141901"
 creator: Rainbond
 name: gr39f329
 service_alias: gr39f329
 service_id: deb5552806914dbc93646c7df839f329
 tenant_id: 3be96e95700a480c9b37c6ef5daf3566
 tenant_name: 2c9v614j
 version: "20210302192942"
 volume_name: log
 name: manual3432-gr39f329-0
 namespace: 3be96e95700a480c9b37c6ef5daf3566
spec:
 accessModes:
 - ReadWriteOnce
 resources:
 requests:
 storage: 500Gi
 storageClassName: rainbondslsc
 volumeMode: Filesystem
总结

基于上诉的方案,我们可以自定义实现一个基础的动态LocalVolume,适合于集群中间件应用使用。这也是云原生应用管理平台 Rainbond 中本地存储的实现思路。在该项目中有较多的 Kubernetes 高级用法实践封装,研究源码访问:https://github.com/goodrain/rainbond


本文转自网络,原文链接:https://developer.aliyun.com/article/785119
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文


随机推荐