【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 存储对象了。

手工分配一个存储卷,指定 storageClassNamenfsaccessMode 设置为 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 里指定了 storageClassNamenfs,节点上也安装了 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,然后重点要修改 volumesenv 里的 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 里用 volumesvolumeMounts 挂载,然后 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
posted @ 2022-10-31 16:39  joexu01  阅读(692)  评论(0编辑  收藏  举报