2.14 网络共享存储 #
由于 Kubernetes 里的 Pod 经常会在集群里 “漂移”,要想让存储卷真正能被 Pod 任意挂载,就不能限定在本地磁盘,而是要改成网络存储,这样 Pod 无论在哪里运行,只要知道 IP 地址或者域名,就可以通过网络通信访问存储设备。
在网络存储中有比较简单的 NFS 系统(Network File System),可以通过 NFS 理解在 Kubernetes 里使用网络存储,以及静态存储卷和动态存储卷的概念。
2.14.1 安装 NFS 服务器 #
NFS 采用的是 Client/Server 架构,需要选定一台主机作为 Server,安装 NFS 服务端;其他要使用存储的主机作为 Client,安装 NFS 客户端工具。
可以在 Kubernetes 集群里增添一台名字叫 Storage 的服务器,在上面安装 NFS,实现网络存储、共享网盘的功能。这台 Storage 只是一个逻辑概念,在实际安装部署的时候完全可以把它合并到集群里的某台主机里。
在 Ubuntu/Debian 系统里安装 NFS 服务端很容易,使用 apt 即可:
sudo apt -y install nfs-kernel-server
安装好之后,需要给 NFS 指定一个存储位置,也就是网络共享目录。一般来说,应该建立一个专门的 /data 目录,这里使用了临时目录 /tmp/nfs:
mkdir -p /tmp/nfs
接下来需要配置 NFS 访问共享目录,修改 /etc/exports,指定目录名、允许访问的网段,还有权限等参数。把下面这行加上就行,注意目录名和 IP 地址要改成和自己的环境一致:
/tmp/nfs 192.168.14.0/24(rw,sync,no_subtree_check,no_root_squash,insecure)
改好之后,需要用 exportfs -ra 通知 NFS,让配置生效,再用 exportfs -v 验证效果:
sudo exportfs -ra
sudo exportfs -v
以上的步骤完成之后,就可以使用 systemctl 来启动 NFS 服务器了:
sudo systemctl start nfs-server
sudo systemctl enable nfs-server
sudo systemctl status nfs-server
可以使用命令 showmount 来检查 NFS 的网络挂载情况:
showmount -e 127.0.0.1
2.14.2 安装 NFS 客户端 #
NFS 服务器安装完成之后,为了让 Kubernetes 集群能够访问 NFS 存储服务,还需要在每个节点上都安装 NFS 客户端。
sudo apt -y install nfs-common
可以在节点上可以用 showmount 检查 NFS 能否正常挂载,这里的 IP 地址要写成 NFS 服务器的地址:
挂载测试 #
现在可以尝试手动挂载一下 NFS 网络存储,可以在 worker 节点创建一个目录 /tmp/test 作为挂载点:
mkdir -p /tmp/test
然后用命令 mount 把 NFS 服务器的共享目录挂载到刚才创建的本地目录上:
sudo mount -t nfs 192.168.14.142:/tmp/nfs /tmp/test
在 worker 节点 /tmp/test 目录里随便创建一个文件,比如 x.yml:
touch /tmp/test/x.yml
回到 NFS 服务器,检查共享目录 /tmp/nfs,应该会看到也出现了一个同样的文件 x.yml,这就说明 NFS 安装成功了。之后集群里的任意节点,只要通过 NFS 客户端,就能把数据写入 NFS 服务器,实现网络存储。
2.14.3 使用 NFS 存储卷 #
在配置好 NFS 存储系统后,就可以使用它来创建 PV 存储对象了。可以先手工分配一个存储卷,需要指定 storageClassName 是 nfs,而 accessModes 可以设置成 ReadWriteMany,这是由 NFS 的特性决定的,因为它支持多个节点同时访问一个共享目录。
因为存储卷是 NFS 系统,所以需要在 YAML 里添加 nfs 字段,指定 NFS 服务器的 IP 地址和共享目录名。
在 NFS 服务器的 /tmp/nfs 目录里创建了一个新的目录 1g-pv,表示分配了 1GB 的可用存储空间,相应的,PV 里的 capacity 也要设置成同样的数值,也就是 1Gi。
以下是一个使用 NFS 网络存储的 YAML 描述文件:
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-1g-pv
spec:
storageClassName: nfs
accessModes:
- ReadWriteMany
capacity:
storage: 1Gi
nfs:
path: /tmp/nfs/1g-pv
server: 192.168.14.142
spec.nfs 里的 IP 地址一定要正确,路径也要事先建好目录,不然在 Pod 使用 NFS 时会报 No such file or directory 的错误。使用命令 kubectl apply 创建 PV 对象,可以使用 kubectl get pv 查看它的状态:
有了 PV,就可以定义申请存储的 PVC 对象了,内容和 PV 差不多,但不涉及 NFS 存储的细节,只需要用 resources.request 来表示希望要有多大的容量,这里写成 1GB,和 PV 的容量相同:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-static-pvc
spec:
storageClassName: nfs
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
创建 PVC 对象之后,Kubernetes 就会根据 PVC 的描述,找到最合适的 PV,把它们 “绑定” 在一起,也就是存储分配成功:
此时可以再创建一个 Pod,把 PVC 挂载成它的一个 volume,用 persistentVolumeClaim 指定 PVC 的名字就可以了:
apiVersion: v1
kind: Pod
metadata:
name: nfs-static-pod
spec:
volumes:
- name: nfs-pvc-vol
persistentVolumeClaim:
claimName: nfs-static-pvc
containers:
- name: nfs-pvc-test
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: nfs-pvc-vol
mountPath: /tmp
Pod、PVC、PV 和 NFS 存储的关系可以用下图来表示:
因为在 PV/PVC 里指定了 storageClassName 是 nfs,节点上也安装了 NFS 客户端,所以 Kubernetes 就会自动执行 NFS 挂载动作,把 NFS 的共享目录 /tmp/nfs/1g-pv 挂载到 Pod 里的 /tmp,完全不需要去手动管理。
spec.nfs 里的路径一定要事先存在,不然在创建 Pod 时会报错
Output: mount.nfs: mounting 192.168.14.142:/tmp/nfs/1g-pv failed, reason given by server: No such file or directory
在用 kubectl apply 创建 Pod 之后,可以使用 kubectl exec 进入 Pod,再试着操作 NFS 共享目录:
可以看到,NFS 服务器的 /tmp/nfs/1g-pv 目录里有在 Pod 里创建的文件,说明文件确实写入了共享目录。
NFS 是一个网络服务,不会受 Pod 调度位置的影响,所以只要网络通畅,这个 PV 对象就会一直可用,数据也就实现了真正的持久化存储。
2.14.4 部署 NFS Provisoner #
网络存储系统确实能够让集群里的 Pod 任意访问,数据在 Pod 销毁后仍然存在,新创建的 Pod 可以再次挂载,然后读取之前写入的数据,整个过程完全是自动化的。但因为 PV 还是需要人工管理,必须要由系统管理员手动维护各种存储设备,再根据开发需求逐个创建 PV,而且 PV 的大小也很难精确控制,容易出现空间不足或者空间浪费的情况。
在一个大集群里,每天可能会有几百几千个应用需要 PV 存储,如果仍然用人力来管理分配存储,管理员很可能会忙得焦头烂额,导致分配存储的工作大量积压。
在 Kubernetes 里有一个 “动态存储卷” 的概念,它可以用 StorageClass 绑定一个 Provisioner /prəˈvɪʒənə(r)/
对象,而这个 Provisioner 就是一个能够自动管理存储、创建 PV 的应用,代替了原来系统管理员的手工劳动。有了 “动态存储卷” 的概念,手工创建的 PV 可以称为 “静态存储卷”。
Kubernetes 里每类存储设备都有相应的 Provisioner 对象,对于 NFS 来说,它的 Provisioner 就是 “NFS subdir external provisioner”,可以在 GitHub 上找到这个项目 https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner。
NFS Provisioner 也是以 Pod 的形式运行在 Kubernetes 里的,在 GitHub 的 deploy 目录里是部署它所需的 YAML 文件,一共有三个,分别是 rbac.yaml、class.yaml 和 deployment.yaml。但是这三个文件只是示例,想要在集群里真正运行起来还要修改其中的两个文件。
第一个要修改的是 rbac.yaml,它使用的是默认的 default 名字空间,应该把它改成其他的名字空间,避免与普通应用混在一起,可以用 “查找替换” 的方式把它统一改成 kube-system。
第二个要修改的是 deployment.yaml,首先要把名字空间改成和 rbac.yaml 一样,比如是 kube-system,然后要修改 volumes 和 env 里的 IP 地址和共享目录名,必须和集群里的 NFS 服务器配置一样。
按照现行环境修改后的 rbac.yaml、class.yaml 和 deployment.yaml 分别如下:
rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: kube-system
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: nfs-client-provisioner-runner
rules:
- apiGroups: [ "" ]
resources: [ "nodes" ]
verbs: [ "get", "list", "watch" ]
- apiGroups: [ "" ]
resources: [ "persistentvolumes" ]
verbs: [ "get", "list", "watch", "create", "delete" ]
- apiGroups: [ "" ]
resources: [ "persistentvolumeclaims" ]
verbs: [ "get", "list", "watch", "update" ]
- apiGroups: [ "storage.k8s.io" ]
resources: [ "storageclasses" ]
verbs: [ "get", "list", "watch" ]
- apiGroups: [ "" ]
resources: [ "events" ]
verbs: [ "create", "update", "patch" ]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: run-nfs-client-provisioner
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: kube-system
roleRef:
kind: ClusterRole
name: nfs-client-provisioner-runner
apiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: kube-system
rules:
- apiGroups: [ "" ]
resources: [ "endpoints" ]
verbs: [ "get", "list", "watch", "create", "update", "patch" ]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: kube-system
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: kube-system
roleRef:
kind: Role
name: leader-locking-nfs-client-provisioner
apiGroup: rbac.authorization.k8s.io
class.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-client
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner # or choose another name, must match deployment's env PROVISIONER_NAME'
parameters:
archiveOnDelete: "false"
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-client-provisioner
labels:
app: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: kube-system
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: nfs-client-provisioner
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
serviceAccountName: nfs-client-provisioner
containers:
- name: nfs-client-provisioner
image: k8s.gcr.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2
# image: xiaobinqt/nfs-subdir-external-provisioner:v4.0.2
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
value: k8s-sigs.io/nfs-subdir-external-provisioner
- name: NFS_SERVER
value: 192.168.14.142 #改IP地址
- name: NFS_PATH
value: /tmp/nfs #改共享目录名
volumes:
- name: nfs-client-root
nfs:
server: 192.168.14.142 #改IP地址
path: /tmp/nfs #改共享目录名
deployment.yaml 的镜像仓库用的是 gcr.io,拉取很困难,可以使用 Docker Hub 的镜像
xiaobinqt/nfs-subdir-external-provisioner:v4.0.2
。或是直接把镜像打成 tar 包上传到服务器上再 load 解包。
YAML 修改好之后,就可以在 Kubernetes 里创建 NFS Provisioner 了:
kubectl apply -f rbac.yaml
kubectl apply -f class.yaml
kubectl apply -f deployment.yaml
使用命令 kubectl get,再加上名字空间限定 -n kube-system,就可以看到 NFS Provisioner 在 Kubernetes 里运行起来了。
2.14.5 使用 NFS 动态存储卷 #
因为有了 Provisioner,就不再需要手工定义 PV 对象了,只需要在 PVC 里指定 StorageClass 对象,它再关联到 Provisioner。
NFS 默认的 StorageClass 定义:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-client
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
parameters:
archiveOnDelete: "false"
YAML 里的关键字段是 provisioner,它指定了应该使用哪个 Provisioner。另一个字段 parameters 是调节 Provisioner 运行的参数,需要参考文档来确定具体值,在这里的 archiveOnDelete: "false"
就是自动回收存储空间。
定义一个 PVC,向系统申请 10MB 的存储空间,使用的 StorageClass 是默认的 nfs-client:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-dyn-10m-pvc
spec:
storageClassName: nfs-client
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Mi
有了 PVC,还需要在 Pod 里用 volumes 和 volumeMounts 挂载,然后 Kubernetes 就会自动找到 NFS Provisioner,在 NFS 的共享目录上创建出合适的 PV 对象:
apiVersion: v1
kind: Pod
metadata:
name: nfs-dyn-pod
spec:
volumes:
- name: nfs-dyn-10m-vol
persistentVolumeClaim:
claimName: nfs-dyn-10m-pvc
containers:
- name: nfs-dyn-test
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: nfs-dyn-10m-vol
mountPath: /tmp
使用 kubectl apply 创建好 PVC 和 Pod,可以查看一下集群里的 PV 状态:
从上图可以看到,虽然没有直接定义 PV 对象,但由于有 NFS Provisioner,它就自动创建一个 PV,大小刚好是在 PVC 里申请的 10MB。
这个时候如果去 NFS 服务器上查看共享目录,会发现多出了一个目录,名字与这个自动创建的 PV 一样,但加上了名字空间和 PVC 的前缀:
Pod、PVC、StorageClass 和 Provisioner 的关联关系,可以通过下图有一个大致的了解: