存储一直是容器运行的关键部分,Kubernetes 为此做了很多努力,从一开始的 Pod Volumes、PV(Persistent Volumes) 与 PVC(Persistent Volume Claim),到 StorageClass 与 Dynamic Provisioning,再到现在 “out-of-tree” 的 CSI(Container Storage Interface),Kubernetes 社区一直在演进存储的实现。

前面基础的就不讲了,我们从 StorageClass 与 Dynamic Provisioning 开始了解。

关于 StorageClass 与 Dynamic Provisioning

StorageClass 为存储提供了“类”的概念,使得 PVC 可以申请不同类别的 PV,以满足用户不同质量、不同策略要求的存储需求。但仅仅是这样还不够,我们还需要手动去创建存储,创建 PV 并与之绑定。所以 StorageClass 还有一个功能就是动态卷供应(Dynamic Provisioning),通过它,Kubernetes 可以根据用户的需求,自动创建其需要的存储。

如何使用

我们需要创建 StorageClass 对象,通过 provisioner 属性指定所用的动态供应的种类:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp2

创建好以后,所有指定这个 StorageClass 的 PVC 都会动态分配 PV:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: example
  namespace: default
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: standard

当然,也需要些其他的配置,比如 aws-ebs 需要在启动参数中加入 --cloud-provider=aws。Glusterfs 需要在集群节点中预先安装好分布式存储等。具体请参考官方手册或 Google,这里不赘述了。

External provisioner

官方提供了许多 Provisioner 的实现:AWSElasticBlockStore、AzureFile、Glusterfs 等等,这些都是 “in-tree” 的,所以官方也在实验一些 external provisioner 的实现方式。在 kubernetes-incubator/external-storage 这个仓库中,就有一些孵化中的项目,不过随着 CSI 的出现,应该已经孵死了。官方也正在将 “in-tree” 的存储实现迁移到 CSI 上。

如何实现

我们根据 external-storage 仓库中的项目,简单的分析一下如何自定义一个 Dynamic Provisioner。

其实这个仓库中的项目都很简单,文件没有几个,代码也没有几行。这是因为它们都是基于官方社区的 library 实现的,它实现了 Provisioner Controller 的整个流程,包括监听、创建 PV 资源等,我们只需要实现 Provisioner 接口的两个方法就可以:

// Provisioner is an interface that creates templates for PersistentVolumes
// and can create the volume as a new resource in the infrastructure provider.
// It can also remove the volume it created from the underlying storage
// provider.
type Provisioner interface {
	// Provision creates a volume i.e. the storage asset and returns a PV object
	// for the volume
	Provision(ProvisionOptions) (*v1.PersistentVolume, error)
	// Delete removes the storage asset that was created by Provision backing the
	// given PV. Does not delete the PV object itself.
	//
	// May return IgnoredError to indicate that the call has been ignored and no
	// action taken.
	Delete(*v1.PersistentVolume) error
}

Provision 方法需要根据给定的数据,分配存储,响应 PV 对象。Delete 方法需要在 PV 删除时,也删除对应存储中的数据。

我们选择仓库中的 nfs 项目来进行详细的分析,它不同于其他 client 类项目,它还维护了一份 nfs server,使得它可以不基于其他外部存储服务。可以在 main 函数中看到,通过 runServer flag 判断是否需要启动服务,默认为 true

	if *runServer {
		......
		go func() {
			for {
				// This blocks until server exits (presumably due to an error)
				err = server.Run(ganeshaLog, ganeshaPid, ganeshaConfig)
				if err != nil {
					glog.Errorf("NFS server Exited Unexpectedly with err: %v", err)
				}

				// take a moment before trying to restart
				time.Sleep(time.Second)
			}
		}()
		// Wait for NFS server to come up before continuing provisioner process
		time.Sleep(5 * time.Second)
	}

随后通过 Provisioner ControllerRun 方法启动 Provisioner 服务:

	// Create the provisioner: it implements the Provisioner interface expected by
	// the controller
	nfsProvisioner := vol.NewNFSProvisioner(exportDir, clientset, outOfCluster, *useGanesha, ganeshaConfig, *enableXfsQuota, *serverHostname, *maxExports, *exportSubnet)

	// Start the provision controller which will dynamically provision NFS PVs
	pc := controller.NewProvisionController(
		clientset,
		*provisioner,
		nfsProvisioner,
		serverVersion.GitVersion,
	)

	pc.Run(wait.NeverStop)

NewNFSProvisioner 返回的是实现了 Provisioner 接口的结构体:

type nfsProvisioner struct {
  ......
}

var _ controller.Provisioner = &nfsProvisioner{}

接下来就看下如何实现的 Provision 方法:

// options 里包含创建 pv 的数据,pvName、pvc、sc、selectedNode 等
func (p *nfsProvisioner) Provision(options controller.ProvisionOptions) (*v1.PersistentVolume, error) {
  // 在这里进行验证,创建目录等操作
	volume, err := p.createVolume(options)
	if err != nil {
		return nil, err
	}

	annotations := make(map[string]string)
  ......

	pv := &v1.PersistentVolume{
		ObjectMeta: metav1.ObjectMeta{
			Name:        options.PVName,
			Labels:      map[string]string{},
			Annotations: annotations,
		},
		Spec: v1.PersistentVolumeSpec{
			PersistentVolumeReclaimPolicy: *options.StorageClass.ReclaimPolicy,
			AccessModes:                   options.PVC.Spec.AccessModes,
			Capacity: v1.ResourceList{
				v1.ResourceName(v1.ResourceStorage): options.PVC.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)],
			},
			PersistentVolumeSource: v1.PersistentVolumeSource{
				NFS: &v1.NFSVolumeSource{
					Server:   volume.server,
					Path:     volume.path,
					ReadOnly: false,
				},
			},
			MountOptions: options.StorageClass.MountOptions,
		},
	}

	return pv, nil
}

func (p *nfsProvisioner) createVolume(options controller.ProvisionOptions) (volume, error) {
	// 在这里验证剩余磁盘空间是否超出请求大小,只计算当前剩余
  gid, rootSquash, mountOptions, err := p.validateOptions(options)
	if err != nil {
		return volume{}, fmt.Errorf("error validating options for volume: %v", err)
	}
  ......
  // 根据 pvc 创建目录
	path := path.Join(p.exportDir, options.PVName)

	err = p.createDirectory(options.PVName, gid)
	if err != nil {
		return volume{}, fmt.Errorf("error creating directory for volume: %v", err)
	}
  ......
}


func (p *nfsProvisioner) validateOptions(options controller.ProvisionOptions) (string, bool, string, error) {
  ......
	var stat syscall.Statfs_t
	if err := syscall.Statfs(p.exportDir, &stat); err != nil {
		return "", false, "", fmt.Errorf("error calling statfs on %v: %v", p.exportDir, err)
	}
	capacity := options.PVC.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)]
	requestBytes := capacity.Value()
	available := int64(stat.Bavail) * int64(stat.Bsize)
	if requestBytes > available {
		return "", false, "", fmt.Errorf("insufficient available space %v bytes to satisfy claim for %v bytes", available, requestBytes)
	}

	return gid, rootSquash, mountOptions, nil
}

然后是 Delete 方法的实现:

func (p *nfsProvisioner) Delete(volume *v1.PersistentVolume) error {
  ......
  // pv 删除后,删除对应的目录
	err = p.deleteDirectory(volume)
	if err != nil {
		return fmt.Errorf("error deleting volume's backing path: %v", err)
	}
  ......
	return nil
}

这里只是简单的讲解下 Provisioner 的实现,省略了其他一些比如 xfs quota 等操作,有兴趣的可以去项目中看一下。顺便提一下,这个项目虽然部署了 nfs server,但没有部署成分布式存储,局限性很大,毕竟只是实验中的项目,生产环境慎用。

后记

碰巧在项目中接触到了 nfs 这个 Provisioner,并且经过测试及源码分析验证了这个项目不可用。经过查阅学习之后写下了这篇文章,算是为以后学习 CSI 作准备吧。