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

实现 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,该参数有两个取值,分别是ImmediateWaitForFirstConsumer

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 动态LocalVolume挂载本地磁盘

对应的翻译为 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

上一篇:js控制ios设备在微信打开网页时,自动播放音乐


下一篇:Openresty动态更新(无reload)TCP Upstream的原理和实现