K8S External-NFS-Storage 简析

工作原理

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kind: Deployment
...
spec:
serviceAccount: nfs-client-provisioner
containers:
- name: nfs-client-provisioner
image: registry.cn-hangzhou.aliyuncs.com/wise2c/nfs-client-provisioner:devel
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
value: wise2c.com/nfs
- name: NFS_SERVER
value: <Masked-NFS-Server-IP>
- name: NFS_PATH
value: /var/nfsshare
volumes:
- name: nfs-client-root
nfs:
server: <Masked-NFS-Server-IP>
path: /var/nfsshare

创建Storage Class

storage class的定义,需要注意的是:provisioner属性要等于驱动所传入的环境变量PROVISIONER_NAME的值。否则,驱动不知道知道如何绑定storage class。

yaml文件如下所示:

1
2
3
4
5
apiVersion: storage.k8s.io/v1beta1
kind: StorageClass
metadata:
name: wise2c-nfs-storage
provisioner: wise2c.com/nfs

完成之后,在k8s里面就能够看到storage class的信息了:

1
2
3
4
[root@dev-6 henry]# kubectl get sc
NAME TYPE
standard kubernetes.io/aws-ebs
wise2c-nfs-storage wise2c.com/nfs

创建PVC

这里指定了其对应的storage-class的名字为wise2c-nfs-storage,如下:

1
2
3
4
5
6
7
8
9
10
11
12
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: henry-claim
annotations:
volume.beta.kubernetes.io/storage-class: "wise2c-nfs-storage"
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Mi

可以看到PVC已经绑定了volume,如下所示(volume的名字是namespace,PVC name以及uuid的组合):

1
2
3
[root@dev-6 ~]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESSMODES STORAGECLASS AGE
henry-claim Bound default-henry-claim-pvc-c265cbd9-74d1-11e7-9990-00163e122a49 1Mi RWX wise2c-nfs-storage 3h

然后,我们进入到NFS的export目录,可以看到对应该volume name的目录已经创建出来了:

1
2
3
[root@dev-5 nfsshare]# ls
default-henry-claim-pvc-c265cbd9-74d1-
11e7-9990-00163e122a49

创建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的流程图如下:

  • 总结

    因此,现在我们头脑里面就应该能够联系起所有的流程了:

    1. external-storage驱动库提供了一些方法,内部实现了对K8S存储资源,如PVC / PV / Storage Class 的实时watch。同时为这些资源对应的各种操作关联了相应的handler函数;
    2. 当k8s上对应存储资源发生变化的时候,external-storage驱动中对应的handler函数就会被调用,从而操作存储资源;
    3. external-storage驱动库为各个不同的存储后端提供可扩展的接口,要实现一种是有的存储驱动,就只需实现其Provision和Delete方法即可。
0%