【Kubernetes】K8s笔记(十四):PersistentVolume 使用网络共享存储(NFS)
要想让存储卷真正能被 Pod 任意挂载,我们需要变更存储的方式,不能限定在本地磁盘,而是要改成网络存储,这样 Pod 无论在哪里运行,只要知道 IP 地址或者域名,就可以通过网络通信访问存储设备。
网络存储是一个非常热门的应用领域,有很多知名的产品,比如 AWS、Azure、Ceph,Kubernetes 还专门定义了 CSI(Container Storage Interface)规范,不过这些存储类型的安装、使用都比较复杂,在实验环境里部署难度比较高。
所以我们以 NFS (Network File System)为例学习如何在 Kubernetes 里使用网络存储,以及静态存储卷和动态存储卷的概念。
0. 安装 NFS 服务器及客户端
NFS 采用的是经典的 Client/Server 架构,需要选定一台主机作为 Server,安装 NFS 服务端;其他要使用存储的主机作为 Client,安装 NFS 客户端工具。
我这里就再安装一台虚拟机作为 NFS Server:
虚拟机地址 | 功能 |
---|---|
172.16.63.128 | Kubernetes Control-Plane |
172.16.63.129 | Kubernetes Worker Node |
172.16.63.131 | NFS Server |
要安装 NFS Server 只需要执行下面的命令:
$ sudo apt -y install nfs-kernel-server
安装好之后,需要给 NFS 指定一个存储位置,也就是网络共享目录。一般来说,应该建立一个专门的 /data
目录,在这里我使用 /data/nfs
:
$ sudo mkdir -p /data/nfs
接下配置 NFS 访问共享目录,修改 /etc/exports
,指定目录名、允许访问的网段,还有权限等参数:
/data/nfs 172.16.63.1/24(rw,sync,no_subtree_check,no_root_squash,insecure)
改好之后,需要用 exportfs -ra 通知 NFS,让配置生效,再用 exportfs -v 验证效果:
$ sudo exportfs -ra
$ sudo exportfs -v
/data/nfs 172.16.63.1/24(sync,wdelay,hide,no_subtree_check,sec=sys,rw,insecure,no_root_squash,no_all_squash)
最后使用 systemctl
启动 NFS 服务:
$ sudo systemctl start nfs-server
$ sudo systemctl enable nfs-server
$ sudo systemctl status nfs-server
● nfs-server.service - NFS server and services
Loaded: loaded (/lib/systemd/system/nfs-server.service; enabled; vendor preset: enabled)
Drop-In: /run/systemd/generator/nfs-server.service.d
└─order-with-mounts.conf
Active: active (exited) since Fri 2022-10-28 08:39:24 UTC; 8min ago
Main PID: 2677 (code=exited, status=0/SUCCESS)
CPU: 8ms
然后使用下面的命令检查 NFS 的网络挂载情况:
$ showmount -e 127.0.0.1
Export list for 127.0.0.1:
/data/nfs 172.16.63.1/24
为了让 Kubernetes 集群能够访问 NFS 存储服务,我们还需要在每个节点上都安装 NFS 客户端:
$ sudo apt -y install nfs-common
同样,在节点上可以用 showmount 检查 NFS 能否正常挂载,注意 IP 地址要写成 NFS 服务器的地址:
$ showmount -e 172.16.63.131
Export list for 172.16.63.131:
/data/nfs 172.16.63.1/24
手动测试挂载 NFS
首先在 Worker 节点上创建一个文件夹作为挂载点:$ sudo mkdir -p /tmp/nfs-test
用命令
mount
把 NFS 服务器的共享目录挂载到刚才创建的本地目录上:$ sudo -i # echo "hello, nfs!" > /tmp/nfs-test/hello # cat /tmp/nfs-test/hello hello, nfs!
回到 NFS 服务器,检查共享目录,应该会看到也出现了一个同样的文件。
1. 在 Kubernetes 中使用 NFS 存储卷
现在我们已经为 Kubernetes 配置好了 NFS 存储系统,就可以使用它来创建新的 PV 存储对象了。
手工分配一个存储卷,指定 storageClassName
为 nfs
,accessMode
设置为 ReadWriteMany
(因为 NFS 支持多个节点同时访问一个共享目录)。
因为这个存储卷是 NFS 系统,所以我们还需要在 YAML 里添加 nfs 字段,指定 NFS 服务器的 IP 地址和共享目录名。
下面我们在 NFS 服务器的 共享目录中建立一个文件夹 1gib-pv
表示一个 1GiB 的 PersistentVolume,然后使用 YAML 文件描述这个 PV:
# nfs-1gib-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-1gib-pv
spec:
storageClassName: nfs
accessModes:
- ReadWriteMany
capacity:
storage: 1Gi
nfs:
path: /data/nfs/1gib-pv
server: 172.16.63.131
然后我们创建这个 PV 对象,然后查看状态:
$ kubectl apply -f nfs-1gib-pv.yaml
persistentvolume/nfs-1gib-pv created
$ kubectl get pv -o wide
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE VOLUMEMODE
nfs-1gib-pv 1Gi RWX Retain Available nfs 22s Filesystem
注意:
spec.nfs
里的 IP 地址一定要正确,路径一定要存在(事先创建好),否则 Kubernetes 按照 PV 的描述会无法挂载 NFS 共享目录,PV 就会处于Pending
状态无法使用。
有了 PV,我们就可以定义申请存储的 PVC 对象了,它的内容和 PV 差不多,但不涉及 NFS 存储的细节,只需要用 resources.request
来表示希望要有多大的容量,这里写成 1GB,和 PV 的容量相同:
# 1gib-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-static-pvc
spec:
storageClassName: nfs
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
创建 PVC 对象之后,Kubernetes 就会根据 PVC 的描述,找到最合适的 PV:
$ kubectl apply -f 1gib-pvc.yaml
persistentvolumeclaim/nfs-static-pvc created
$ kubectl get pv -o wide
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE VOLUMEMODE
nfs-1gib-pv 1Gi RWX Retain Bound default/nfs-static-pvc nfs 8m1s Filesystem
$ kubectl get pvc -o wide
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE VOLUMEMODE
nfs-static-pvc Bound nfs-1gib-pv 1Gi RWX nfs 17s Filesystem
最后创建一个 Pod,把 PVC 挂载成它的一个 volume。在这一步我们只需要在 persistentVolumeClaim
中指定 PVC 的名称就可以了:
# nfs-static-pod.yaml
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 后我们使用 describe
命令查看 Volumes:
$ kubectl apply -f nfs-static-pod.yaml
pod/nfs-static-pod created
$ kubectl describe pod nfs-static-pod
Name: nfs-static-pod
Namespace: default
Priority: 0
Service Account: default
Node: worker1/172.16.63.129
...
Volumes:
nfs-pvc-vol:
Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
ClaimName: nfs-static-pvc
ReadOnly: false
...
Pod、PVC、PV 和 NFS 存储的关系可以用下图来形象地表示:
因为我们在 PV/PVC 里指定了 storageClassName
是 nfs
,节点上也安装了 NFS 客户端,所以 Kubernetes 就会自动执行 NFS 挂载动作,把 NFS 的共享目录 /tmp/nfs/1g-pv
挂载到 Pod 里的 /tmp
,完全不需要我们去手动管理。
现在测试一下挂载的正确性,首先我们使用命令进入 Pod:
$ kubectl exec -it pods/nfs-static-pod -- sh
进入 Pod 后我们在挂载目录建立一个文件:
/ # cd tmp
/tmp # echo Hello! This is a file created on a pod. > hello.text
/tmp #
然后在 NFS 服务器查看该文件:
$ ls
hello.text
$ cat hello.text
Hello! This is a file created on a pod.
发现 Pod 里创建的文件确实写入了共享目录。
而且因为 NFS 是一个网络服务,不会受 Pod 调度位置的影响,所以只要网络通畅,这个 PV 对象就会一直可用,数据也就实现了真正的持久化存储。
2. 动态存储卷 Provisioner
现在网络存储系统确实能够让集群里的 Pod 任意访问,数据在 Pod 销毁后仍然存在,新创建的 Pod 可以再次挂载,然后读取之前写入的数据。但是,PV 之类的对象还是需要运维人员手工管理,而且 PV 的大小也很难提前知晓、精确控制,容易出现空间不足或者空间浪费等情况。
在一个大集群里,每天可能会有几百几千个应用需要 PV 存储,如果仍然用人力来管理分配存储,管理员很可能会忙得焦头烂额,导致分配存储的工作大量积压。
为了实现 PV 创建自动化和卷分配自动化,Kubernetes 提出“动态存储卷”的概念:它可以用 StorageClass 绑定一个 Provisioner 对象,而这个 Provisioner 就是一个能够自动管理存储、创建 PV 的应用,代替了原来系统管理员的手工劳动。
目前,Kubernetes 里每类存储设备都有相应的 Provisioner 对象,对于 NFS 来说,它的 Provisioner 就是 NFS subdir external provisioner
NFS Provisioner 也是以 Pod 的形式运行在 Kubernetes 里的,在 GitHub 的 deploy 目录里是部署它所需的 YAML 文件,一共有三个,分别是 rbac.yaml
class.yaml
deployment.yaml
。
这里我将部署文件放在 nfs/provisioner
目录下。
要想在集群内运行 Provisioner,我们还要对其中两个文件进行修改:
第一个要修改的是 rbac.yaml
,它使用的是默认的 default 名字空间,应该把它改成其他的名字空间,避免与普通应用混在一起,可以用“查找替换”的方式把它统一改成 kube-system
。
然后修改 deployment.yaml
,首先要把名字空间改成和 rbac.yaml
一样,比如是 kube-system
,然后重点要修改 volumes
和 env
里的 IP 地址和共享目录名,必须和集群里的 NFS 服务器配置一样。
# nfs/provisoner/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: docker.io/chronolaw/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: 172.16.63.131
- name: NFS_PATH
value: /data/nfs
volumes:
- name: nfs-client-root
nfs:
server: 172.16.63.131
path: /data/nfs
还有一件事就是 gcr.io
上的镜像拉取困难,罗剑锋老师把它的镜像转存到了 Docker Hub 上。我们只需要更改一下镜像地址即可 image: docker.io/chronolaw/nfs-subdir-external-provisioner:v4.0.2
。
把这两个 YAML 修改好之后,我们就可以在 Kubernetes 里创建 NFS Provisioner 了。
$ kubectl apply -f rbac.yaml -f class.yaml -f deployment.yaml
serviceaccount/nfs-client-provisioner created
clusterrole.rbac.authorization.k8s.io/nfs-client-provisioner-runner created
clusterrolebinding.rbac.authorization.k8s.io/run-nfs-client-provisioner created
role.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created
rolebinding.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created
storageclass.storage.k8s.io/nfs-client created
deployment.apps/nfs-client-provisioner created
使用命令 kubectl get
,再加上名字空间限定 -n kube-system
,就可以看到 NFS Provisioner 在 Kubernetes 里运行起来了。
$ kubectl get deploy -n kube-system -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
coredns 2/2 2 2 13d coredns registry.aliyuncs.com/google_containers/coredns:v1.9.3 k8s-app=kube-dns
nfs-client-provisioner 1/1 1 1 61s nfs-client-provisioner docker.io/chronolaw/nfs-subdir-external-provisioner:v4.0.2 app=nfs-client-provisioner
$ kubectl get pods -n kube-system -l app=nfs-client-provisioner
NAME READY STATUS RESTARTS AGE
nfs-client-provisioner-7f58779d49-k78m2 1/1 Running 0 2m22s
3. 使用 NFS 动态存储卷
因为有了 Provisioner,我们就不再需要手工定义 PV 对象了,只需要在 PVC 里指定 StorageClass 对象,它再关联到 Provisioner。
我们来看一下 NFS 默认的 StorageClass 定义:
# nfs/provisioner/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"
YAML 里的关键字段是 provisioner
,它指定了应该使用哪个 Provisioner。另一个字段 parameters
是调节 Provisioner 运行的参数,需要参考文档来确定具体值,在这里的 archiveOnDelete: "false"
就是自动回收存储空间。
理解了 StorageClass 的 YAML 之后,你也可以不使用默认的 StorageClass,而是根据自己的需求,任意定制具有不同存储特性的 StorageClass,比如添加字段 onDelete: "retain"
暂时保留分配的存储,之后再手动删除。
现在我们定义一个 PVC,向系统申请 10MB 的存储空间,使用的 StorageClass 是默认的 nfs-client
:
# nfs/test/nfs-provisioner-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-dyn-10mib-pvc
spec:
storageClassName: nfs-client
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Mi
写好了 PVC,我们还是在 Pod 里用 volumes
和 volumeMounts
挂载,然后 Kubernetes 就会自动找到 NFS Provisioner,在 NFS 的共享目录上创建出合适的 PV 对象:
# nfs/test/nfs-provisioner-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: nfs-dyn-pod
spec:
volumes:
- name: nfs-dyn-10mib-vol
persistentVolumeClaim:
claimName: nfs-dyn-10mib-pvc
containers:
- name: nfs-dyn-test
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: nfs-dyn-10mib-vol
mountPath: /tmp
创建 PVC 和 Pod,然后查看集群状态:
$ kubectl apply -f nfs-provisioner-pvc.yaml -f nfs-provisioner-pod.yaml
persistentvolumeclaim/nfs-dyn-10mib-pvc created
pod/nfs-dyn-pod created
$ kubectl get pv -o wide
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE VOLUMEMODE
pvc-4a7bc325-ca6e-46ca-9f96-8d0217647019 10Mi RWX Delete Bound default/nfs-dyn-10mib-pvc nfs-client 35s Filesystem
$ kubectl get pvc -o wide
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE VOLUMEMODE
nfs-dyn-10mib-pvc Bound pvc-4a7bc325-ca6e-46ca-9f96-8d0217647019 10Mi RWX nfs-client 23s Filesystem
虽然我们没有直接定义 PV 对象,但由于有 NFS Provisioner,它就自动创建一个 PV,大小刚好是在 PVC 里申请的 10MiB。
如果这个时候再去 NFS 服务器上查看共享目录,也会发现多出了一个目录,名字与这个自动创建的 PV 一样,但加上了名字空间和 PVC 的前缀:
nfs-server:/data/nfs$ ls
1gib-pv default-nfs-dyn-10mib-pvc-pvc-4a7bc325-ca6e-46ca-9f96-8d0217647019 hello