工作原理
K8S的外部NFS驱动,可以按照其工作方式(是作为NFS server还是NFS client)分为两类:
nfs-client
也就是我们接下来演示的这一类,它通过K8S的内置的NFS驱动挂载远端的NFS服务器到本地目录;然后将自身作为storage provider,关联storage class。当用户创建对应的PVC来申请PV时,该provider就将PVC的要求与自身的属性比较,一旦满足就在本地挂载好的NFS目录中创建PV所属的子目录,为Pod提供动态的存储服务。nfs
与nfs-client不同,该驱动并不使用k8s的NFS驱动来挂载远端的NFS到本地再分配,而是直接将本地文件映射到容器内部,然后在容器内使用ganesha.nfsd来对外提供NFS服务;在每次创建PV的时候,直接在本地的NFS根目录中创建对应文件夹,并export出该子目录。
接下来我们来操作一个nfs-client驱动的例子,先对其有个直观的认识!
部署实例
这里,我们将nfs-client驱动做一个deployment部署到K8S集群中,然后对外提供存储服务。
部署nfs-client-provisioner
环境变量的PROVISIONER_NAME、NFS服务器地址、NFS对外提供服务的路径信息等需要设置好;部署所使用的yaml文件关键代码如下所示:
1 | kind: Deployment |
创建Storage Class
storage class的定义,需要注意的是:provisioner属性要等于驱动所传入的环境变量PROVISIONER_NAME的值。否则,驱动不知道知道如何绑定storage class。
yaml文件如下所示:
1 | apiVersion: storage.k8s.io/v1beta1 |
完成之后,在k8s里面就能够看到storage class的信息了:
1 | [root@dev-6 henry]# kubectl get sc |
创建PVC
这里指定了其对应的storage-class的名字为wise2c-nfs-storage,如下:
1 | kind: PersistentVolumeClaim |
可以看到PVC已经绑定了volume,如下所示(volume的名字是namespace,PVC name以及uuid的组合):
1 | [root@dev-6 ~]# kubectl get pvc |
然后,我们进入到NFS的export目录,可以看到对应该volume name的目录已经创建出来了:
1 | [root@dev-5 nfsshare]# ls |
创建pod
指定该pod使用我们刚刚创建的PVC:henry-claim:
完成之后,如果attach到pod中执行一些文件的读写操作,就可以确定pod的/mnt已经使用了NFS的存储服务了。
那么,NFS-client又是如何从代码侧面来实现这些服务的呢?我们接下来就来分析其代码实现。
实现方式
K8S 的external NFS storage驱动源码可以在这里找到:https://github.com/kubernetes-incubator/external-storage。
猜测
在分析代码之前,我们先来猜测一下storage provider是如何实现的。通过刚才的实践,我们已经大体了解了NFS provider的功能和用法,基于这些用法我们可以设想,应该至少可以有两种实现方式:
- 所有的provider启动之后都注册到K8S,K8S提供了一种调度算法,用于分析PVC指名的要求,然后调度到具体的provider上为其分配PV;
- K8S没有这样一个集中的调度算法,在每一个provider内部内部实现了一种类似于实时监测的机制,用于分析每一个PVC的需求,当自己条件符合PVC的要求时,为其创建对应的PV。
源码分析
Main函数
nfs-client的代码非常简洁,只有一个文件。其main函数的实现里面,从环境变量读取了NFS_SERVER,NFS_PATH以及PROVISIONER_NAME这些参数之后,又收集了K8S inCluster的配置信息。然后把这些信息用于生成了驱动中最重要的结构ProvisionController。然后就开始调用由库函数提供的ProvisionController的run函数了。
接口实现
在provisioner.go中,除main函数外,还为nfsProvisioner实现了两个方法:
Provision: 按照PVC的namespace、PVC name以及PVC指定的PV name,来在远端NFS上创建PV所属的目录,并返回所创建好的PV的数据结构。
Delete: 其功能恰好与Provision方法相反,其将原来为PVC创建的目录重命名为archived-的目录。之所以不直接删除,应该也是为了便于以后能够手动找回数据吧。 ProvisionController库
至此,我想大家都开始很关心,到底是驱动是如何调用这两个方法的呢?
那么,整个驱动的关键就在这里了,controller库中有这样一个函数:NewProvisionController,我们截取一小段代码来看看它都做了什么。看过kubernetes实现的人一定对这种结构非常熟悉。我们看到这里watch了所有namespace的PVC, 然后下面的handlerFuncs里面映射了三种行为的处理函数:
除了PVC资源意外,采用这种结构,NewProvisionController同样为PV和storage class的各种行为指定了对应的处理函数。添加PVC的流程图如下:
总结
因此,现在我们头脑里面就应该能够联系起所有的流程了:
- external-storage驱动库提供了一些方法,内部实现了对K8S存储资源,如PVC / PV / Storage Class 的实时watch。同时为这些资源对应的各种操作关联了相应的handler函数;
- 当k8s上对应存储资源发生变化的时候,external-storage驱动中对应的handler函数就会被调用,从而操作存储资源;
- external-storage驱动库为各个不同的存储后端提供可扩展的接口,要实现一种是有的存储驱动,就只需实现其Provision和Delete方法即可。