提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
要想让存储卷真正能被 Pod 任意挂载,我们需要变更存储的方式,不能限定在本地磁盘,而是要改成网络存储,这样 Pod 无论在哪里运行,只要知道 IP 地址或者域名,就可以通过网络通信访问存储设备。
网络存储是一个非常热门的应用领域,有很多知名的产品,比如 AWS、Azure、Ceph,Kubernetes
还专门定义了 CSI(Container Storage Interface)
规范,不过这些存储类型的安装、使用都比较复杂,在实验环境里部署难度比较高。
所以我们以 NFS (Network File System)
为例学习如何在 Kubernetes 里使用网络存储,以及静态存储卷和动态存储卷的概念。
提示:以下是本篇文章正文内容,下面案例可供参考
一、安装 NFS 服务器及客户端
NFS
采用的是经典的 Client/Server
架构,需要选定一台主机作为 Server
,安装 NFS
服务端;其他要使用存储的主机作为 Client
,安装 NFS 客户端工具。
我这里就再安装一台虚拟机作为 NFS Server
:
虚拟机地址 | 功能 |
---|---|
10.0.0.11 | Kubernetes Control-Plane |
10.0.0.12 | Kubernetes Worker Node |
10.0.0.13 | NFS Server |
要安装 NFS Server 只需要执行下面的命令:
# os centos7.9 如果是其他系统可能安装的命令会有些变化
sudo yum install nfs-utils
sudo sudo systemctl enable nfs-server.service --now
安装好之后,需要给 NFS 指定一个存储位置,也就是网络共享目录。一般来说,应该建立一个专门的 s数据 目录,在这里我使用 /tmp/nfs
:
sudo mkdir -p /tmp/nfs
接下配置 NFS 访问共享目录,修改 /etc/exports
,指定目录名、允许访问的网段,还有权限等参数:
/tmp/nfs 10.0.0.0/24(rw,sync,no_subtree_check,no_root_squash,insecure)
改好之后,需要用 exportfs -ra
通知 NFS,让配置生效,再用 exportfs -v
验证效果:
$ sudo exportfs -ra
$ sudo exportfs -v
[root@k8s-console ~]# exportfs -v
/tmp/nfs 10.0.0.0/24(sync,wdelay,hide,no_subtree_check,sec=sys,rw,insecure,no_root_squash,no_all_squash)
然后使用下面的命令检查 NFS 的网络挂载情况:
[root@k8s-console ~]# showmount -e 127.0.0.1
Export list for 127.0.0.1:
/tmp/nfs 10.0.0.0/24
为了让 Kubernetes 集群能够访问 NFS 存储服务,我们还需要在每个节点上都安装 NFS 客户端:
sudo yum install nfs-utils
同样,在节点上可以用 showmount 检查 NFS 能否正常挂载,注意 IP 地址要写成 NFS 服务器的地址:
[root@k8s-work01 ~]# showmount -e 10.0.0.13
Export list for 10.0.0.13:
/tmp/nfs 10.0.0.0/24
手动测试挂载 NFS
首先在 Worker 节点上创建一个文件夹作为挂载点:
sudo mkdir -p /tmp/nfs-test
用命令 mount 把 NFS 服务器的共享目录挂载到刚才创建的本地目录上:
sudo mount -t nfs 10.0.0.13:/tmp/nfs /tmp/test
最后测试一下,我们在 /tmp/test 里随便创建一个文件,比如 x.yml:
touch /tmp/test/x.yml
再回到 NFS 服务器,检查共享目录 /tmp/nfs,应该会看到也出现了一个同样的文件 x.yml,这就说明 NFS 安装成功了。之后集群里的任意节点,只要通过 NFS 客户端,就能把数据写入 NFS 服务器,实现网络存储。
二、在 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: /tmp/nfs/1gib-pv
server: 10.0.0.13
然后我们创建这个 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
来表示希望要有多大的容量,这里写成 1GI,和 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: k8s-work01/10.0.0.12
...
Volumes:
nfs-pvc-vol:
Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
ClaimName: nfs-static-pvc
ReadOnly: false
...
因为我们在 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 123455 > x.text
/tmp #
然后在 NFS 服务器查看该文件:
$ ls
x.text
$ cat x.text
123455
发现 Pod 里创建的文件确实写入了共享目录。
而且因为 NFS 是一个网络服务,不会受 Pod 调度位置的影响,所以只要网络通畅,这个 PV 对象就会一直可用,数据也就实现了真正的持久化存储。
三、动态存储卷 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: 10.0.0.13
- name: NFS_PATH
value: /tmp/nfs
volumes:
- name: nfs-client-root
nfs:
server: 10.0.0.13
path: /tmp/nfs
还有一件事就是 gcr.io 上的镜像拉取困难, 我们只需要更改一下镜像地址即可 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
四、使用 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 对象:
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 x.txt
总结
- 在 Kubernetes 集群里,网络存储系统更适合数据持久化,NFS 是最容易使用的一种网络存储系统,要事先安装好服务端和客户端。
- 可以编写 PV 手工定义 NFS 静态存储卷,要指定 NFS 服务器的 IP 地址和共享目录名。
- 使用 NFS 动态存储卷必须要部署相应的 Provisioner,在 YAML 里正确配置 NFS 服务器。
- 动态存储卷不需要手工定义 PV,而是要定义 StorageClass,由关联的 Provisioner 自动创建 PV 完成绑定。