【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
posted @ 2022-10-31 16:39  joexu01  阅读(1061)  评论(0)    收藏  举报